Completed
Push — master ( 618bcd...4a6efe )
by Bart
01:11
created

Contour.interpolate_travel_time()   C

Complexity

Conditions 7

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 26
rs 5.5
cc 7
1
import sys
2
import os
3
import math
4
5
from timeit import default_timer as timer
6
from multiprocessing import Process, Queue
7
8
import numpy
9
from scipy.spatial import KDTree
10
11
import matplotlib.pyplot as plt
12
from matplotlib.figure import Figure
13
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
14
15
import geojson
16
import geojsoncontour
17
import togeojsontiles
18
19
import nsmaps
20
from nsmaps.logger import logger
21
22
23
TIPPECANOE_DIR = '/usr/local/bin/'
24
25
26
def dotproduct(v1, v2):
27
    return sum((a * b) for a, b in zip(v1, v2))
28
29
def length(v):
30
    return math.sqrt(dotproduct(v, v))
31
32
def angle(v1, v2):
33
    return math.acos(dotproduct(v1, v2) / (length(v1) * length(v2)))
34
35
36
class ContourData(object):
37
    def __init__(self):
38
        self.Z = None
39
        self.index_begin = 0
40
41
42
class ContourPlotConfig(object):
43
    def __init__(self):
44
        self.stepsize_deg = 0.005
45
        self.n_processes = 4
46
        self.cycle_speed_kmh = 18.0
47
        self.n_nearest = 20
48
        self.lon_start = 3.0
49
        self.lat_start = 50.5
50
        self.delta_deg = 6.5
51
        self.lon_end = self.lon_start + self.delta_deg
52
        self.lat_end = self.lat_start + self.delta_deg / 2.0
53
        self.min_angle_between_segments = 7
54
55
    def print_bounding_box(self):
56
        print(
57
            '[[' + str(self.lon_start) + ',' + str(self.lat_start) + '],' \
58
            '[' + str(self.lon_start) + ',' + str(self.lat_end) + '],' \
59
            '[' + str(self.lon_end) + ',' + str(self.lat_end) + '],' \
60
            '[' + str(self.lon_end) + ',' + str(self.lat_start) + '],' \
61
            '[' + str(self.lon_start) + ',' + str(self.lat_start) + ']]' \
62
        )
63
64
65
class TestConfig(ContourPlotConfig):
66
    def __init__(self):
67
        super().__init__()
68
        self.stepsize_deg = 0.005
69
        self.n_processes = 4
70
        self.lon_start = 4.8
71
        self.lat_start = 52.0
72
        self.delta_deg = 1.0
73
        self.lon_end = self.lon_start + self.delta_deg
74
        self.lat_end = self.lat_start + self.delta_deg / 2.0
75
        self.min_angle_between_segments = 7
76
        self.latrange = []
77
        self.lonrange = []
78
        self.Z = [[]]
79
80
81
class Contour(object):
82
    def __init__(self, departure_station, stations, config, data_dir):
83
        self.departure_station = departure_station
84
        self.stations = stations
85
        self.config = config
86
        self.data_dir = data_dir
87
88
    def create_contour_data(self, filepath):
89
        if self.departure_station.has_travel_time_data():
90
            self.stations.travel_times_from_json(self.departure_station.get_travel_time_filepath())
91
            if os.path.exists(filepath):
92
                logger.error('Output file ' + filepath + ' already exists. Will not override.')
93
                return
94
        else:
95
            logger.error('Input file ' + self.departure_station.get_travel_time_filepath() + ' not found. Skipping station.')
96
97
        start = timer()
98
        numpy.set_printoptions(3, threshold=100, suppress=True)  # .3f
99
100
        altitude = 0.0
101
        self.lonrange = numpy.arange(self.config.lon_start, self.config.lon_end, self.config.stepsize_deg)
102
        self.latrange = numpy.arange(self.config.lat_start, self.config.lat_end, self.config.stepsize_deg / 2.0)
103
        self.Z = numpy.zeros((int(self.lonrange.shape[0]), int(self.latrange.shape[0])))
104
        gps = nsmaps.utilgeo.GPS()
105
106
        positions = []
107
        for station in self.stations:
108
            x, y, z = gps.lla2ecef([station.get_lat(), station.get_lon(), altitude])
109
            positions.append([x, y, z])
110
111
        logger.info('starting spatial interpolation')
112
113
        # tree to find nearest neighbors
114
        tree = KDTree(positions)
115
116
        queue = Queue()
117
        processes = []
118
        if self.config.n_nearest > len(self.stations):
119
            self.config.n_nearest = len(self.stations)
120
        for i in range(0, self.config.n_processes):
121
            begin = i * len(self.latrange)/self.config.n_processes
122
            end = (i+1)*len(self.latrange)/self.config.n_processes
123
            latrange_part = self.latrange[begin:end]
124
            process = Process(target=self.interpolate_travel_time, args=(queue, i, self.stations.stations, tree, gps, latrange_part,
125
                                                                         self.lonrange, altitude, self.config.n_nearest, self.config.cycle_speed_kmh))
126
            processes.append(process)
127
128
        for process in processes:
129
            process.start()
130
131
        # get from the queue and append the values
132
        for i in range(0, self.config.n_processes):
133
            data = queue.get()
134
            index_begin = data.index_begin
135
            begin = int(index_begin*len(self.latrange)/self.config.n_processes)
136
            end = int((index_begin+1)*len(self.latrange)/self.config.n_processes)
137
            self.Z[0:][begin:end] = data.Z
138
139
        for process in processes:
140
            process.join()
141
142
        end = timer()
143
        logger.info('finished spatial interpolation in ' + str(end - start) + ' [sec]')
144
145
        # self.create_geojson(filepath, max_zoom, min_zoom, stroke_width)
146
147
    def create_geojson(self, filepath, min_zoom=0, max_zoom=12, stroke_width=1, n_contours=41):
148
        figure = Figure(frameon=False)
149
        FigureCanvas(figure)
150
        ax = figure.add_subplot(111)
151
        levels = numpy.linspace(0, 200, num=n_contours)
152
        # contours = plt.contourf(lonrange, latrange, Z, levels=levels, cmap=plt.cm.plasma)
153
        contours = ax.contour(self.lonrange, self.latrange, self.Z, levels=levels, cmap=plt.cm.jet, linewidths=3)
154
        # cbar = figure.colorbar(contours, format='%.1f')
155
        # plt.savefig('contour_example.png', dpi=150)
156
        ndigits = len(str(int(1.0 / self.config.stepsize_deg))) + 1
157
        logger.info('converting contour to geojson file: ' + filepath)
158
        geojsoncontour.contour_to_geojson(
159
            contour=contours,
160
            geojson_filepath=filepath,
161
            contour_levels=levels,
162
            min_angle_deg=self.config.min_angle_between_segments,
163
            ndigits=ndigits,
164
            unit='min',
165
            stroke_width=stroke_width
166
        )
167
        with open(filepath, 'r') as jsonfile:
168
            feature_collection = geojson.load(jsonfile)
169
            for feature in feature_collection['features']:
170
                feature["tippecanoe"] = {"maxzoom": str(int(max_zoom)), "minzoom": str(int(min_zoom))}
171
        dump = geojson.dumps(feature_collection, sort_keys=True)
172
        with open(filepath, 'w') as fileout:
173
            fileout.write(dump)
174
175
        cbar = figure.colorbar(contours, format='%d', orientation='horizontal')
176
        cbar.set_label('Travel time [minutes]')
177
        # cbar.set_ticks(self.config.colorbar_ticks)
178
        ax.set_visible(False)
179
        figure.savefig(
180
            filepath.replace('.geojson', '') + "_colorbar.png",
181
            dpi=90,
182
            bbox_inches='tight',
183
            pad_inches=0,
184
            transparent=True,
185
        )
186
187
    def create_geojson_tiles(self, filepaths, tile_dir, min_zoom=0, max_zoom=12):
188
        bound_box_filepath = os.path.join(self.data_dir, 'bounding_box.geojson')
189
        assert os.path.exists(bound_box_filepath)
190
        filepaths.append(bound_box_filepath)
191
        togeojsontiles.geojson_to_mbtiles(
192
            filepaths=filepaths,
193
            tippecanoe_dir=TIPPECANOE_DIR,
194
            mbtiles_file='out.mbtiles',
195
            minzoom=min_zoom,
196
            maxzoom=max_zoom,
197
            full_detail=10,
198
            lower_detail=9,
199
            min_detail=7
200
        )
201
        logger.info('converting mbtiles to geojson-tiles')
202
        togeojsontiles.mbtiles_to_geojsontiles(
203
            tippecanoe_dir=TIPPECANOE_DIR,
204
            tile_dir=tile_dir,
205
            mbtiles_file='out.mbtiles',
206
        )
207
        logger.info('DONE: create contour json tiles')
208
209
    @staticmethod
210
    def interpolate_travel_time(q, position, stations, kdtree, gps, latrange, lonrange, altitude, n_nearest, cycle_speed_kmh):
211
        # n_nearest: check N nearest stations as best start for cycle route
212
        logger.info('interpolate_travel_time')
213
        Z = numpy.zeros((int(latrange.shape[0]), int(lonrange.shape[0])))
214
        for i, lat in enumerate(latrange):
215
            if i % (len(latrange) / 10) == 0:
216
                logger.debug(str(int(i / len(latrange) * 100)) + '%')
217
218
            for j, lon in enumerate(lonrange):
219
                x, y, z = gps.lla2ecef([lat, lon, altitude])
220
                distances, indexes = kdtree.query([x, y, z], n_nearest)
221
                min_travel_time = sys.float_info.max
222
                for distance, index in zip(distances, indexes):
223
                    if stations[index].travel_time_min is None:
224
                        continue
225
                    travel_time = stations[index].travel_time_min + distance / 1000.0 / cycle_speed_kmh * 60.0
226
                    if travel_time < min_travel_time:
227
                        min_travel_time = travel_time
228
                Z[i][j] = min_travel_time
229
        data = ContourData()
230
        data.index_begin = position
231
        data.Z = Z
232
        q.put(data)
233
        logger.info('end interpolate_travel_time')
234
        return
235
236
237
# def contour_to_json(contour, filename, contour_labels, min_angle=2, ndigits=5):
238
#     # min_angle: only create a new line segment if the angle is larger than this angle, to compress output
239
#     collections = contour.collections
240
#     with open(filename, 'w') as fileout:
241
#         total_points = 0
242
#         total_points_original = 0
243
#         collections_json = []
244
#         contour_index = 0
245
#         assert len(contour_labels) == len(collections)
246
#         for collection in collections:
247
#             paths = collection.get_paths()
248
#             color = collection.get_edgecolor()
249
#             paths_json = []
250
#             for path in paths:
251
#                 v = path.vertices
252
#                 x = []
253
#                 y = []
254
#                 v1 = v[1] - v[0]
255
#                 x.append(round(v[0][0], ndigits))
256
#                 y.append(round(v[0][1], ndigits))
257
#                 for i in range(1, len(v) - 2):
258
#                     v2 = v[i + 1] - v[i - 1]
259
#                     diff_angle = math.fabs(angle(v1, v2) * 180.0 / math.pi)
260
#                     if diff_angle > min_angle:
261
#                         x.append(round(v[i][0], ndigits))
262
#                         y.append(round(v[i][1], ndigits))
263
#                         v1 = v[i] - v[i - 1]
264
#                 x.append(round(v[-1][0], ndigits))
265
#                 y.append(round(v[-1][1], ndigits))
266
#                 total_points += len(x)
267
#                 total_points_original += len(v)
268
#
269
#                 # x = v[:,0].tolist()
270
#                 # y = v[:,1].tolist()
271
#                 paths_json.append({u"x": x, u"y": y, u"linecolor": color[0].tolist(), u"label": str(int(contour_labels[contour_index])) + ' min'})
272
#             contour_index += 1
273
#
274
#             if paths_json:
275
#                 collections_json.append({u"paths": paths_json})
276
#         collections_json_f = {}
277
#         collections_json_f[u"contours"] = collections_json
278
#         fileout.write(json.dumps(collections_json_f, sort_keys=True))  # indent=2)
279
#         logger.info('total points: ' + str(total_points) + ', compression: ' + str(int((1.0 - total_points / total_points_original) * 100)) + '%')
280