gmaprium 0.1.0__py3-none-any.whl
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.
- gmaprium/__init__.py +21 -0
- gmaprium/elements.py +692 -0
- gmaprium/streamlit.py +40 -0
- gmaprium/tiles.py +58 -0
- gmaprium-0.1.0.dist-info/METADATA +183 -0
- gmaprium-0.1.0.dist-info/RECORD +8 -0
- gmaprium-0.1.0.dist-info/WHEEL +4 -0
- gmaprium-0.1.0.dist-info/licenses/LICENSE +21 -0
gmaprium/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Folium-style Python helpers for Google Maps."""
|
|
2
|
+
|
|
3
|
+
from .elements import Circle, GeoJson, GoogleMapsError, HeatMap, LayerControl, Map, Marker, Polygon, Polyline
|
|
4
|
+
from .streamlit import st_google_map
|
|
5
|
+
from .tiles import GoogleMapType, add_google_tiles, google_tiles_url
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Circle",
|
|
9
|
+
"GeoJson",
|
|
10
|
+
"GoogleMapType",
|
|
11
|
+
"GoogleMapsError",
|
|
12
|
+
"HeatMap",
|
|
13
|
+
"LayerControl",
|
|
14
|
+
"Map",
|
|
15
|
+
"Marker",
|
|
16
|
+
"Polygon",
|
|
17
|
+
"Polyline",
|
|
18
|
+
"add_google_tiles",
|
|
19
|
+
"google_tiles_url",
|
|
20
|
+
"st_google_map",
|
|
21
|
+
]
|
gmaprium/elements.py
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
"""Folium-style map elements for Google Maps output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Iterable, Sequence
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
Location = Sequence[float]
|
|
12
|
+
_MAP_TYPES = {"roadmap", "satellite", "hybrid", "terrain"}
|
|
13
|
+
_DEMO_MAP_ID = "DEMO_MAP_ID"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GoogleMapsError(RuntimeError):
|
|
17
|
+
"""Raised when a map cannot be rendered with the supplied configuration."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Element:
|
|
21
|
+
"""Base class for objects that can be added to a map."""
|
|
22
|
+
|
|
23
|
+
def add_to(self, map_obj: "Map") -> "Element":
|
|
24
|
+
map_obj.add_child(self)
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
def to_spec(self) -> dict[str, Any]:
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Map:
|
|
32
|
+
"""A Folium-style Google Maps HTML renderer."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
location: Location,
|
|
37
|
+
zoom_start: int = 10,
|
|
38
|
+
*,
|
|
39
|
+
api_key: str | None = None,
|
|
40
|
+
map_type: str = "roadmap",
|
|
41
|
+
width: str | int = "100%",
|
|
42
|
+
height: str | int = "100%",
|
|
43
|
+
map_id: str | None = None,
|
|
44
|
+
options: dict[str, Any] | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
self.location = _location(location)
|
|
47
|
+
self.zoom_start = zoom_start
|
|
48
|
+
self.api_key = api_key
|
|
49
|
+
if map_type not in _MAP_TYPES:
|
|
50
|
+
supported = ", ".join(sorted(_MAP_TYPES))
|
|
51
|
+
raise ValueError(f"Unsupported map_type {map_type!r}. Expected one of: {supported}.")
|
|
52
|
+
self.map_type = map_type
|
|
53
|
+
self.width = _css_size(width)
|
|
54
|
+
self.height = _css_size(height)
|
|
55
|
+
self.map_id = map_id
|
|
56
|
+
self.options = options or {}
|
|
57
|
+
self.children: list[Element] = []
|
|
58
|
+
self._id = f"fgm_{id(self):x}"
|
|
59
|
+
|
|
60
|
+
def add_child(self, child: Element) -> Element:
|
|
61
|
+
self.children.append(child)
|
|
62
|
+
return child
|
|
63
|
+
|
|
64
|
+
def render_fragment(self) -> str:
|
|
65
|
+
api_key = self._resolve_api_key()
|
|
66
|
+
specs = [child.to_spec() for child in self.children]
|
|
67
|
+
needs_marker = any(spec["type"] == "marker" for spec in specs)
|
|
68
|
+
layer_control = any(spec["type"] == "layer_control" for spec in specs)
|
|
69
|
+
drawable_specs = [spec for spec in specs if spec["type"] != "layer_control"]
|
|
70
|
+
config = {
|
|
71
|
+
"center": {"lat": self.location[0], "lng": self.location[1]},
|
|
72
|
+
"zoom": self.zoom_start,
|
|
73
|
+
"mapTypeId": self.map_type,
|
|
74
|
+
**self.options,
|
|
75
|
+
}
|
|
76
|
+
if self.map_id or needs_marker:
|
|
77
|
+
config["mapId"] = self.map_id or _DEMO_MAP_ID
|
|
78
|
+
|
|
79
|
+
context = {
|
|
80
|
+
"api_key": api_key,
|
|
81
|
+
"callback": f"{self._id}_init",
|
|
82
|
+
"config": config,
|
|
83
|
+
"height": self.height,
|
|
84
|
+
"map_id": self._id,
|
|
85
|
+
"specs": drawable_specs,
|
|
86
|
+
"width": self.width,
|
|
87
|
+
"layer_control": layer_control,
|
|
88
|
+
}
|
|
89
|
+
return _render_fragment(context)
|
|
90
|
+
|
|
91
|
+
def render_html(self) -> str:
|
|
92
|
+
fragment = self.render_fragment()
|
|
93
|
+
return "\n".join(
|
|
94
|
+
[
|
|
95
|
+
"<!doctype html>",
|
|
96
|
+
'<html lang="en">',
|
|
97
|
+
"<head>",
|
|
98
|
+
' <meta charset="utf-8">',
|
|
99
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
100
|
+
" <title>Google Map</title>",
|
|
101
|
+
"</head>",
|
|
102
|
+
"<body>",
|
|
103
|
+
fragment,
|
|
104
|
+
"</body>",
|
|
105
|
+
"</html>",
|
|
106
|
+
]
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def save(self, path: str | os.PathLike[str]) -> None:
|
|
110
|
+
Path(path).write_text(self.render_html(), encoding="utf-8")
|
|
111
|
+
|
|
112
|
+
def _repr_html_(self) -> str:
|
|
113
|
+
return self.render_fragment()
|
|
114
|
+
|
|
115
|
+
def _resolve_api_key(self) -> str:
|
|
116
|
+
api_key = self.api_key or os.environ.get("GOOGLE_MAPS_API_KEY")
|
|
117
|
+
if not api_key:
|
|
118
|
+
raise GoogleMapsError("Google Maps API key is required. Pass api_key=... or set GOOGLE_MAPS_API_KEY.")
|
|
119
|
+
return api_key
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class Marker(Element):
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
location: Location,
|
|
126
|
+
*,
|
|
127
|
+
popup: str | None = None,
|
|
128
|
+
tooltip: str | None = None,
|
|
129
|
+
icon: str | None = None,
|
|
130
|
+
draggable: bool = False,
|
|
131
|
+
name: str | None = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
self.location = _location(location)
|
|
134
|
+
self.popup = popup
|
|
135
|
+
self.tooltip = tooltip
|
|
136
|
+
self.icon = icon
|
|
137
|
+
self.draggable = draggable
|
|
138
|
+
self.name = name
|
|
139
|
+
|
|
140
|
+
def to_spec(self) -> dict[str, Any]:
|
|
141
|
+
return {
|
|
142
|
+
"type": "marker",
|
|
143
|
+
"name": self.name,
|
|
144
|
+
"position": {"lat": self.location[0], "lng": self.location[1]},
|
|
145
|
+
"popup": self.popup,
|
|
146
|
+
"tooltip": self.tooltip,
|
|
147
|
+
"icon": self.icon,
|
|
148
|
+
"draggable": self.draggable,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class Polyline(Element):
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
locations: Iterable[Location],
|
|
156
|
+
*,
|
|
157
|
+
color: str = "#3388ff",
|
|
158
|
+
weight: int = 3,
|
|
159
|
+
opacity: float = 1.0,
|
|
160
|
+
name: str | None = None,
|
|
161
|
+
) -> None:
|
|
162
|
+
self.locations = [_lat_lng(location) for location in locations]
|
|
163
|
+
self.color = color
|
|
164
|
+
self.weight = weight
|
|
165
|
+
self.opacity = opacity
|
|
166
|
+
self.name = name
|
|
167
|
+
|
|
168
|
+
def to_spec(self) -> dict[str, Any]:
|
|
169
|
+
return {
|
|
170
|
+
"type": "polyline",
|
|
171
|
+
"name": self.name,
|
|
172
|
+
"path": self.locations,
|
|
173
|
+
"options": {"strokeColor": self.color, "strokeWeight": self.weight, "strokeOpacity": self.opacity},
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class Polygon(Element):
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
locations: Iterable[Location] | Iterable[Iterable[Location]],
|
|
181
|
+
*,
|
|
182
|
+
color: str = "#3388ff",
|
|
183
|
+
fill_color: str | None = None,
|
|
184
|
+
fill_opacity: float = 0.2,
|
|
185
|
+
weight: int = 3,
|
|
186
|
+
name: str | None = None,
|
|
187
|
+
) -> None:
|
|
188
|
+
self.locations = _polygon_paths(locations)
|
|
189
|
+
self.color = color
|
|
190
|
+
self.fill_color = fill_color or color
|
|
191
|
+
self.fill_opacity = fill_opacity
|
|
192
|
+
self.weight = weight
|
|
193
|
+
self.name = name
|
|
194
|
+
|
|
195
|
+
def to_spec(self) -> dict[str, Any]:
|
|
196
|
+
return {
|
|
197
|
+
"type": "polygon",
|
|
198
|
+
"name": self.name,
|
|
199
|
+
"paths": self.locations,
|
|
200
|
+
"options": {
|
|
201
|
+
"strokeColor": self.color,
|
|
202
|
+
"strokeWeight": self.weight,
|
|
203
|
+
"fillColor": self.fill_color,
|
|
204
|
+
"fillOpacity": self.fill_opacity,
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class Circle(Element):
|
|
210
|
+
def __init__(
|
|
211
|
+
self,
|
|
212
|
+
location: Location,
|
|
213
|
+
radius: float,
|
|
214
|
+
*,
|
|
215
|
+
color: str = "#3388ff",
|
|
216
|
+
fill_color: str | None = None,
|
|
217
|
+
fill_opacity: float = 0.2,
|
|
218
|
+
weight: int = 3,
|
|
219
|
+
name: str | None = None,
|
|
220
|
+
) -> None:
|
|
221
|
+
self.location = _location(location)
|
|
222
|
+
self.radius = radius
|
|
223
|
+
self.color = color
|
|
224
|
+
self.fill_color = fill_color or color
|
|
225
|
+
self.fill_opacity = fill_opacity
|
|
226
|
+
self.weight = weight
|
|
227
|
+
self.name = name
|
|
228
|
+
|
|
229
|
+
def to_spec(self) -> dict[str, Any]:
|
|
230
|
+
return {
|
|
231
|
+
"type": "circle",
|
|
232
|
+
"name": self.name,
|
|
233
|
+
"center": {"lat": self.location[0], "lng": self.location[1]},
|
|
234
|
+
"radius": self.radius,
|
|
235
|
+
"options": {
|
|
236
|
+
"strokeColor": self.color,
|
|
237
|
+
"strokeWeight": self.weight,
|
|
238
|
+
"fillColor": self.fill_color,
|
|
239
|
+
"fillOpacity": self.fill_opacity,
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class GeoJson(Element):
|
|
245
|
+
def __init__(self, data: Any, *, name: str | None = None, style_function: Any | None = None) -> None:
|
|
246
|
+
self.data = _geojson_data(data)
|
|
247
|
+
self.name = name
|
|
248
|
+
self.style_function = style_function
|
|
249
|
+
|
|
250
|
+
def to_spec(self) -> dict[str, Any]:
|
|
251
|
+
style = None
|
|
252
|
+
if self.style_function:
|
|
253
|
+
style = self.style_function(_sample_geojson_feature(self.data))
|
|
254
|
+
return {"type": "geojson", "name": self.name, "data": self.data, "style": style}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class HeatMap(Element):
|
|
258
|
+
def __init__(
|
|
259
|
+
self,
|
|
260
|
+
data: Iterable[Any],
|
|
261
|
+
*,
|
|
262
|
+
name: str | None = None,
|
|
263
|
+
radius: int = 25,
|
|
264
|
+
blur: int = 15,
|
|
265
|
+
min_opacity: float = 0.05,
|
|
266
|
+
max_zoom: int | None = 18,
|
|
267
|
+
max_value: float = 1.0,
|
|
268
|
+
gradient: dict[float, str] | None = None,
|
|
269
|
+
opacity: float = 1.0,
|
|
270
|
+
intensity: float = 1.0,
|
|
271
|
+
threshold: float = 0.03,
|
|
272
|
+
scale_radius_with_zoom: bool = False,
|
|
273
|
+
min_radius: int = 6,
|
|
274
|
+
max_radius: int = 240,
|
|
275
|
+
) -> None:
|
|
276
|
+
self.data = [_heatmap_point(point) for point in data]
|
|
277
|
+
self.name = name
|
|
278
|
+
self.radius = radius
|
|
279
|
+
self.blur = blur
|
|
280
|
+
self.min_opacity = min_opacity
|
|
281
|
+
self.max_zoom = max_zoom
|
|
282
|
+
self.max_value = max_value
|
|
283
|
+
self.gradient = gradient or {0.4: "blue", 0.6: "cyan", 0.7: "lime", 0.8: "yellow", 1.0: "red"}
|
|
284
|
+
self.opacity = opacity
|
|
285
|
+
self.intensity = intensity
|
|
286
|
+
self.threshold = threshold
|
|
287
|
+
self.scale_radius_with_zoom = scale_radius_with_zoom
|
|
288
|
+
self.min_radius = min_radius
|
|
289
|
+
self.max_radius = max_radius
|
|
290
|
+
|
|
291
|
+
def to_spec(self) -> dict[str, Any]:
|
|
292
|
+
return {
|
|
293
|
+
"type": "heatmap",
|
|
294
|
+
"name": self.name,
|
|
295
|
+
"data": self.data,
|
|
296
|
+
"options": {
|
|
297
|
+
"radiusPixels": self.radius,
|
|
298
|
+
"blurPixels": self.blur,
|
|
299
|
+
"minOpacity": self.min_opacity,
|
|
300
|
+
"maxZoom": self.max_zoom,
|
|
301
|
+
"max": self.max_value,
|
|
302
|
+
"gradient": self.gradient,
|
|
303
|
+
"opacity": self.opacity,
|
|
304
|
+
"intensity": self.intensity,
|
|
305
|
+
"threshold": self.threshold,
|
|
306
|
+
"scaleRadiusWithZoom": self.scale_radius_with_zoom,
|
|
307
|
+
"minRadiusPixels": self.min_radius,
|
|
308
|
+
"maxRadiusPixels": self.max_radius,
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class LayerControl(Element):
|
|
314
|
+
def to_spec(self) -> dict[str, Any]:
|
|
315
|
+
return {"type": "layer_control"}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _location(value: Location) -> tuple[float, float]:
|
|
319
|
+
if len(value) != 2:
|
|
320
|
+
raise ValueError("Location must be a [lat, lng] pair.")
|
|
321
|
+
return (float(value[0]), float(value[1]))
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _lat_lng(value: Location) -> dict[str, float]:
|
|
325
|
+
lat, lng = _location(value)
|
|
326
|
+
return {"lat": lat, "lng": lng}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _css_size(value: str | int) -> str:
|
|
330
|
+
if isinstance(value, int):
|
|
331
|
+
return f"{value}px"
|
|
332
|
+
return value
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _polygon_paths(locations: Iterable[Location] | Iterable[Iterable[Location]]) -> list[Any]:
|
|
336
|
+
items = list(locations)
|
|
337
|
+
if not items:
|
|
338
|
+
return []
|
|
339
|
+
first = items[0]
|
|
340
|
+
if _looks_like_location(first):
|
|
341
|
+
return [_lat_lng(item) for item in items] # type: ignore[arg-type]
|
|
342
|
+
return [[_lat_lng(point) for point in path] for path in items] # type: ignore[arg-type]
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _looks_like_location(value: Any) -> bool:
|
|
346
|
+
return isinstance(value, Sequence) and len(value) == 2 and all(isinstance(item, (int, float)) for item in value)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _geojson_data(data: Any) -> dict[str, Any]:
|
|
350
|
+
if isinstance(data, (str, os.PathLike)):
|
|
351
|
+
return json.loads(Path(data).read_text(encoding="utf-8"))
|
|
352
|
+
if isinstance(data, dict):
|
|
353
|
+
return data
|
|
354
|
+
if hasattr(data, "__geo_interface__"):
|
|
355
|
+
return data.__geo_interface__
|
|
356
|
+
if hasattr(data, "to_json"):
|
|
357
|
+
return json.loads(data.to_json())
|
|
358
|
+
raise TypeError("GeoJson data must be a dict, path, __geo_interface__ object, or object with to_json().")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _sample_geojson_feature(data: dict[str, Any]) -> dict[str, Any]:
|
|
362
|
+
if data.get("type") == "FeatureCollection" and data.get("features"):
|
|
363
|
+
return data["features"][0]
|
|
364
|
+
if data.get("type") == "Feature":
|
|
365
|
+
return data
|
|
366
|
+
return {"type": "Feature", "properties": {}, "geometry": None}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _heatmap_point(point: Any) -> dict[str, Any]:
|
|
370
|
+
if isinstance(point, dict):
|
|
371
|
+
location = point.get("location")
|
|
372
|
+
if location is None:
|
|
373
|
+
location = [point.get("lat"), point.get("lng")]
|
|
374
|
+
lat, lng = _location(location)
|
|
375
|
+
return {"position": [lng, lat], "weight": float(point.get("weight", 1))}
|
|
376
|
+
|
|
377
|
+
if isinstance(point, Sequence) and len(point) in {2, 3}:
|
|
378
|
+
lat, lng = _location(point[:2])
|
|
379
|
+
weight = float(point[2]) if len(point) == 3 else 1.0
|
|
380
|
+
return {"position": [lng, lat], "weight": weight}
|
|
381
|
+
|
|
382
|
+
raise TypeError("HeatMap points must be [lat, lng], [lat, lng, weight], or {'location': [lat, lng], 'weight': n}.")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _json(value: Any) -> str:
|
|
386
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _render_fragment(context: dict[str, Any]) -> str:
|
|
390
|
+
specs_json = _json(context["specs"])
|
|
391
|
+
config_json = _json(context["config"])
|
|
392
|
+
api_key = _json(context["api_key"])
|
|
393
|
+
map_id = context["map_id"]
|
|
394
|
+
callback = context["callback"]
|
|
395
|
+
control_style = (
|
|
396
|
+
f"<style>#{map_id}_layers{{background:#fff;"
|
|
397
|
+
"border:1px solid #dadce0;border-radius:4px;padding:8px;font:13px Arial,sans-serif;"
|
|
398
|
+
"box-shadow:0 1px 4px rgba(0,0,0,.2);margin:10px}"
|
|
399
|
+
f"#{map_id}_layers label{{display:block;white-space:nowrap;margin:4px 0}}</style>\n"
|
|
400
|
+
if context["layer_control"]
|
|
401
|
+
else ""
|
|
402
|
+
)
|
|
403
|
+
control_div = f'<div id="{map_id}_layers" hidden></div>' if context["layer_control"] else ""
|
|
404
|
+
return f"""<div id="{map_id}_wrap" style="position:relative;width:{context['width']};height:{context['height']};">
|
|
405
|
+
<div id="{map_id}" style="width:100%;height:100%;"></div>
|
|
406
|
+
{control_div}
|
|
407
|
+
</div>
|
|
408
|
+
{control_style}<script>
|
|
409
|
+
function {callback}_loadScript(src, test) {{
|
|
410
|
+
if (test()) return Promise.resolve();
|
|
411
|
+
return new Promise((resolve, reject) => {{
|
|
412
|
+
const existing = Array.from(document.scripts).find(script => script.dataset.fgmSrc === src);
|
|
413
|
+
if (existing) {{
|
|
414
|
+
existing.addEventListener("load", resolve, {{ once: true }});
|
|
415
|
+
existing.addEventListener("error", reject, {{ once: true }});
|
|
416
|
+
if (test()) resolve();
|
|
417
|
+
return;
|
|
418
|
+
}}
|
|
419
|
+
const script = document.createElement("script");
|
|
420
|
+
script.async = true;
|
|
421
|
+
script.dataset.fgmSrc = src;
|
|
422
|
+
script.src = src;
|
|
423
|
+
script.addEventListener("load", resolve, {{ once: true }});
|
|
424
|
+
script.addEventListener("error", reject, {{ once: true }});
|
|
425
|
+
document.head.appendChild(script);
|
|
426
|
+
}});
|
|
427
|
+
}}
|
|
428
|
+
|
|
429
|
+
window.{callback} = async function() {{
|
|
430
|
+
const specs = {specs_json};
|
|
431
|
+
const config = {config_json};
|
|
432
|
+
const namedLayers = [];
|
|
433
|
+
const {{ Map, InfoWindow, Polyline, Polygon, Circle }} = await google.maps.importLibrary("maps");
|
|
434
|
+
const {{ AdvancedMarkerElement }} = await google.maps.importLibrary("marker");
|
|
435
|
+
const map = new Map(document.getElementById("{map_id}"), config);
|
|
436
|
+
const panorama = map.getStreetView();
|
|
437
|
+
let streetViewVisible = panorama.getVisible();
|
|
438
|
+
|
|
439
|
+
function track(name, layer, setVisible, options = {{}}) {{
|
|
440
|
+
if (!name) return;
|
|
441
|
+
const entry = {{
|
|
442
|
+
name,
|
|
443
|
+
layer,
|
|
444
|
+
setVisible,
|
|
445
|
+
hideInStreetView: Boolean(options.hideInStreetView),
|
|
446
|
+
layerVisible: true,
|
|
447
|
+
applyVisibility() {{
|
|
448
|
+
setVisible(this.layerVisible && !(this.hideInStreetView && streetViewVisible));
|
|
449
|
+
}}
|
|
450
|
+
}};
|
|
451
|
+
namedLayers.push(entry);
|
|
452
|
+
}}
|
|
453
|
+
|
|
454
|
+
for (const spec of specs) {{
|
|
455
|
+
if (spec.type === "marker") {{
|
|
456
|
+
const markerOptions = {{
|
|
457
|
+
map,
|
|
458
|
+
position: spec.position,
|
|
459
|
+
title: spec.tooltip || undefined,
|
|
460
|
+
gmpClickable: Boolean(spec.popup),
|
|
461
|
+
gmpDraggable: spec.draggable || false
|
|
462
|
+
}};
|
|
463
|
+
if (spec.icon) {{
|
|
464
|
+
const img = document.createElement("img");
|
|
465
|
+
img.src = spec.icon;
|
|
466
|
+
markerOptions.content = img;
|
|
467
|
+
}}
|
|
468
|
+
const marker = new AdvancedMarkerElement(markerOptions);
|
|
469
|
+
if (spec.popup) {{
|
|
470
|
+
const info = new InfoWindow({{ content: spec.popup }});
|
|
471
|
+
marker.addListener("click", () => info.open({{ anchor: marker, map }}));
|
|
472
|
+
}}
|
|
473
|
+
track(spec.name, marker, visible => marker.map = visible ? map : null);
|
|
474
|
+
}} else if (spec.type === "polyline") {{
|
|
475
|
+
const layer = new Polyline({{ map, path: spec.path, ...spec.options }});
|
|
476
|
+
track(spec.name, layer, visible => layer.setMap(visible ? map : null));
|
|
477
|
+
}} else if (spec.type === "polygon") {{
|
|
478
|
+
const layer = new Polygon({{ map, paths: spec.paths, ...spec.options }});
|
|
479
|
+
track(spec.name, layer, visible => layer.setMap(visible ? map : null));
|
|
480
|
+
}} else if (spec.type === "circle") {{
|
|
481
|
+
const layer = new Circle({{ map, center: spec.center, radius: spec.radius, ...spec.options }});
|
|
482
|
+
track(spec.name, layer, visible => layer.setMap(visible ? map : null));
|
|
483
|
+
}} else if (spec.type === "geojson") {{
|
|
484
|
+
const layer = new google.maps.Data({{ map }});
|
|
485
|
+
layer.addGeoJson(spec.data);
|
|
486
|
+
if (spec.style) layer.setStyle(spec.style);
|
|
487
|
+
track(spec.name, layer, visible => layer.setMap(visible ? map : null));
|
|
488
|
+
}} else if (spec.type === "heatmap") {{
|
|
489
|
+
class CanvasHeatmapOverlay extends google.maps.OverlayView {{
|
|
490
|
+
constructor(data, options, baseZoom) {{
|
|
491
|
+
super();
|
|
492
|
+
this.data = data;
|
|
493
|
+
this.options = options;
|
|
494
|
+
this.baseZoom = baseZoom;
|
|
495
|
+
this.canvas = null;
|
|
496
|
+
this.listeners = [];
|
|
497
|
+
this.circle = null;
|
|
498
|
+
this.circleRadius = null;
|
|
499
|
+
this.circleBlur = null;
|
|
500
|
+
this.gradientPixels = null;
|
|
501
|
+
this.gradientKey = null;
|
|
502
|
+
}}
|
|
503
|
+
|
|
504
|
+
onAdd() {{
|
|
505
|
+
this.canvas = document.createElement("canvas");
|
|
506
|
+
this.canvas.style.position = "absolute";
|
|
507
|
+
this.canvas.style.inset = "0";
|
|
508
|
+
this.canvas.style.zIndex = "5";
|
|
509
|
+
this.canvas.style.pointerEvents = "none";
|
|
510
|
+
this.canvas.dataset.fgmHeatmap = "true";
|
|
511
|
+
this.getMap().getDiv().appendChild(this.canvas);
|
|
512
|
+
this.listeners.push(google.maps.event.addListener(this.getMap(), "idle", () => this.draw()));
|
|
513
|
+
this.listeners.push(google.maps.event.addListener(this.getMap(), "bounds_changed", () => this.draw()));
|
|
514
|
+
this.listeners.push(google.maps.event.addListener(this.getMap(), "zoom_changed", () => this.draw()));
|
|
515
|
+
}}
|
|
516
|
+
|
|
517
|
+
draw() {{
|
|
518
|
+
if (!this.canvas) return;
|
|
519
|
+
const projection = this.getProjection();
|
|
520
|
+
const mapDiv = this.getMap().getDiv();
|
|
521
|
+
if (!projection || !mapDiv) return;
|
|
522
|
+
const width = Math.max(1, mapDiv.clientWidth);
|
|
523
|
+
const height = Math.max(1, mapDiv.clientHeight);
|
|
524
|
+
this.canvas.width = width;
|
|
525
|
+
this.canvas.height = height;
|
|
526
|
+
this.canvas.style.width = width + "px";
|
|
527
|
+
this.canvas.style.height = height + "px";
|
|
528
|
+
|
|
529
|
+
const ctx = this.canvas.getContext("2d", {{ willReadFrequently: true }});
|
|
530
|
+
ctx.clearRect(0, 0, width, height);
|
|
531
|
+
|
|
532
|
+
const radius = this.options.radiusPixels || 25;
|
|
533
|
+
const blur = this.options.blurPixels === undefined ? 15 : this.options.blurPixels;
|
|
534
|
+
const drawRadius = radius + blur;
|
|
535
|
+
const circle = this.getCircle(radius, blur);
|
|
536
|
+
const zoom = this.getMap().getZoom() ?? this.baseZoom;
|
|
537
|
+
const maxZoom = this.options.maxZoom ?? 18;
|
|
538
|
+
const zoomIntensity = 1 / Math.pow(2, Math.max(0, Math.min(maxZoom - zoom, 12)));
|
|
539
|
+
const maxValue = this.options.max || 1;
|
|
540
|
+
const minOpacity = this.options.minOpacity ?? 0.05;
|
|
541
|
+
const bounds = {{
|
|
542
|
+
minX: -drawRadius,
|
|
543
|
+
minY: -drawRadius,
|
|
544
|
+
maxX: width + drawRadius,
|
|
545
|
+
maxY: height + drawRadius
|
|
546
|
+
}};
|
|
547
|
+
const cellSize = drawRadius / 2;
|
|
548
|
+
const grid = [];
|
|
549
|
+
|
|
550
|
+
for (const point of this.data) {{
|
|
551
|
+
const lng = point.position[0];
|
|
552
|
+
const lat = point.position[1];
|
|
553
|
+
const pixel = projection.fromLatLngToContainerPixel(new google.maps.LatLng(lat, lng));
|
|
554
|
+
if (!pixel) continue;
|
|
555
|
+
if (pixel.x < bounds.minX || pixel.x > bounds.maxX || pixel.y < bounds.minY || pixel.y > bounds.maxY) continue;
|
|
556
|
+
const x = Math.floor(pixel.x / cellSize) + 2;
|
|
557
|
+
const y = Math.floor(pixel.y / cellSize) + 2;
|
|
558
|
+
const value = (point.weight === undefined ? 1 : point.weight) * zoomIntensity;
|
|
559
|
+
grid[y] = grid[y] || [];
|
|
560
|
+
const cell = grid[y][x];
|
|
561
|
+
if (!cell) {{
|
|
562
|
+
grid[y][x] = [pixel.x, pixel.y, value];
|
|
563
|
+
}} else {{
|
|
564
|
+
cell[0] = (cell[0] * cell[2] + pixel.x * value) / (cell[2] + value);
|
|
565
|
+
cell[1] = (cell[1] * cell[2] + pixel.y * value) / (cell[2] + value);
|
|
566
|
+
cell[2] += value;
|
|
567
|
+
}}
|
|
568
|
+
}}
|
|
569
|
+
|
|
570
|
+
for (const row of grid) {{
|
|
571
|
+
if (!row) continue;
|
|
572
|
+
for (const cell of row) {{
|
|
573
|
+
if (!cell) continue;
|
|
574
|
+
ctx.globalAlpha = Math.min(Math.max(cell[2] / maxValue, minOpacity), 1);
|
|
575
|
+
ctx.drawImage(circle, Math.round(cell[0]) - drawRadius, Math.round(cell[1]) - drawRadius);
|
|
576
|
+
}}
|
|
577
|
+
}}
|
|
578
|
+
ctx.globalAlpha = 1;
|
|
579
|
+
|
|
580
|
+
const image = ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
|
581
|
+
this.colorize(image.data, this.getGradient());
|
|
582
|
+
ctx.putImageData(image, 0, 0);
|
|
583
|
+
}}
|
|
584
|
+
|
|
585
|
+
getCircle(radius, blur) {{
|
|
586
|
+
if (this.circle && this.circleRadius === radius && this.circleBlur === blur) return this.circle;
|
|
587
|
+
const drawRadius = radius + blur;
|
|
588
|
+
const circle = document.createElement("canvas");
|
|
589
|
+
const ctx = circle.getContext("2d");
|
|
590
|
+
circle.width = circle.height = drawRadius * 2;
|
|
591
|
+
ctx.shadowOffsetX = ctx.shadowOffsetY = drawRadius * 2;
|
|
592
|
+
ctx.shadowBlur = blur;
|
|
593
|
+
ctx.shadowColor = "black";
|
|
594
|
+
ctx.beginPath();
|
|
595
|
+
ctx.arc(-drawRadius, -drawRadius, radius, 0, Math.PI * 2, true);
|
|
596
|
+
ctx.closePath();
|
|
597
|
+
ctx.fill();
|
|
598
|
+
this.circle = circle;
|
|
599
|
+
this.circleRadius = radius;
|
|
600
|
+
this.circleBlur = blur;
|
|
601
|
+
return circle;
|
|
602
|
+
}}
|
|
603
|
+
|
|
604
|
+
getGradient() {{
|
|
605
|
+
const gradient = this.options.gradient || {{ 0.4: "blue", 0.6: "cyan", 0.7: "lime", 0.8: "yellow", 1.0: "red" }};
|
|
606
|
+
const key = JSON.stringify(gradient);
|
|
607
|
+
if (this.gradientPixels && this.gradientKey === key) return this.gradientPixels;
|
|
608
|
+
const canvas = document.createElement("canvas");
|
|
609
|
+
const ctx = canvas.getContext("2d", {{ willReadFrequently: true }});
|
|
610
|
+
const linearGradient = ctx.createLinearGradient(0, 0, 0, 256);
|
|
611
|
+
canvas.width = 1;
|
|
612
|
+
canvas.height = 256;
|
|
613
|
+
for (const stop in gradient) {{
|
|
614
|
+
linearGradient.addColorStop(Number(stop), gradient[stop]);
|
|
615
|
+
}}
|
|
616
|
+
ctx.fillStyle = linearGradient;
|
|
617
|
+
ctx.fillRect(0, 0, 1, 256);
|
|
618
|
+
this.gradientPixels = ctx.getImageData(0, 0, 1, 256).data;
|
|
619
|
+
this.gradientKey = key;
|
|
620
|
+
return this.gradientPixels;
|
|
621
|
+
}}
|
|
622
|
+
|
|
623
|
+
colorize(pixels, gradient) {{
|
|
624
|
+
for (let i = 0; i < pixels.length; i += 4) {{
|
|
625
|
+
const j = pixels[i + 3] * 4;
|
|
626
|
+
if (j) {{
|
|
627
|
+
pixels[i] = gradient[j];
|
|
628
|
+
pixels[i + 1] = gradient[j + 1];
|
|
629
|
+
pixels[i + 2] = gradient[j + 2];
|
|
630
|
+
}}
|
|
631
|
+
}}
|
|
632
|
+
}}
|
|
633
|
+
|
|
634
|
+
onRemove() {{
|
|
635
|
+
for (const listener of this.listeners) {{
|
|
636
|
+
google.maps.event.removeListener(listener);
|
|
637
|
+
}}
|
|
638
|
+
this.listeners = [];
|
|
639
|
+
if (this.canvas) {{
|
|
640
|
+
this.canvas.remove();
|
|
641
|
+
this.canvas = null;
|
|
642
|
+
}}
|
|
643
|
+
}}
|
|
644
|
+
}}
|
|
645
|
+
|
|
646
|
+
const layer = new CanvasHeatmapOverlay(spec.data, spec.options, config.zoom);
|
|
647
|
+
layer.setMap(map);
|
|
648
|
+
track(spec.name, layer, visible => layer.setMap(visible ? map : null), {{ hideInStreetView: true }});
|
|
649
|
+
}}
|
|
650
|
+
}}
|
|
651
|
+
|
|
652
|
+
panorama.addListener("visible_changed", () => {{
|
|
653
|
+
streetViewVisible = panorama.getVisible();
|
|
654
|
+
for (const entry of namedLayers) {{
|
|
655
|
+
entry.applyVisibility();
|
|
656
|
+
}}
|
|
657
|
+
}});
|
|
658
|
+
|
|
659
|
+
const control = document.getElementById("{map_id}_layers");
|
|
660
|
+
if (control && namedLayers.length) {{
|
|
661
|
+
control.hidden = false;
|
|
662
|
+
for (const entry of namedLayers) {{
|
|
663
|
+
const label = document.createElement("label");
|
|
664
|
+
const checkbox = document.createElement("input");
|
|
665
|
+
checkbox.type = "checkbox";
|
|
666
|
+
checkbox.checked = true;
|
|
667
|
+
checkbox.addEventListener("change", () => {{
|
|
668
|
+
entry.layerVisible = checkbox.checked;
|
|
669
|
+
entry.applyVisibility();
|
|
670
|
+
}});
|
|
671
|
+
label.appendChild(checkbox);
|
|
672
|
+
label.appendChild(document.createTextNode(" " + entry.name));
|
|
673
|
+
control.appendChild(label);
|
|
674
|
+
}}
|
|
675
|
+
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(control);
|
|
676
|
+
}}
|
|
677
|
+
}};
|
|
678
|
+
</script>
|
|
679
|
+
<script>
|
|
680
|
+
(function() {{
|
|
681
|
+
if (window.google && google.maps && google.maps.importLibrary) {{
|
|
682
|
+
window.{callback}();
|
|
683
|
+
return;
|
|
684
|
+
}}
|
|
685
|
+
if (document.querySelector("script[data-fgm-google='{callback}']")) return;
|
|
686
|
+
const script = document.createElement("script");
|
|
687
|
+
script.async = true;
|
|
688
|
+
script.dataset.fgmGoogle = "{callback}";
|
|
689
|
+
script.src = "https://maps.googleapis.com/maps/api/js?key=" + encodeURIComponent({api_key}) + "&loading=async&callback={callback}";
|
|
690
|
+
document.head.appendChild(script);
|
|
691
|
+
}})();
|
|
692
|
+
</script>"""
|
gmaprium/streamlit.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Streamlit integration helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .elements import Map
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def st_google_map(
|
|
11
|
+
map_obj: Map,
|
|
12
|
+
*,
|
|
13
|
+
height: int | None = None,
|
|
14
|
+
width: int | None = None,
|
|
15
|
+
scrolling: bool = False,
|
|
16
|
+
**kwargs: Any,
|
|
17
|
+
) -> Any:
|
|
18
|
+
"""Render a gmaprium map in Streamlit."""
|
|
19
|
+
try:
|
|
20
|
+
import streamlit.components.v1 as components
|
|
21
|
+
except ImportError as exc:
|
|
22
|
+
raise RuntimeError('streamlit is required. Install with: pip install "gmaprium[streamlit]"') from exc
|
|
23
|
+
|
|
24
|
+
component_height = height or _height_to_pixels(map_obj.height)
|
|
25
|
+
return components.html(
|
|
26
|
+
map_obj.render_html(),
|
|
27
|
+
height=component_height,
|
|
28
|
+
width=width,
|
|
29
|
+
scrolling=scrolling,
|
|
30
|
+
**kwargs,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _height_to_pixels(value: str) -> int:
|
|
35
|
+
if value.endswith("px"):
|
|
36
|
+
try:
|
|
37
|
+
return int(value[:-2])
|
|
38
|
+
except ValueError:
|
|
39
|
+
return 500
|
|
40
|
+
return 500
|
gmaprium/tiles.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Utilities for adding Google Maps tile layers to Folium maps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
|
|
8
|
+
GoogleMapType = Literal["roadmap", "satellite", "terrain", "hybrid"]
|
|
9
|
+
|
|
10
|
+
_GOOGLE_TILE_ENDPOINT = "https://mt1.google.com/vt/lyrs={layer}&x={x}&y={y}&z={z}"
|
|
11
|
+
_MAP_TYPE_TO_LAYER: dict[GoogleMapType, str] = {
|
|
12
|
+
"roadmap": "m",
|
|
13
|
+
"satellite": "s",
|
|
14
|
+
"terrain": "p",
|
|
15
|
+
"hybrid": "y",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def google_tiles_url(map_type: GoogleMapType = "roadmap", api_key: str | None = None) -> str:
|
|
20
|
+
"""Return a Google Maps tile URL template for Folium."""
|
|
21
|
+
layer = _MAP_TYPE_TO_LAYER.get(map_type)
|
|
22
|
+
if layer is None:
|
|
23
|
+
supported = ", ".join(sorted(_MAP_TYPE_TO_LAYER))
|
|
24
|
+
raise ValueError(f"Unsupported map_type {map_type!r}. Expected one of: {supported}.")
|
|
25
|
+
|
|
26
|
+
url = _GOOGLE_TILE_ENDPOINT.format(layer=layer, x="{x}", y="{y}", z="{z}")
|
|
27
|
+
if api_key:
|
|
28
|
+
url = f"{url}&{urlencode({'key': api_key})}"
|
|
29
|
+
return url
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def add_google_tiles(
|
|
33
|
+
map_obj: object,
|
|
34
|
+
*,
|
|
35
|
+
api_key: str | None = None,
|
|
36
|
+
map_type: GoogleMapType = "roadmap",
|
|
37
|
+
name: str | None = None,
|
|
38
|
+
attr: str = "Google",
|
|
39
|
+
overlay: bool = False,
|
|
40
|
+
control: bool = True,
|
|
41
|
+
show: bool = True,
|
|
42
|
+
) -> object:
|
|
43
|
+
"""Add a Google Maps tile layer to a Folium map and return the layer."""
|
|
44
|
+
try:
|
|
45
|
+
import folium
|
|
46
|
+
except ImportError as exc: # pragma: no cover - dependency metadata should install folium
|
|
47
|
+
raise RuntimeError("folium is required to add Google Maps tiles.") from exc
|
|
48
|
+
|
|
49
|
+
layer = folium.TileLayer(
|
|
50
|
+
tiles=google_tiles_url(map_type=map_type, api_key=api_key),
|
|
51
|
+
name=name or f"Google {map_type.title()}",
|
|
52
|
+
attr=attr,
|
|
53
|
+
overlay=overlay,
|
|
54
|
+
control=control,
|
|
55
|
+
show=show,
|
|
56
|
+
)
|
|
57
|
+
layer.add_to(map_obj)
|
|
58
|
+
return layer
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gmaprium
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Folium-style Python helpers for rendering Google Maps.
|
|
5
|
+
Project-URL: Homepage, https://github.com/KentaroAOKI/gmaprium
|
|
6
|
+
Project-URL: Repository, https://github.com/KentaroAOKI/gmaprium
|
|
7
|
+
Project-URL: Issues, https://github.com/KentaroAOKI/gmaprium/issues
|
|
8
|
+
Author: Kentaro
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: folium,geojson,google-maps,heatmap,maps,streamlit
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Requires-Dist: jinja2>=3.1
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
30
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
31
|
+
Provides-Extra: streamlit
|
|
32
|
+
Requires-Dist: streamlit>=1.30; extra == 'streamlit'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# gmaprium
|
|
36
|
+
|
|
37
|
+
Folium-style Python helpers for rendering Google Maps HTML.
|
|
38
|
+
|
|
39
|
+
gmaprium can be used to create standalone HTML maps, display maps in Jupyter Notebook, add Google tile layers to existing Folium maps, and embed maps in Streamlit apps.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install gmaprium
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from gmaprium import GeoJson, HeatMap, LayerControl, Map, Marker, Polygon
|
|
51
|
+
|
|
52
|
+
m = Map(
|
|
53
|
+
location=[35.6812, 139.7671],
|
|
54
|
+
zoom_start=12,
|
|
55
|
+
api_key="YOUR_GOOGLE_MAPS_API_KEY",
|
|
56
|
+
map_type="roadmap",
|
|
57
|
+
height="500px",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
Marker([35.6812, 139.7671], popup="Tokyo Station", tooltip="Tokyo", name="Stations").add_to(m)
|
|
61
|
+
Polygon(
|
|
62
|
+
[[35.70, 139.70], [35.70, 139.82], [35.62, 139.82], [35.62, 139.70]],
|
|
63
|
+
name="Area",
|
|
64
|
+
).add_to(m)
|
|
65
|
+
HeatMap([[35.6812, 139.7671, 2], [35.6895, 139.6917, 1]], name="Heat").add_to(m)
|
|
66
|
+
LayerControl().add_to(m)
|
|
67
|
+
|
|
68
|
+
# Full HTML document.
|
|
69
|
+
m.save("map.html")
|
|
70
|
+
|
|
71
|
+
# Embeddable HTML fragment for web apps.
|
|
72
|
+
html = m.render_fragment()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
You can also pass the API key with the `GOOGLE_MAPS_API_KEY` environment variable.
|
|
76
|
+
|
|
77
|
+
Markers use Google's `AdvancedMarkerElement`. If you add markers without passing `map_id=...`, the renderer uses Google's `DEMO_MAP_ID` for local testing.
|
|
78
|
+
|
|
79
|
+
## Supported Elements
|
|
80
|
+
|
|
81
|
+
- `Map`
|
|
82
|
+
- `Marker`
|
|
83
|
+
- `Polyline`
|
|
84
|
+
- `Polygon`
|
|
85
|
+
- `Circle`
|
|
86
|
+
- `GeoJson`
|
|
87
|
+
- `HeatMap`
|
|
88
|
+
- `LayerControl`
|
|
89
|
+
|
|
90
|
+
`GeoJson` accepts GeoJSON dictionaries, JSON file paths, objects with `__geo_interface__`, and GeoPandas-like objects with `to_json()`.
|
|
91
|
+
|
|
92
|
+
`HeatMap` uses a canvas overlay instead of Google's deprecated JavaScript `HeatmapLayer`.
|
|
93
|
+
It ports the Leaflet.heat/simpleheat rendering model, including `radius`, `blur`, `min_opacity`, `max_zoom`, `max_value`, and `gradient`.
|
|
94
|
+
The defaults match Leaflet.heat/simpleheat on a typical OSM map: `radius=25`, `blur=15`, `min_opacity=0.05`, `max_zoom=18`, `max_value=1.0`, and the blue-cyan-lime-yellow-red gradient.
|
|
95
|
+
Lower zoom levels are intentionally faded by Leaflet.heat's `max_zoom - zoom` intensity scale. Set `max_zoom` closer to your initial zoom if the heat fades too quickly.
|
|
96
|
+
|
|
97
|
+
The legacy `google_tiles_url()` and `add_google_tiles()` helpers are still available for projects that want to add Google tile URLs to an existing Folium map.
|
|
98
|
+
|
|
99
|
+
## Notebook
|
|
100
|
+
|
|
101
|
+
In Jupyter Notebook, display the map by returning the `Map` object as the last expression in a cell.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import os
|
|
105
|
+
|
|
106
|
+
from gmaprium import HeatMap, LayerControl, Map, Marker
|
|
107
|
+
|
|
108
|
+
os.environ["GOOGLE_MAPS_API_KEY"] = "your-api-key"
|
|
109
|
+
|
|
110
|
+
m = Map(location=[35.6812, 139.7671], zoom_start=12, height="500px")
|
|
111
|
+
Marker([35.6812, 139.7671], popup="Tokyo Station", name="Markers").add_to(m)
|
|
112
|
+
HeatMap([[35.6812, 139.7671, 1.0]], name="Heat", max_zoom=14).add_to(m)
|
|
113
|
+
LayerControl().add_to(m)
|
|
114
|
+
|
|
115
|
+
m
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
You can also display explicitly:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from IPython.display import HTML, display
|
|
122
|
+
|
|
123
|
+
display(HTML(m.render_fragment()))
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The notebook example is available at `examples/example.ipynb`.
|
|
127
|
+
|
|
128
|
+
## Folium Extension
|
|
129
|
+
|
|
130
|
+
If you already use Folium, add Google tile layers to a `folium.Map` with `add_google_tiles()`.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import folium
|
|
134
|
+
|
|
135
|
+
from gmaprium import add_google_tiles
|
|
136
|
+
|
|
137
|
+
m = folium.Map(location=[35.6812, 139.7671], zoom_start=12, tiles=None)
|
|
138
|
+
add_google_tiles(m, api_key="your-api-key", map_type="roadmap", name="Google Roadmap")
|
|
139
|
+
add_google_tiles(m, api_key="your-api-key", map_type="satellite", name="Google Satellite", show=False)
|
|
140
|
+
|
|
141
|
+
folium.LayerControl().add_to(m)
|
|
142
|
+
m.save("folium_google_tiles.html")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
This Folium extension only adds Google tile layers. It does not enable the Google Maps JavaScript API renderer or gmaprium's `HeatMap`.
|
|
146
|
+
|
|
147
|
+
The Folium example is available at `examples/folium_extension.py`.
|
|
148
|
+
|
|
149
|
+
## Streamlit
|
|
150
|
+
|
|
151
|
+
Use `st_google_map()` to render a `gmaprium.Map` inside a Streamlit app.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
import os
|
|
155
|
+
|
|
156
|
+
import streamlit as st
|
|
157
|
+
|
|
158
|
+
from gmaprium import Map, Marker, st_google_map
|
|
159
|
+
|
|
160
|
+
api_key = os.environ["GOOGLE_MAPS_API_KEY"]
|
|
161
|
+
|
|
162
|
+
m = Map(location=[35.6812, 139.7671], zoom_start=12, api_key=api_key, height="600px")
|
|
163
|
+
Marker([35.6812, 139.7671], popup="Tokyo Station").add_to(m)
|
|
164
|
+
|
|
165
|
+
st_google_map(m)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Install the optional dependency and run the example app:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
python -m pip install -e ".[dev,streamlit]"
|
|
172
|
+
export GOOGLE_MAPS_API_KEY="your-api-key"
|
|
173
|
+
streamlit run examples/streamlit_app.py
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The Streamlit example is available at `examples/streamlit_app.py`.
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
python -m pip install -e ".[dev]"
|
|
182
|
+
pytest
|
|
183
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
gmaprium/__init__.py,sha256=RsseaUbwHy638v9s4Ro84JeF4IWM4oDHyTh5eBK66bg,516
|
|
2
|
+
gmaprium/elements.py,sha256=6LFCe3suTBGgVbwIWwI20wp2nbXdwKpzHQjcW1_7ZOg,24794
|
|
3
|
+
gmaprium/streamlit.py,sha256=rd-RhUXjBqBPy5K8SsvkiP9lrgzPJqUT-7i5sc13zIw,958
|
|
4
|
+
gmaprium/tiles.py,sha256=1pEhpsm3_kBxnCT9U3NkVHzecTFM8vBMBrO2KL7PH6I,1835
|
|
5
|
+
gmaprium-0.1.0.dist-info/METADATA,sha256=PyZvyPnuidbizmeTvfkbxlt-kjiF0-wqCqvu5ZEM1yA,5753
|
|
6
|
+
gmaprium-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
gmaprium-0.1.0.dist-info/licenses/LICENSE,sha256=3I50LCak8rl9pvb4R1Ij_06bwTDMJ2d7HHVRiCNKuyE,1069
|
|
8
|
+
gmaprium-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kentaro Aoki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|