forked from tilezen/mapbox-vector-tile
-
Notifications
You must be signed in to change notification settings - Fork 0
/
encoder.py
312 lines (251 loc) · 10.8 KB
/
encoder.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
from mapbox_vector_tile.polygon import make_it_valid, clean_multi
from numbers import Number
from past.builtins import long
from past.builtins import unicode
from past.builtins import xrange
from shapely.geometry.base import BaseGeometry
from shapely.geometry.multipolygon import MultiPolygon
from shapely.geometry.polygon import orient
from shapely.geometry.polygon import Polygon
from shapely.ops import transform
from shapely.wkb import loads as load_wkb
from shapely.wkt import loads as load_wkt
from shapely.validation import explain_validity
import decimal
from .compat import PY3, vector_tile, apply_map
from .geom_encoder import GeometryEncoder
def on_invalid_geometry_raise(shape):
raise ValueError('Invalid geometry: %s' % shape.wkt)
def on_invalid_geometry_ignore(shape):
return None
def on_invalid_geometry_make_valid(shape):
return make_it_valid(shape)
def on_invalid_geometry_make_valid_and_clean(shape):
shape = make_it_valid(shape, asserted=False)
if not shape.is_valid and shape.type == 'MultiPolygon':
shape = clean_multi(shape)
assert shape.is_valid, \
"Not valid %s %s because %s" \
% (shape.type, shape.wkt, explain_validity(shape))
return shape
class VectorTile:
"""
"""
def __init__(self, extents, on_invalid_geometry=None,
max_geometry_validate_tries=5, round_fn=None,
check_winding_order=True):
self.tile = vector_tile.tile()
self.extents = extents
self.on_invalid_geometry = on_invalid_geometry
self.check_winding_order = check_winding_order
self.max_geometry_validate_tries = max_geometry_validate_tries
if round_fn:
self._round = round_fn
else:
self._round = self._round_quantize
def _round_quantize(self, val):
# round() has different behavior in python 2/3
# For consistency between 2 and 3 we use quantize, however
# it is slower than the built in round function.
d = decimal.Decimal(val)
rounded = d.quantize(1, rounding=decimal.ROUND_HALF_EVEN)
return float(rounded)
def addFeatures(self, features, layer_name='',
quantize_bounds=None, y_coord_down=False):
self.layer = self.tile.layers.add()
self.layer.name = layer_name
self.layer.version = 1
self.layer.extent = self.extents
self.key_idx = 0
self.val_idx = 0
self.seen_keys_idx = {}
self.seen_values_idx = {}
for feature in features:
# skip missing or empty geometries
geometry_spec = feature.get('geometry')
if geometry_spec is None:
continue
shape = self._load_geometry(geometry_spec)
if shape is None:
raise NotImplementedError(
'Can\'t do geometries that are not wkt, wkb, or shapely '
'geometries')
if shape.is_empty:
continue
if quantize_bounds:
shape = self.quantize(shape, quantize_bounds)
if self.check_winding_order:
shape = self.enforce_winding_order(shape, y_coord_down)
if shape is not None and not shape.is_empty:
self.addFeature(feature, shape, y_coord_down)
def enforce_winding_order(self, shape, y_coord_down, n_try=1):
if shape.type == 'MultiPolygon':
# If we are a multipolygon, we need to ensure that the
# winding orders of the consituent polygons are
# correct. In particular, the winding order of the
# interior rings need to be the opposite of the
# exterior ones, and all interior rings need to follow
# the exterior one. This is how the end of one polygon
# and the beginning of another are signaled.
shape = self.enforce_multipolygon_winding_order(
shape, y_coord_down, n_try)
elif shape.type == 'Polygon':
# Ensure that polygons are also oriented with the
# appropriate winding order. Their exterior rings must
# have a clockwise order, which is translated into a
# clockwise order in MVT's tile-local coordinates with
# the Y axis in "screen" (i.e: +ve down) configuration.
# Note that while the Y axis flips, we also invert the
# Y coordinate to get the tile-local value, which means
# the clockwise orientation is unchanged.
shape = self.enforce_polygon_winding_order(
shape, y_coord_down, n_try)
# other shapes just get passed through
return shape
def quantize(self, shape, bounds):
minx, miny, maxx, maxy = bounds
def fn(x, y, z=None):
xfac = self.extents / (maxx - minx)
yfac = self.extents / (maxy - miny)
x = xfac * (x - minx)
y = yfac * (y - miny)
return self._round(x), self._round(y)
return transform(fn, shape)
def handle_shape_validity(self, shape, y_coord_down, n_try):
if shape.is_valid:
return shape
if n_try >= self.max_geometry_validate_tries:
# ensure that we don't recurse indefinitely with an
# invalid geometry handler that doesn't validate
# geometries
return None
if self.on_invalid_geometry:
shape = self.on_invalid_geometry(shape)
if shape is not None and not shape.is_empty:
# this means that we have a handler that might have
# altered the geometry. We'll run through the process
# again, but keep track of which attempt we are on to
# terminate the recursion.
shape = self.enforce_winding_order(
shape, y_coord_down, n_try + 1)
return shape
def enforce_multipolygon_winding_order(self, shape, y_coord_down, n_try):
assert shape.type == 'MultiPolygon'
parts = []
for part in shape.geoms:
part = self.enforce_polygon_winding_order(
part, y_coord_down, n_try)
if part is not None and not part.is_empty:
if part.type == 'MultiPolygon':
parts.extend(part.geoms)
else:
parts.append(part)
if not parts:
return None
if len(parts) == 1:
oriented_shape = parts[0]
else:
oriented_shape = MultiPolygon(parts)
oriented_shape = self.handle_shape_validity(
oriented_shape, y_coord_down, n_try)
return oriented_shape
def enforce_polygon_winding_order(self, shape, y_coord_down, n_try):
assert shape.type == 'Polygon'
def fn(point):
x, y = point
return self._round(x), self._round(y)
exterior = apply_map(fn, shape.exterior.coords)
rings = None
if len(shape.interiors) > 0:
rings = [apply_map(fn, ring.coords) for ring in shape.interiors]
sign = 1.0 if y_coord_down else -1.0
oriented_shape = orient(Polygon(exterior, rings), sign=sign)
oriented_shape = self.handle_shape_validity(
oriented_shape, y_coord_down, n_try)
return oriented_shape
def _load_geometry(self, geometry_spec):
if isinstance(geometry_spec, BaseGeometry):
return geometry_spec
try:
return load_wkb(geometry_spec)
except:
try:
return load_wkt(geometry_spec)
except:
return None
def addFeature(self, feature, shape, y_coord_down):
geom_encoder = GeometryEncoder(y_coord_down, self.extents,
self._round)
geometry = geom_encoder.encode(shape)
feature_type = self._get_feature_type(shape)
if len(geometry) == 0:
# Don't add geometry if it's too small
return
f = self.layer.features.add()
fid = feature.get('id')
if fid is not None:
if isinstance(fid, Number) and fid >= 0:
f.id = fid
# properties
properties = feature.get('properties')
if properties is not None:
self._handle_attr(self.layer, f, properties)
f.type = feature_type
f.geometry.extend(geometry)
def _get_feature_type(self, shape):
if shape.type == 'Point' or shape.type == 'MultiPoint':
return self.tile.Point
elif shape.type == 'LineString' or shape.type == 'MultiLineString':
return self.tile.LineString
elif shape.type == 'Polygon' or shape.type == 'MultiPolygon':
return self.tile.Polygon
elif shape.type == 'GeometryCollection':
raise ValueError('Encoding geometry collections not supported')
else:
raise ValueError('Cannot encode unknown geometry type: %s' %
shape.type)
def _chunker(self, seq, size):
return [seq[pos:pos + size] for pos in xrange(0, len(seq), size)]
def _can_handle_key(self, k):
return isinstance(k, (str, unicode))
def _can_handle_val(self, v):
if isinstance(v, (str, unicode)):
return True
elif isinstance(v, bool):
return True
elif isinstance(v, (int, long)):
return True
elif isinstance(v, float):
return True
return False
def _can_handle_attr(self, k, v):
return self._can_handle_key(k) and \
self._can_handle_val(v)
def _handle_attr(self, layer, feature, props):
for k, v in props.items():
if self._can_handle_attr(k, v):
if not PY3 and isinstance(k, str):
k = k.decode('utf-8')
if k not in self.seen_keys_idx:
layer.keys.append(k)
self.seen_keys_idx[k] = self.key_idx
self.key_idx += 1
feature.tags.append(self.seen_keys_idx[k])
if v not in self.seen_values_idx:
self.seen_values_idx[v] = self.val_idx
self.val_idx += 1
val = layer.values.add()
if isinstance(v, bool):
val.bool_value = v
elif isinstance(v, str):
if PY3:
val.string_value = v
else:
val.string_value = unicode(v, 'utf-8')
elif isinstance(v, unicode):
val.string_value = v
elif isinstance(v, (int, long)):
val.int_value = v
elif isinstance(v, float):
val.double_value = v
feature.tags.append(self.seen_values_idx[v])