simplestmaps 1.4.0__tar.gz → 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simplestmaps-1.4.0/simplestmaps.egg-info → simplestmaps-1.6.0}/PKG-INFO +2 -2
- {simplestmaps-1.4.0 → simplestmaps-1.6.0}/README.md +1 -1
- {simplestmaps-1.4.0 → simplestmaps-1.6.0}/setup.py +1 -1
- {simplestmaps-1.4.0 → simplestmaps-1.6.0/simplestmaps.egg-info}/PKG-INFO +2 -2
- simplestmaps-1.6.0/simplestmaps.py +427 -0
- simplestmaps-1.4.0/simplestmaps.py +0 -357
- {simplestmaps-1.4.0 → simplestmaps-1.6.0}/LICENSE +0 -0
- {simplestmaps-1.4.0 → simplestmaps-1.6.0}/setup.cfg +0 -0
- {simplestmaps-1.4.0 → simplestmaps-1.6.0}/simplestmaps.egg-info/SOURCES.txt +0 -0
- {simplestmaps-1.4.0 → simplestmaps-1.6.0}/simplestmaps.egg-info/dependency_links.txt +0 -0
- {simplestmaps-1.4.0 → simplestmaps-1.6.0}/simplestmaps.egg-info/requires.txt +0 -0
- {simplestmaps-1.4.0 → simplestmaps-1.6.0}/simplestmaps.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: simplestmaps
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Super simple on-liner maps in python.
|
|
5
5
|
Home-page: http://github.com/fisadev/simplestmaps
|
|
6
6
|
Author: Juan Pedro Fisanotti
|
|
@@ -49,7 +49,7 @@ Description: # Simplest Maps
|
|
|
49
49
|
|
|
50
50
|
Here are a good set of examples that showcase (and document) all of the supported features:
|
|
51
51
|
|
|
52
|
-
[Take a look at everything that's possible with so little code!](https://nbviewer.org/github/fisadev/simplestmaps/blob/
|
|
52
|
+
## [Take a look at everything that's possible with so little code!](https://nbviewer.org/github/fisadev/simplestmaps/blob/e26f7cbe1eb195714f87e3804e766e302b7e51b5/demo_and_docs.ipynb)
|
|
53
53
|
|
|
54
54
|
Platform: UNKNOWN
|
|
55
55
|
Classifier: Intended Audience :: Developers
|
|
@@ -41,4 +41,4 @@ And every element plotted into the map is also customizable (colors, fonts, size
|
|
|
41
41
|
|
|
42
42
|
Here are a good set of examples that showcase (and document) all of the supported features:
|
|
43
43
|
|
|
44
|
-
[Take a look at everything that's possible with so little code!](https://nbviewer.org/github/fisadev/simplestmaps/blob/
|
|
44
|
+
## [Take a look at everything that's possible with so little code!](https://nbviewer.org/github/fisadev/simplestmaps/blob/e26f7cbe1eb195714f87e3804e766e302b7e51b5/demo_and_docs.ipynb)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: simplestmaps
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Super simple on-liner maps in python.
|
|
5
5
|
Home-page: http://github.com/fisadev/simplestmaps
|
|
6
6
|
Author: Juan Pedro Fisanotti
|
|
@@ -49,7 +49,7 @@ Description: # Simplest Maps
|
|
|
49
49
|
|
|
50
50
|
Here are a good set of examples that showcase (and document) all of the supported features:
|
|
51
51
|
|
|
52
|
-
[Take a look at everything that's possible with so little code!](https://nbviewer.org/github/fisadev/simplestmaps/blob/
|
|
52
|
+
## [Take a look at everything that's possible with so little code!](https://nbviewer.org/github/fisadev/simplestmaps/blob/e26f7cbe1eb195714f87e3804e766e302b7e51b5/demo_and_docs.ipynb)
|
|
53
53
|
|
|
54
54
|
Platform: UNKNOWN
|
|
55
55
|
Classifier: Intended Audience :: Developers
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
from collections import namedtuple
|
|
2
|
+
from functools import partial, wraps
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from types import GeneratorType
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import folium
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Point = namedtuple("Point", "lat lon")
|
|
11
|
+
|
|
12
|
+
# type -> converter function that receives instance of type and returns either a single (lat,lon)
|
|
13
|
+
# tuple, or a sequence of (lat,lon) tuples
|
|
14
|
+
converters = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def all_numbers(sequence):
|
|
18
|
+
"""
|
|
19
|
+
Is a sequence composed of just numbers?
|
|
20
|
+
"""
|
|
21
|
+
return all(
|
|
22
|
+
isinstance(x, (int, float)) and not isinstance(x, bool)
|
|
23
|
+
for x in sequence
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def to_points(source, inverted_tuples=False):
|
|
28
|
+
"""
|
|
29
|
+
Convert any possible thing to lat lon points and/or sequences of points.
|
|
30
|
+
Optionally reverse lat,lon values (used when converting points from lists of coordinates in
|
|
31
|
+
geojsons).
|
|
32
|
+
"""
|
|
33
|
+
while source.__class__ in converters:
|
|
34
|
+
# a custom type for which we have a converter registered, use it before trying to extract
|
|
35
|
+
# points from it
|
|
36
|
+
source = converters[source.__class__](source)
|
|
37
|
+
|
|
38
|
+
if isinstance(source, Point):
|
|
39
|
+
# don't re-convert things that are already points
|
|
40
|
+
return source
|
|
41
|
+
elif isinstance(source, GeneratorType):
|
|
42
|
+
# traverse the generator and build a tuple with its components, that we can inspect and
|
|
43
|
+
# parse (so we can do things like decide how to parse it based on its length and the types
|
|
44
|
+
# of its items)
|
|
45
|
+
return to_points(tuple(source), inverted_tuples=inverted_tuples)
|
|
46
|
+
elif hasattr(source, "lat") and hasattr(source, "lon"):
|
|
47
|
+
# a few known classes that have lat long attributes
|
|
48
|
+
lat, lon = source.lat, source.lon
|
|
49
|
+
elif hasattr(source, "latitude") and hasattr(source, "longitude"):
|
|
50
|
+
# a few known classes that have latitude longitude attributes, like some common geo
|
|
51
|
+
# libs
|
|
52
|
+
lat, lon = source.latitude, source.longitude
|
|
53
|
+
elif hasattr(source, "latitude_deg") and hasattr(source, "longitude_deg"):
|
|
54
|
+
# a few known classes that have latitude_deg longitude_deg attributes, like the
|
|
55
|
+
# locations from the orbit-predictor lib
|
|
56
|
+
lat, lon = source.latitude_deg, source.longitude_deg
|
|
57
|
+
elif isinstance(source, (tuple, list)):
|
|
58
|
+
if len(source) == 1:
|
|
59
|
+
# a sequence with a single object inside, so extract a point from it as if we just got
|
|
60
|
+
# that object alone
|
|
61
|
+
return to_points(source[0], inverted_tuples=inverted_tuples)
|
|
62
|
+
elif len(source) in (2, 3) and all_numbers(source):
|
|
63
|
+
# a sequence of 2 or 3 numbers, these must be coordinates, finally!
|
|
64
|
+
if inverted_tuples:
|
|
65
|
+
lon, lat = source[:2]
|
|
66
|
+
else:
|
|
67
|
+
lat, lon = source[:2]
|
|
68
|
+
else:
|
|
69
|
+
# a sequence that doesn't look like coordinates but maybe a collection of points, try
|
|
70
|
+
# to extract all of them into a list
|
|
71
|
+
return [to_points(item, inverted_tuples=inverted_tuples) for item in source]
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"Can't guess the latitude and longitude from this object: {repr(source)}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return Point(lat, lon)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def auto_convert(custom_type, converter_function):
|
|
81
|
+
"""
|
|
82
|
+
Register a custom type to be automatically converted to single coordinates or sequences of
|
|
83
|
+
coordinates, to be able to use them in the helper functions for map elements.
|
|
84
|
+
"""
|
|
85
|
+
converters[custom_type] = converter_function
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# types of things we can display on a map, with their attributes
|
|
89
|
+
Marker = namedtuple("Marker", "point popup")
|
|
90
|
+
Dot = namedtuple("Dot", "point color radius opacity border_color border_width popup")
|
|
91
|
+
Label = namedtuple("Label", "point text color size font opacity popup")
|
|
92
|
+
Html = namedtuple("Html", "point code popup")
|
|
93
|
+
Line = namedtuple("Line", "points_sequence color width opacity popup")
|
|
94
|
+
Area = namedtuple("Area", "points_sequence color opacity border_color border_width popup")
|
|
95
|
+
Geojson = namedtuple("Geojson", "path points_as lines_as areas_as")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def extract_single_points(points_source):
|
|
99
|
+
"""
|
|
100
|
+
Extract and iterate over single points that might be part of point sequences, or even trees of
|
|
101
|
+
points.
|
|
102
|
+
|
|
103
|
+
This function assumes that points_source can contain only two types of things: Point instances,
|
|
104
|
+
or sequences (lists/tuples/generators) containing any of both.
|
|
105
|
+
|
|
106
|
+
Something like [[point_a], [point_b, point_c, point_d], [[[point_e]], point_f]]
|
|
107
|
+
Will yield all of the Point instances, one by one, in order.
|
|
108
|
+
"""
|
|
109
|
+
pending = [points_source]
|
|
110
|
+
while pending:
|
|
111
|
+
current = pending.pop(0)
|
|
112
|
+
|
|
113
|
+
if isinstance(current, Point):
|
|
114
|
+
yield current
|
|
115
|
+
elif isinstance(current, (list, tuple, GeneratorType)):
|
|
116
|
+
pending.extend(current)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def extract_points_sequences(points_source):
|
|
120
|
+
"""
|
|
121
|
+
Extract and iterate over point sequences that might be themselves part of higher order
|
|
122
|
+
sequences, or even trees of sequences.
|
|
123
|
+
|
|
124
|
+
This function assumes that points_source can contain only two types of things: Point instances,
|
|
125
|
+
or sequences (lists/tuples/generators) containing any of both.
|
|
126
|
+
|
|
127
|
+
The only condition is that any sequence (list/tuple/generator) contains either all Point
|
|
128
|
+
instances, or all sequences. No mixing of the two element types inside the same sequence.
|
|
129
|
+
|
|
130
|
+
Something like [[point_a], [point_b, point_c, point_d], [[[point_e]]]]
|
|
131
|
+
Will yield three sequences:
|
|
132
|
+
[point_a]
|
|
133
|
+
[point_b, point_c, point_d]
|
|
134
|
+
[point_e]
|
|
135
|
+
|
|
136
|
+
While something like [point_a, [point_b, point_c]]
|
|
137
|
+
Will yield an error, as there's a sequence (the top level one) mixing types inside it.
|
|
138
|
+
"""
|
|
139
|
+
pending = [points_source]
|
|
140
|
+
while pending:
|
|
141
|
+
current = pending.pop(0)
|
|
142
|
+
|
|
143
|
+
if isinstance(current, Point):
|
|
144
|
+
# we received a single point, not even in a sequence, just yield it as a sequence of
|
|
145
|
+
# one element
|
|
146
|
+
yield [current]
|
|
147
|
+
elif isinstance(current, (list, tuple, GeneratorType)):
|
|
148
|
+
# we have a sequence, but is it a sequence of points? or a sequence of other sequences?
|
|
149
|
+
if all(isinstance(item, Point) for item in current):
|
|
150
|
+
# current is a sequence of coords!
|
|
151
|
+
yield current
|
|
152
|
+
elif any(isinstance(item, Point) for item in current):
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"There's a sequence mixing points and other kinds of objects: {repr(current)}"
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
# a sequence of sequences, or a sequence of broken things. Add each item to the
|
|
158
|
+
# pending queue and deal with them when popped
|
|
159
|
+
pending.extend(current)
|
|
160
|
+
else:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"Can't guess a sequence of coordinates from this object: {repr(current)}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def marker(*points_sources, popup=None):
|
|
167
|
+
"""
|
|
168
|
+
Helper to easily build a Marker instance.
|
|
169
|
+
"""
|
|
170
|
+
if not points_sources:
|
|
171
|
+
return partial(marker, popup=popup)
|
|
172
|
+
|
|
173
|
+
return [
|
|
174
|
+
Marker(point, popup)
|
|
175
|
+
for point in extract_single_points(to_points(points_sources))
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def dot(*points_sources, color="blue", radius=3, opacity=1, border_color=None, border_width=0,
|
|
180
|
+
popup=None):
|
|
181
|
+
"""
|
|
182
|
+
Helper to easily build a Dot instance.
|
|
183
|
+
"""
|
|
184
|
+
if not points_sources:
|
|
185
|
+
return partial(dot, color=color, radius=radius, opacity=opacity, border_color=border_color,
|
|
186
|
+
border_width=border_width, popup=popup)
|
|
187
|
+
|
|
188
|
+
if border_color is None:
|
|
189
|
+
border_color = color
|
|
190
|
+
elif border_width == 0:
|
|
191
|
+
border_width = 2
|
|
192
|
+
|
|
193
|
+
return [
|
|
194
|
+
Dot(point, color, radius, opacity, border_color, border_width, popup)
|
|
195
|
+
for point in extract_single_points(to_points(points_sources))
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def label(*points_sources, text, color="blue", size=12, font="arial", opacity=1, popup=None):
|
|
200
|
+
"""
|
|
201
|
+
Helper to easily build a Label instance.
|
|
202
|
+
"""
|
|
203
|
+
if not points_sources:
|
|
204
|
+
return partial(label, text=text, color=color, size=size, font=font, opacity=opacity,
|
|
205
|
+
popup=popup)
|
|
206
|
+
|
|
207
|
+
return [
|
|
208
|
+
Label(point, text, color, size, font, opacity, popup)
|
|
209
|
+
for point in extract_single_points(to_points(points_sources))
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def html(*points_sources, code, popup=None):
|
|
214
|
+
"""
|
|
215
|
+
Helper to easily build a Html instance.
|
|
216
|
+
"""
|
|
217
|
+
if not points_sources:
|
|
218
|
+
return partial(html, code=code, popup=popup)
|
|
219
|
+
|
|
220
|
+
return [
|
|
221
|
+
Html(point, code, popup)
|
|
222
|
+
for point in extract_single_points(to_points(points_sources))
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def line(*points_sequences_sources, color="blue", width=2, opacity=1, popup=None):
|
|
227
|
+
"""
|
|
228
|
+
Helper to easily build a Line instance.
|
|
229
|
+
"""
|
|
230
|
+
if not points_sequences_sources:
|
|
231
|
+
return partial(line, color=color, width=width, opacity=opacity, popup=popup)
|
|
232
|
+
|
|
233
|
+
return [
|
|
234
|
+
Line(points_sequence, color, width, opacity, popup)
|
|
235
|
+
for points_sequence in extract_points_sequences(to_points(points_sequences_sources))
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def area(*points_sequences_sources, color="blue", opacity=0.5, border_color=None, border_width=0,
|
|
240
|
+
popup=None):
|
|
241
|
+
"""
|
|
242
|
+
Helper to easily build an Area instance.
|
|
243
|
+
"""
|
|
244
|
+
if not points_sequences_sources:
|
|
245
|
+
return partial(area, color=color, opacity=opacity, border_color=border_color,
|
|
246
|
+
border_width=border_width, popup=popup)
|
|
247
|
+
|
|
248
|
+
if border_color is None:
|
|
249
|
+
border_color = color
|
|
250
|
+
elif border_width == 0:
|
|
251
|
+
border_width = 2
|
|
252
|
+
|
|
253
|
+
return [
|
|
254
|
+
Area(points_sequence, color, opacity, border_color, border_width, popup)
|
|
255
|
+
for points_sequence in extract_points_sequences(to_points(points_sequences_sources))
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def geojson(path_or_data, points_as=marker, lines_as=line, areas_as=area):
|
|
260
|
+
"""
|
|
261
|
+
Helper to easily read the contents of a geojson, and extract all the things it contains.
|
|
262
|
+
"""
|
|
263
|
+
if isinstance(path_or_data, (str, Path)):
|
|
264
|
+
# a path to a geojson file, read it and extract its contents
|
|
265
|
+
with open(path_or_data, "r") as geojson_f:
|
|
266
|
+
geojson_data = json.load(geojson_f)
|
|
267
|
+
else:
|
|
268
|
+
geojson_data = path_or_data
|
|
269
|
+
|
|
270
|
+
if isinstance(geojson_data, list):
|
|
271
|
+
# a list of geo items, extract them individually
|
|
272
|
+
for item_data in geojson_data:
|
|
273
|
+
yield from geojson(item_data, points_as, lines_as, areas_as)
|
|
274
|
+
|
|
275
|
+
elif isinstance(geojson_data, dict):
|
|
276
|
+
# a single geo object, try to identify its type
|
|
277
|
+
geojson_type = geojson_data.get("type")
|
|
278
|
+
|
|
279
|
+
if geojson_type is None:
|
|
280
|
+
raise ValueError(
|
|
281
|
+
"The provided geojson seems to contain invalid data (no 'type' key present):\n\n"
|
|
282
|
+
f"{repr(geojson_data)}"
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
elif geojson_type == "FeatureCollection":
|
|
286
|
+
# a collection of features, extract them individually
|
|
287
|
+
for feature in geojson_data["features"]:
|
|
288
|
+
yield from geojson(feature["geometry"], points_as, lines_as, areas_as)
|
|
289
|
+
elif geojson_type == "Point":
|
|
290
|
+
yield points_as(
|
|
291
|
+
extract_single_points(to_points(geojson_data["coordinates"],
|
|
292
|
+
inverted_tuples=True))
|
|
293
|
+
)
|
|
294
|
+
elif geojson_type == "LineString":
|
|
295
|
+
yield lines_as(
|
|
296
|
+
extract_points_sequences(to_points(geojson_data["coordinates"],
|
|
297
|
+
inverted_tuples=True))
|
|
298
|
+
)
|
|
299
|
+
elif geojson_type in ("Polygon", "MultiPolygon"):
|
|
300
|
+
yield areas_as(
|
|
301
|
+
extract_points_sequences(to_points(geojson_data["coordinates"],
|
|
302
|
+
inverted_tuples=True))
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def draw_map(*things, center=(0, 0), zoom=1.5, tiles="cartodbpositron"):
|
|
307
|
+
"""
|
|
308
|
+
Draw a simple map with elements on it. Good known working tiles with folium 0.14:
|
|
309
|
+
- "cartodbpositron" (white maps)
|
|
310
|
+
- "OpenStreetMap"
|
|
311
|
+
|
|
312
|
+
More info on tiles:
|
|
313
|
+
https://python-visualization.github.io/folium/latest/user_guide/raster_layers/tiles.html
|
|
314
|
+
"""
|
|
315
|
+
center = to_points(center)
|
|
316
|
+
|
|
317
|
+
map_ = folium.Map(location=(center.lat, center.lon), zoom_start=zoom, attr=".", tiles=tiles)
|
|
318
|
+
|
|
319
|
+
# consume all the things to display, and use inverted order so the user can easily understand
|
|
320
|
+
# what's o top of what
|
|
321
|
+
things = list(reversed(things))
|
|
322
|
+
|
|
323
|
+
while things:
|
|
324
|
+
thing = things.pop()
|
|
325
|
+
|
|
326
|
+
if isinstance(thing, (list, tuple, GeneratorType)):
|
|
327
|
+
# a collection of things, probably created using the geojson() helper. Feed those
|
|
328
|
+
# things into the pending list
|
|
329
|
+
things.extend(thing)
|
|
330
|
+
|
|
331
|
+
if isinstance(thing, Marker):
|
|
332
|
+
folium.Marker(
|
|
333
|
+
location=[thing.point.lat, thing.point.lon],
|
|
334
|
+
popup=thing.popup,
|
|
335
|
+
).add_to(map_)
|
|
336
|
+
|
|
337
|
+
elif isinstance(thing, Dot):
|
|
338
|
+
folium.CircleMarker(
|
|
339
|
+
location=[thing.point.lat, thing.point.lon],
|
|
340
|
+
radius=thing.radius,
|
|
341
|
+
color=thing.border_color,
|
|
342
|
+
weight=thing.border_width,
|
|
343
|
+
fillColor=thing.color,
|
|
344
|
+
fillOpacity=thing.opacity,
|
|
345
|
+
popup=thing.popup,
|
|
346
|
+
).add_to(map_)
|
|
347
|
+
|
|
348
|
+
elif isinstance(thing, Label):
|
|
349
|
+
folium.Marker(
|
|
350
|
+
location=[thing.point.lat, thing.point.lon],
|
|
351
|
+
icon=folium.DivIcon(html=f"""
|
|
352
|
+
<div style="font-family: {thing.font};
|
|
353
|
+
font-size: {thing.size}px;
|
|
354
|
+
color: {thing.color};
|
|
355
|
+
opacity: {thing.opacity}">
|
|
356
|
+
{thing.text}
|
|
357
|
+
</div>
|
|
358
|
+
"""),
|
|
359
|
+
popup=thing.popup,
|
|
360
|
+
).add_to(map_)
|
|
361
|
+
|
|
362
|
+
elif isinstance(thing, Html):
|
|
363
|
+
folium.Marker(
|
|
364
|
+
location=[thing.point.lat, thing.point.lon],
|
|
365
|
+
icon=folium.DivIcon(html=thing.code),
|
|
366
|
+
popup=thing.popup,
|
|
367
|
+
).add_to(map_)
|
|
368
|
+
|
|
369
|
+
elif isinstance(thing, Line):
|
|
370
|
+
folium.PolyLine(
|
|
371
|
+
locations=[(point.lat, point.lon)
|
|
372
|
+
for point in thing.points_sequence],
|
|
373
|
+
color=thing.color,
|
|
374
|
+
weight=thing.width,
|
|
375
|
+
opacity=thing.opacity,
|
|
376
|
+
popup=thing.popup,
|
|
377
|
+
).add_to(map_)
|
|
378
|
+
|
|
379
|
+
elif isinstance(thing, Area):
|
|
380
|
+
folium.Polygon(
|
|
381
|
+
locations=[(point.lat, point.lon)
|
|
382
|
+
for point in thing.points_sequence],
|
|
383
|
+
color=thing.border_color,
|
|
384
|
+
weight=thing.border_width,
|
|
385
|
+
fill_color=thing.color,
|
|
386
|
+
fill_opacity=thing.opacity,
|
|
387
|
+
popup=thing.popup,
|
|
388
|
+
).add_to(map_)
|
|
389
|
+
|
|
390
|
+
return map_
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
# add shapely support if present
|
|
395
|
+
from shapely.geometry import (
|
|
396
|
+
Point as ShpPoint,
|
|
397
|
+
LineString as ShpLineString,
|
|
398
|
+
LinearRing as ShpLinearRing,
|
|
399
|
+
Polygon as ShpPolygon,
|
|
400
|
+
MultiPoint as ShpMultiPoint,
|
|
401
|
+
MultiPolygon as ShpMultiPolygon,
|
|
402
|
+
MultiLineString as ShpMultiLineString
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
auto_convert(ShpPoint, lambda p: (p.y, p.x))
|
|
406
|
+
auto_convert(ShpLineString, lambda ls: [(c[1], c[0]) for c in ls.coords])
|
|
407
|
+
auto_convert(ShpLinearRing, lambda ls: [(c[1], c[0]) for c in ls.coords])
|
|
408
|
+
auto_convert(ShpPolygon, lambda p: [(c[1], c[0]) for c in p.exterior.coords])
|
|
409
|
+
auto_convert(ShpMultiPoint, lambda mp: list(mp.geoms))
|
|
410
|
+
auto_convert(ShpMultiLineString, lambda mp: list(mp.geoms))
|
|
411
|
+
auto_convert(ShpMultiPolygon, lambda mp: list(mp.geoms))
|
|
412
|
+
except ImportError:
|
|
413
|
+
# shapely not present, do nothing
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
# add shapely support if present
|
|
419
|
+
# (https://telluric.readthedocs.io/en/latest/index.html)
|
|
420
|
+
from telluric import GeoVector, FeatureCollection, GeoFeature
|
|
421
|
+
|
|
422
|
+
auto_convert(GeoVector, lambda gv: gv.get_shape(gv.crs))
|
|
423
|
+
auto_convert(GeoFeature, lambda gf: gf.geometry)
|
|
424
|
+
auto_convert(FeatureCollection, lambda fc: fc.geometries)
|
|
425
|
+
except ImportError:
|
|
426
|
+
# telluric not present, do nothing
|
|
427
|
+
pass
|
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
from collections import namedtuple
|
|
2
|
-
from functools import partial, wraps
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from types import GeneratorType
|
|
5
|
-
import json
|
|
6
|
-
|
|
7
|
-
import folium
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Coords = namedtuple("Coords", "lat lon")
|
|
11
|
-
|
|
12
|
-
# type -> converter function that receives instance of type and returns either a single (lat,lon)
|
|
13
|
-
# tuple, or a sequence of (lat,lon) tuples
|
|
14
|
-
coords_converters = {}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def extract_coords(coords_source):
|
|
18
|
-
"""
|
|
19
|
-
Extract lat lon coordinates from any possible known thing.
|
|
20
|
-
"""
|
|
21
|
-
if coords_source.__class__ in coords_converters:
|
|
22
|
-
# a custom type for which we have a converter registered, use it
|
|
23
|
-
converter = coords_converters[coords_source.__class__]
|
|
24
|
-
return extract_coords(converter(coords_source))
|
|
25
|
-
if hasattr(coords_source, "lat") and hasattr(coords_source, "lon"):
|
|
26
|
-
# a few known classes that have lat long attributes
|
|
27
|
-
lat, lon = coords_source.lat, coords_source.lon
|
|
28
|
-
elif hasattr(coords_source, "latitude") and hasattr(coords_source, "longitude"):
|
|
29
|
-
# a few known classes that have latitude longitude attributes, like some common geo
|
|
30
|
-
# libs
|
|
31
|
-
lat, lon = coords_source.latitude, coords_source.longitude
|
|
32
|
-
elif hasattr(coords_source, "latitude_deg") and hasattr(coords_source, "longitude_deg"):
|
|
33
|
-
# a few known classes that have latitude_deg longitude_deg attributes, like the
|
|
34
|
-
# locations from the orbit-predictor lib
|
|
35
|
-
lat, lon = coords_source.latitude_deg, coords_source.longitude_deg
|
|
36
|
-
elif isinstance(coords_source, (tuple, list)) and len(coords_source) == 1:
|
|
37
|
-
return extract_coords(coords_source[0])
|
|
38
|
-
elif isinstance(coords_source, (tuple, list)) and len(coords_source) in (2, 3):
|
|
39
|
-
lat, lon = coords_source[:2]
|
|
40
|
-
else:
|
|
41
|
-
raise ValueError(
|
|
42
|
-
f"Can't guess the latitude and longitude from this object: {repr(coords_source)}"
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
return Coords(lat, lon)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def extract_coords_sequence(coords_sequence):
|
|
49
|
-
"""
|
|
50
|
-
Extract a sequence of lat lon coordinates from any possible known thing.
|
|
51
|
-
"""
|
|
52
|
-
while coords_sequence.__class__ in coords_converters:
|
|
53
|
-
# a custom type from which we need to extract the coordinates sequence
|
|
54
|
-
converter = coords_converters[coords_sequence.__class__]
|
|
55
|
-
coords_sequence = converter(coords_sequence)
|
|
56
|
-
|
|
57
|
-
# a sequence from which we need to extract each coordinate
|
|
58
|
-
return [extract_coords(coords) for coords in coords_sequence]
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def auto_convert(custom_type, converter_function):
|
|
62
|
-
"""
|
|
63
|
-
Register a custom type to be automatically converted to single coordinates or sequences of
|
|
64
|
-
coordinates, to be able to use them in the helper functions for map elements.
|
|
65
|
-
"""
|
|
66
|
-
coords_converters[custom_type] = converter_function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# types of things we can display on a map, with their attributes
|
|
70
|
-
Marker = namedtuple("Marker", "coords popup")
|
|
71
|
-
Dot = namedtuple("Dot", "coords color radius opacity border_color border_width popup")
|
|
72
|
-
Label = namedtuple("Label", "coords text color size font opacity popup")
|
|
73
|
-
Html = namedtuple("Html", "coords code popup")
|
|
74
|
-
Line = namedtuple("Line", "coords_sequence color width opacity popup")
|
|
75
|
-
Area = namedtuple("Area", "coords_sequence color opacity border_color border_width popup")
|
|
76
|
-
Geojson = namedtuple("Geojson", "path points_as lines_as areas_as")
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def marker(*coords, popup=None):
|
|
80
|
-
"""
|
|
81
|
-
Helper to easily build a Marker instance.
|
|
82
|
-
"""
|
|
83
|
-
if not coords:
|
|
84
|
-
return partial(marker, popup=popup)
|
|
85
|
-
|
|
86
|
-
return Marker(extract_coords(coords), popup)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def dot(*coords, color="blue", radius=3, opacity=1, border_color=None, border_width=0, popup=None):
|
|
90
|
-
"""
|
|
91
|
-
Helper to easily build a Dot instance.
|
|
92
|
-
"""
|
|
93
|
-
if not coords:
|
|
94
|
-
return partial(dot, color=color, radius=radius, opacity=opacity, border_color=border_color,
|
|
95
|
-
border_width=border_width, popup=popup)
|
|
96
|
-
|
|
97
|
-
if border_color is None:
|
|
98
|
-
border_color = color
|
|
99
|
-
elif border_width == 0:
|
|
100
|
-
border_width = 2
|
|
101
|
-
|
|
102
|
-
return Dot(extract_coords(coords), color, radius, opacity, border_color, border_width, popup)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def label(*coords, text, color="blue", size=12, font="arial", opacity=1, popup=None):
|
|
106
|
-
"""
|
|
107
|
-
Helper to easily build a Label instance.
|
|
108
|
-
"""
|
|
109
|
-
if not coords:
|
|
110
|
-
return partial(label, text=text, color=color, size=size, font=font, opacity=opacity,
|
|
111
|
-
popup=popup)
|
|
112
|
-
|
|
113
|
-
return Label(extract_coords(coords), text, color, size, font, opacity, popup)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def html(*coords, code, popup=None):
|
|
117
|
-
"""
|
|
118
|
-
Helper to easily build a Html instance.
|
|
119
|
-
"""
|
|
120
|
-
if not coords:
|
|
121
|
-
return partial(html, code=code, popup=popup)
|
|
122
|
-
|
|
123
|
-
return Html(extract_coords(coords), code, popup)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def line(coords_sequence=None, color="blue", width=2, opacity=1, popup=None):
|
|
127
|
-
"""
|
|
128
|
-
Helper to easily build a Line instance.
|
|
129
|
-
"""
|
|
130
|
-
if coords_sequence is None:
|
|
131
|
-
return partial(line, color=color, width=width, opacity=opacity, popup=popup)
|
|
132
|
-
|
|
133
|
-
return Line(extract_coords_sequence(coords_sequence), color, width, opacity, popup)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def area(coords_sequence=None, color="blue", opacity=0.5, border_color=None, border_width=0,
|
|
137
|
-
popup=None):
|
|
138
|
-
"""
|
|
139
|
-
Helper to easily build an Area instance.
|
|
140
|
-
"""
|
|
141
|
-
if coords_sequence is None:
|
|
142
|
-
return partial(area, color=color, opacity=opacity, border_color=border_color,
|
|
143
|
-
border_width=border_width, popup=popup)
|
|
144
|
-
|
|
145
|
-
if border_color is None:
|
|
146
|
-
border_color = color
|
|
147
|
-
elif border_width == 0:
|
|
148
|
-
border_width = 2
|
|
149
|
-
|
|
150
|
-
return Area(extract_coords_sequence(coords_sequence), color, opacity, border_color,
|
|
151
|
-
border_width, popup)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def geojson(path_or_data, points_as=marker, lines_as=line, areas_as=area):
|
|
155
|
-
"""
|
|
156
|
-
Helper to easily read the contents of a geojson, and extract all the things it contains.
|
|
157
|
-
"""
|
|
158
|
-
if isinstance(path_or_data, (str, Path)):
|
|
159
|
-
# a path to a geojson file, read it and extract its contents
|
|
160
|
-
with open(path_or_data, "r") as geojson_f:
|
|
161
|
-
geojson_data = json.load(geojson_f)
|
|
162
|
-
else:
|
|
163
|
-
geojson_data = path_or_data
|
|
164
|
-
|
|
165
|
-
if isinstance(geojson_data, list):
|
|
166
|
-
# a list of geo items, extract them individually
|
|
167
|
-
for item_data in geojson_data:
|
|
168
|
-
yield from geojson(item_data, points_as, lines_as, areas_as)
|
|
169
|
-
|
|
170
|
-
elif isinstance(geojson_data, dict):
|
|
171
|
-
# a single geo object, try to identify its type
|
|
172
|
-
geojson_type = geojson_data.get("type")
|
|
173
|
-
|
|
174
|
-
if geojson_type is None:
|
|
175
|
-
raise ValueError(
|
|
176
|
-
"The provided geojson seems to contain invalid data (no 'type' key present):\n\n"
|
|
177
|
-
f"{repr(geojson_data)}"
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
elif geojson_type == "FeatureCollection":
|
|
181
|
-
# a collection of features, extract them individually
|
|
182
|
-
for feature in geojson_data["features"]:
|
|
183
|
-
yield from geojson(feature["geometry"], points_as, lines_as, areas_as)
|
|
184
|
-
|
|
185
|
-
elif geojson_type == "Point":
|
|
186
|
-
# geojson uses lon,lat instead of lat,lon
|
|
187
|
-
raw_coords = geojson_data["coordinates"]
|
|
188
|
-
yield points_as(raw_coords[1], raw_coords[0])
|
|
189
|
-
|
|
190
|
-
elif geojson_type == "LineString":
|
|
191
|
-
# geojson uses lon,lat instead of lat,lon
|
|
192
|
-
raw_coords_sequence = geojson_data["coordinates"]
|
|
193
|
-
yield lines_as(
|
|
194
|
-
(raw_coords[1], raw_coords[0])
|
|
195
|
-
for raw_coords in raw_coords_sequence
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
elif geojson_type == "Polygon":
|
|
199
|
-
# geojson uses lon,lat instead of lat,lon. And polygons have lists of lists of coords
|
|
200
|
-
for raw_coords_sequence in geojson_data["coordinates"]:
|
|
201
|
-
yield areas_as(
|
|
202
|
-
(raw_coords[1], raw_coords[0])
|
|
203
|
-
for raw_coords in raw_coords_sequence
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def pluralize(helper):
|
|
208
|
-
"""
|
|
209
|
-
Make a helper able to work with multiple sources instead of just one.
|
|
210
|
-
Example: use the 'label' helper to create a new 'labels' helper, that works with multiple
|
|
211
|
-
points coordinates instead of just one point.
|
|
212
|
-
"""
|
|
213
|
-
@wraps(helper)
|
|
214
|
-
def plural_helper(sources, **kwargs):
|
|
215
|
-
while sources.__class__ in coords_converters:
|
|
216
|
-
# a custom type from which we need to extract the coordinates sequence
|
|
217
|
-
converter = coords_converters[sources.__class__]
|
|
218
|
-
sources = converter(sources)
|
|
219
|
-
|
|
220
|
-
# a sequence from which we need to extract each coordinate
|
|
221
|
-
for source in sources:
|
|
222
|
-
yield helper(source, **kwargs)
|
|
223
|
-
|
|
224
|
-
return plural_helper
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
marker_set = pluralize(marker)
|
|
228
|
-
dot_set = pluralize(dot)
|
|
229
|
-
label_set = pluralize(label)
|
|
230
|
-
html_set = pluralize(html)
|
|
231
|
-
line_set = pluralize(line)
|
|
232
|
-
area_set = pluralize(area)
|
|
233
|
-
geojson_set = pluralize(geojson)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def draw_map(*things, center=(0, 0), zoom=1.5, tiles="cartodbpositron"):
|
|
237
|
-
"""
|
|
238
|
-
Draw a simple map with elements on it. Good known working tiles with folium 0.14:
|
|
239
|
-
- "cartodbpositron" (white maps)
|
|
240
|
-
- "OpenStreetMap"
|
|
241
|
-
|
|
242
|
-
More info on tiles:
|
|
243
|
-
https://python-visualization.github.io/folium/latest/user_guide/raster_layers/tiles.html
|
|
244
|
-
"""
|
|
245
|
-
center = extract_coords(center)
|
|
246
|
-
|
|
247
|
-
map_ = folium.Map(location=(center.lat, center.lon), zoom_start=zoom, attr=".", tiles=tiles)
|
|
248
|
-
|
|
249
|
-
# consume all the things to display, and use inverted order so the user can easily understand
|
|
250
|
-
# what's o top of what
|
|
251
|
-
things = list(reversed(things))
|
|
252
|
-
|
|
253
|
-
while things:
|
|
254
|
-
thing = things.pop()
|
|
255
|
-
|
|
256
|
-
if isinstance(thing, (list, tuple, GeneratorType)):
|
|
257
|
-
# a collection of things, probably created using the geojson() helper. Feed those
|
|
258
|
-
# things into the pending list
|
|
259
|
-
things.extend(thing)
|
|
260
|
-
|
|
261
|
-
if isinstance(thing, Marker):
|
|
262
|
-
folium.Marker(
|
|
263
|
-
location=[thing.coords.lat, thing.coords.lon],
|
|
264
|
-
popup=thing.popup,
|
|
265
|
-
).add_to(map_)
|
|
266
|
-
|
|
267
|
-
elif isinstance(thing, Dot):
|
|
268
|
-
folium.CircleMarker(
|
|
269
|
-
location=[thing.coords.lat, thing.coords.lon],
|
|
270
|
-
radius=thing.radius,
|
|
271
|
-
color=thing.border_color,
|
|
272
|
-
weight=thing.border_width,
|
|
273
|
-
fillColor=thing.color,
|
|
274
|
-
fillOpacity=thing.opacity,
|
|
275
|
-
popup=thing.popup,
|
|
276
|
-
).add_to(map_)
|
|
277
|
-
|
|
278
|
-
elif isinstance(thing, Label):
|
|
279
|
-
folium.Marker(
|
|
280
|
-
location=[thing.coords.lat, thing.coords.lon],
|
|
281
|
-
icon=folium.DivIcon(html=f"""
|
|
282
|
-
<div style="font-family: {thing.font};
|
|
283
|
-
font-size: {thing.size}px;
|
|
284
|
-
color: {thing.color};
|
|
285
|
-
opacity: {thing.opacity}">
|
|
286
|
-
{thing.text}
|
|
287
|
-
</div>
|
|
288
|
-
"""),
|
|
289
|
-
popup=thing.popup,
|
|
290
|
-
).add_to(map_)
|
|
291
|
-
|
|
292
|
-
elif isinstance(thing, Html):
|
|
293
|
-
folium.Marker(
|
|
294
|
-
location=[thing.coords.lat, thing.coords.lon],
|
|
295
|
-
icon=folium.DivIcon(html=thing.code),
|
|
296
|
-
popup=thing.popup,
|
|
297
|
-
).add_to(map_)
|
|
298
|
-
|
|
299
|
-
elif isinstance(thing, Line):
|
|
300
|
-
folium.PolyLine(
|
|
301
|
-
locations=[(coords.lat, coords.lon)
|
|
302
|
-
for coords in thing.coords_sequence],
|
|
303
|
-
color=thing.color,
|
|
304
|
-
weight=thing.width,
|
|
305
|
-
opacity=thing.opacity,
|
|
306
|
-
popup=thing.popup,
|
|
307
|
-
).add_to(map_)
|
|
308
|
-
|
|
309
|
-
elif isinstance(thing, Area):
|
|
310
|
-
folium.Polygon(
|
|
311
|
-
locations=[(coords.lat, coords.lon)
|
|
312
|
-
for coords in thing.coords_sequence],
|
|
313
|
-
color=thing.border_color,
|
|
314
|
-
weight=thing.border_width,
|
|
315
|
-
fill_color=thing.color,
|
|
316
|
-
fill_opacity=thing.opacity,
|
|
317
|
-
popup=thing.popup,
|
|
318
|
-
).add_to(map_)
|
|
319
|
-
|
|
320
|
-
return map_
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
try:
|
|
324
|
-
# add shapely support if present
|
|
325
|
-
from shapely.geometry import (
|
|
326
|
-
Point as ShpPoint,
|
|
327
|
-
LineString as ShpLineString,
|
|
328
|
-
LinearRing as ShpLinearRing,
|
|
329
|
-
Polygon as ShpPolygon,
|
|
330
|
-
MultiPoint as ShpMultiPoint,
|
|
331
|
-
MultiPolygon as ShpMultiPolygon,
|
|
332
|
-
MultiLineString as ShpMultiLineString
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
auto_convert(ShpPoint, lambda p: (p.y, p.x))
|
|
336
|
-
auto_convert(ShpLineString, lambda ls: [(c[1], c[0]) for c in ls.coords])
|
|
337
|
-
auto_convert(ShpLinearRing, lambda ls: [(c[1], c[0]) for c in ls.coords])
|
|
338
|
-
auto_convert(ShpPolygon, lambda p: [(c[1], c[0]) for c in p.exterior.coords])
|
|
339
|
-
auto_convert(ShpMultiPoint, lambda mp: list(mp.geoms))
|
|
340
|
-
auto_convert(ShpMultiLineString, lambda mp: list(mp.geoms))
|
|
341
|
-
auto_convert(ShpMultiPolygon, lambda mp: list(mp.geoms))
|
|
342
|
-
except ImportError:
|
|
343
|
-
# shapely not present, do nothing
|
|
344
|
-
pass
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
try:
|
|
348
|
-
# add shapely support if present
|
|
349
|
-
# (https://telluric.readthedocs.io/en/latest/index.html)
|
|
350
|
-
from telluric import GeoVector, FeatureCollection, GeoFeature
|
|
351
|
-
|
|
352
|
-
auto_convert(GeoVector, lambda gv: gv.get_shape(gv.crs))
|
|
353
|
-
auto_convert(GeoFeature, lambda gf: gf.geometry)
|
|
354
|
-
auto_convert(FeatureCollection, lambda fc: fc.geometries)
|
|
355
|
-
except ImportError:
|
|
356
|
-
# telluric not present, do nothing
|
|
357
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|