isard-map 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.
- isard_map/__init__.py +8 -0
- isard_map/_data.py +48 -0
- isard_map/_geoplateforme.py +30 -0
- isard_map/_map.py +145 -0
- isard_map/static/widget.js +17 -0
- isard_map-0.1.0.dist-info/METADATA +78 -0
- isard_map-0.1.0.dist-info/RECORD +8 -0
- isard_map-0.1.0.dist-info/WHEEL +4 -0
isard_map/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""isard maps in Jupyter: the IGN's WebGL2 map engine as a notebook widget (anywidget)."""
|
|
2
|
+
|
|
3
|
+
from ._data import prop, to_geojson
|
|
4
|
+
from ._geoplateforme import geoplateforme_wfs
|
|
5
|
+
from ._map import Map
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
__all__ = ["Map", "geoplateforme_wfs", "prop", "to_geojson", "__version__"]
|
isard_map/_data.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""The layers' data and options as the map takes them: GeoJSON in longitudes and latitudes, options in camelCase."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def to_geojson(data: Any) -> dict[str, Any]:
|
|
12
|
+
"""A layer's data as GeoJSON: a dict as it is, a JSON string read, anything with `__geo_interface__` (a
|
|
13
|
+
GeoDataFrame, a GeoSeries, a shapely geometry) taken through it, a frame with a CRS first brought to WGS 84."""
|
|
14
|
+
if isinstance(data, dict):
|
|
15
|
+
return data
|
|
16
|
+
if isinstance(data, str):
|
|
17
|
+
return json.loads(data)
|
|
18
|
+
if getattr(data, "crs", None) is not None and hasattr(data, "to_crs"):
|
|
19
|
+
data = data.to_crs(4326)
|
|
20
|
+
interface = getattr(data, "__geo_interface__", None)
|
|
21
|
+
if interface is None:
|
|
22
|
+
raise TypeError(f"isard: no GeoJSON in a {type(data).__name__} (a dict, a JSON string or __geo_interface__)")
|
|
23
|
+
# Through JSON: plain values only (numpy numbers, timestamps written as the frame's to_json writes them)
|
|
24
|
+
return json.loads(json.dumps(interface, default=str))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def prop(name: str, default: Any = None) -> dict[str, Any]:
|
|
28
|
+
"""An option read from each feature's properties: `height=prop("hauteur", 6)`, `default` where a feature has none"""
|
|
29
|
+
return {"property": name} if default is None else {"property": name, "default": default}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def camel(name: str) -> str:
|
|
33
|
+
"""`stroke_color` as the map names it: `strokeColor`"""
|
|
34
|
+
return re.sub(r"_([a-z0-9])", lambda m: m.group(1).upper(), name)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def plain(value: Any) -> Any:
|
|
38
|
+
"""A value as JSON takes it: a column or an array a list (`tolist`), a missing number (NaN) None"""
|
|
39
|
+
if hasattr(value, "tolist"):
|
|
40
|
+
value = value.tolist()
|
|
41
|
+
if isinstance(value, list):
|
|
42
|
+
return [None if isinstance(v, float) and math.isnan(v) else v for v in value]
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def options(values: dict[str, Any]) -> dict[str, Any]:
|
|
47
|
+
"""A layer's or the map's options in camelCase, those left as None out, columns and arrays as lists"""
|
|
48
|
+
return {camel(k): plain(v) for k, v in values.items() if v is not None}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""The IGN's Géoplateforme from Python: its WFS layers as GeoDataFrames (geopandas)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import urlencode
|
|
8
|
+
|
|
9
|
+
WFS = "https://data.geopf.fr/wfs/ows"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def geoplateforme_wfs(type_name: str, bbox: Sequence[float], count: int = 5000) -> Any:
|
|
13
|
+
"""The features of a Géoplateforme WFS layer within `bbox` (west, south, east, north, degrees), as a GeoDataFrame in
|
|
14
|
+
longitudes and latitudes: `geoplateforme_wfs("BDTOPO_V3:batiment", (2.41, 48.84, 2.43, 48.85))`. At most `count`
|
|
15
|
+
features. The type names are those of its capabilities (BD TOPO, ADMIN EXPRESS…). Needs geopandas."""
|
|
16
|
+
import geopandas as gpd
|
|
17
|
+
|
|
18
|
+
west, south, east, north = bbox
|
|
19
|
+
query = {
|
|
20
|
+
"SERVICE": "WFS",
|
|
21
|
+
"VERSION": "2.0.0",
|
|
22
|
+
"REQUEST": "GetFeature",
|
|
23
|
+
"TYPENAMES": type_name,
|
|
24
|
+
"OUTPUTFORMAT": "application/json",
|
|
25
|
+
"SRSNAME": "EPSG:4326",
|
|
26
|
+
# WFS 2.0 in EPSG:4326: latitude first
|
|
27
|
+
"BBOX": f"{south},{west},{north},{east},urn:ogc:def:crs:EPSG::4326",
|
|
28
|
+
"COUNT": str(count),
|
|
29
|
+
}
|
|
30
|
+
return gpd.read_file(f"{WFS}?{urlencode(query)}")
|
isard_map/_map.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""The map widget: an isard map in a notebook, its layers set from Python."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pathlib
|
|
6
|
+
from collections.abc import Callable, Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import anywidget
|
|
10
|
+
import traitlets
|
|
11
|
+
|
|
12
|
+
from ._data import camel, options, to_geojson
|
|
13
|
+
|
|
14
|
+
Camera = dict[str, Any]
|
|
15
|
+
"""Where the map looks: `center` ([longitude, latitude]), `zoom`, `pitch` and `bearing` (degrees)"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Map(anywidget.AnyWidget):
|
|
19
|
+
"""An isard map: the IGN's WebGL2 engine in the notebook, the globe or a flat map, the relief, vector tiles, 3D
|
|
20
|
+
Tiles, point clouds, glTF models and the data of your frames.
|
|
21
|
+
|
|
22
|
+
>>> m = Map(center=(2.35, 48.85), zoom=15, pitch=60, basemap="ign-plan")
|
|
23
|
+
>>> m.add_circles(gdf, color="#e8590c", radius=6)
|
|
24
|
+
>>> m
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
_esm = pathlib.Path(__file__).parent / "static" / "widget.js"
|
|
28
|
+
|
|
29
|
+
#: The map's options at its creation (`GeoMapOptions` of isard, in camelCase): set by the constructor
|
|
30
|
+
options = traitlets.Dict().tag(sync=True)
|
|
31
|
+
#: The map's layers, the last on top: `map.add`'s options with an `id` (set them with `add_…`, `remove`)
|
|
32
|
+
layers = traitlets.List().tag(sync=True)
|
|
33
|
+
#: Where the map stopped last (read only): `center`, `zoom`, `pitch`, `bearing`
|
|
34
|
+
camera = traitlets.Dict().tag(sync=True)
|
|
35
|
+
#: The height of the map in the notebook (CSS)
|
|
36
|
+
height = traitlets.Unicode("500px").tag(sync=True)
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
center: Sequence[float] = (2.35, 46.6),
|
|
41
|
+
zoom: float = 5,
|
|
42
|
+
pitch: float = 0,
|
|
43
|
+
bearing: float = 0,
|
|
44
|
+
basemap: str | None = None,
|
|
45
|
+
height: str = "500px",
|
|
46
|
+
**more: Any,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""`basemap`: a name of isard's (`'ign-plan'`, `'ign-ortho'`, `'osm-liberty'`…); `more`: other options of
|
|
49
|
+
`GeoMap.create` (`terrain`, `projection`, `sun`, `quality`…), in snake_case or camelCase"""
|
|
50
|
+
settings = {"center": list(center), "zoom": zoom, "pitch": pitch, "bearing": bearing, "basemap": basemap}
|
|
51
|
+
super().__init__(options=options({**settings, **more}), height=height)
|
|
52
|
+
self._clicks: list[Callable[[dict[str, Any]], None]] = []
|
|
53
|
+
self._ids = 0
|
|
54
|
+
# The calls made before the map is shown, sent once it is there
|
|
55
|
+
self._ready = False
|
|
56
|
+
self._pending: list[dict[str, Any]] = []
|
|
57
|
+
self.on_msg(self._message)
|
|
58
|
+
|
|
59
|
+
# Layers
|
|
60
|
+
|
|
61
|
+
def add(self, type: str, data: Any = None, id: str | None = None, **settings: Any) -> str:
|
|
62
|
+
"""Adds a layer of a kind of `map.add` (`'circles'`, `'lines'`, `'fills'`, `'extrusions'`, `'tiles3d'`,
|
|
63
|
+
`'models'`…) with its options; its id, given or made, is returned. A layer of the same id is replaced."""
|
|
64
|
+
self._ids += 1
|
|
65
|
+
key = id or f"{type}-{self._ids}"
|
|
66
|
+
spec = {"id": key, "type": type, **options(settings)}
|
|
67
|
+
if data is not None:
|
|
68
|
+
spec["data"] = to_geojson(data)
|
|
69
|
+
self.layers = [*(layer for layer in self.layers if layer["id"] != key), spec]
|
|
70
|
+
return key
|
|
71
|
+
|
|
72
|
+
def add_circles(self, data: Any, **style: Any) -> str:
|
|
73
|
+
"""Points as circles: `color`, `radius`, `stroke_color`, `stroke_width`, `opacity`"""
|
|
74
|
+
return self.add("circles", data, **style)
|
|
75
|
+
|
|
76
|
+
def add_lines(self, data: Any, **style: Any) -> str:
|
|
77
|
+
"""Lines (and the rings of polygons): `color`, `width`, `opacity`"""
|
|
78
|
+
return self.add("lines", data, **style)
|
|
79
|
+
|
|
80
|
+
def add_fills(self, data: Any, **style: Any) -> str:
|
|
81
|
+
"""Polygons filled: `color`, `opacity`"""
|
|
82
|
+
return self.add("fills", data, **style)
|
|
83
|
+
|
|
84
|
+
def add_extrusions(self, data: Any, **style: Any) -> str:
|
|
85
|
+
"""Polygons raised: `height` (metres, or a property's name), `base`, `color`"""
|
|
86
|
+
return self.add("extrusions", data, **style)
|
|
87
|
+
|
|
88
|
+
def add_tiles3d(self, url: str, **style: Any) -> str:
|
|
89
|
+
"""A 3D Tiles tileset (`tileset.json`): `max_screen_space_error`, `opacity`"""
|
|
90
|
+
return self.add("tiles3d", url=url, **style)
|
|
91
|
+
|
|
92
|
+
def set_data(self, id: str, data: Any) -> None:
|
|
93
|
+
"""A layer's data, given anew (the layer kept, its data set in place)"""
|
|
94
|
+
self.layers = [{**layer, "data": to_geojson(data)} if layer["id"] == id else layer for layer in self.layers]
|
|
95
|
+
|
|
96
|
+
def remove(self, id: str) -> None:
|
|
97
|
+
"""Takes a layer away"""
|
|
98
|
+
self.layers = [layer for layer in self.layers if layer["id"] != id]
|
|
99
|
+
|
|
100
|
+
# Camera
|
|
101
|
+
|
|
102
|
+
def fly_to(self, center: Sequence[float] | None = None, duration: float | None = None, **camera: Any) -> None:
|
|
103
|
+
"""A flight to a camera (`center`, `zoom`, `pitch`, `bearing`), zooming out on its way"""
|
|
104
|
+
self._call("flyTo", self._camera(center, camera), duration)
|
|
105
|
+
|
|
106
|
+
def ease_to(self, center: Sequence[float] | None = None, duration: float | None = None, **camera: Any) -> None:
|
|
107
|
+
"""An animated move to a camera, in `duration` milliseconds"""
|
|
108
|
+
self._call("easeTo", self._camera(center, camera), duration)
|
|
109
|
+
|
|
110
|
+
def set_camera(self, center: Sequence[float] | None = None, **camera: Any) -> None:
|
|
111
|
+
"""Moves the camera at once"""
|
|
112
|
+
self._call("setCamera", self._camera(center, camera))
|
|
113
|
+
|
|
114
|
+
# Events
|
|
115
|
+
|
|
116
|
+
def on_click(self, callback: Callable[[dict[str, Any]], None]) -> None:
|
|
117
|
+
"""Calls `callback` with each click: `point` (pixels), `lon_lat`, and `feature` (its layer's `layer` id and
|
|
118
|
+
`properties`) when one of the layers is under it"""
|
|
119
|
+
self._clicks.append(callback)
|
|
120
|
+
|
|
121
|
+
# The widget's messages
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _camera(center: Sequence[float] | None, camera: dict[str, Any]) -> Camera:
|
|
125
|
+
return {camel(k): v for k, v in {**camera, "center": list(center) if center else None}.items() if v is not None}
|
|
126
|
+
|
|
127
|
+
def _call(self, method: str, *args: Any) -> None:
|
|
128
|
+
self._ids += 1
|
|
129
|
+
message = {"kind": "call", "id": self._ids, "method": method, "args": [a for a in args if a is not None]}
|
|
130
|
+
if self._ready:
|
|
131
|
+
self.send(message)
|
|
132
|
+
else:
|
|
133
|
+
self._pending.append(message)
|
|
134
|
+
|
|
135
|
+
def _message(self, _widget: Any, content: dict[str, Any], _buffers: Any) -> None:
|
|
136
|
+
if content.get("kind") == "ready":
|
|
137
|
+
self._ready = True
|
|
138
|
+
for message in self._pending:
|
|
139
|
+
self.send(message)
|
|
140
|
+
self._pending.clear()
|
|
141
|
+
if content.get("kind") == "click":
|
|
142
|
+
click = content["click"]
|
|
143
|
+
event = {"point": click.get("point"), "lon_lat": click.get("lonLat"), "feature": click.get("feature")}
|
|
144
|
+
for callback in self._clicks:
|
|
145
|
+
callback(event)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Made by `bun scripts/build-widget.ts` from js/widget.ts: not to edit (isard 1.12.1)
|
|
2
|
+
var k="1.12.1";var z=`https://cdn.jsdelivr.net/npm/isard@${k}/dist/index.js`,v=`(() => {
|
|
3
|
+
const Native = window.Worker;
|
|
4
|
+
window.Worker = class extends Native {
|
|
5
|
+
constructor(url, options) {
|
|
6
|
+
const href = new URL(String(url), document.baseURI);
|
|
7
|
+
if (href.origin === location.origin || href.protocol === 'blob:' || href.protocol === 'data:') {
|
|
8
|
+
super(url, options);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const source = options && options.type === 'module'
|
|
12
|
+
? 'import ' + JSON.stringify(href.href) + ';'
|
|
13
|
+
: 'importScripts(' + JSON.stringify(href.href) + ');';
|
|
14
|
+
super(URL.createObjectURL(new Blob([source], { type: 'text/javascript' })), options);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
})();`;import{GeoMap as j}from"https://cdn.jsdelivr.net/npm/isard@1.12.1/dist/index.js";var y=(e)=>typeof e==="object"&&e!==null&&!Array.isArray(e),N=["setCamera","easeTo","flyTo","getCamera","pickSurface"],A=(e)=>y(e)&&typeof e.id==="string"&&typeof e.type==="string",w=(e)=>Array.isArray(e)&&e.every(A),O=(e)=>typeof e==="string"&&N.includes(e);var x=(e)=>{if(!y(e))return!1;switch(e.kind){case"init":return y(e.options)&&w(e.layers);case"layers":return w(e.layers);case"call":return typeof e.id==="number"&&O(e.method)&&Array.isArray(e.args);default:return!1}};var f=(e)=>{let r=b(e);return x(r)?r:void 0},b=(e)=>{if(typeof e!=="string")return e;try{return JSON.parse(e)}catch{return}},c=({id:e,...r})=>r,P=(e)=>y(e)&&typeof e.property==="string"&&Object.keys(e).every((r)=>r==="property"||r==="default"),M=(e)=>({...Object.fromEntries(Object.entries(e).map(([r,t])=>{if(r==="data"||r==="values"||!P(t))return[r,t];let{property:n,default:a}=t;return[r,(o)=>o?.[n]??a]})),type:e.type}),L=(e,r)=>{let t=new Map(r.map((a)=>[a.id,a])),n={add:[],data:[],rebuild:[],remove:[]};return t.forEach((a,o)=>{let s=e.get(o);if(s===void 0){n.add.push(a);return}if(s===JSON.stringify(c(a)))return;let d=b(s),{data:p,values:i,...l}=y(d)?d:{},{data:g,values:m,...u}=c(a);(JSON.stringify(l)===JSON.stringify(u)?n.data:n.rebuild).push(a)}),e.forEach((a,o)=>{if(!t.has(o))n.remove.push(o)}),n};var J=()=>{let e=Reflect.get(window,"ReactNativeWebView"),r=typeof e==="object"&&e!==null?Reflect.get(e,"postMessage"):void 0;return typeof r==="function"?{postMessage:(t)=>Reflect.apply(r,e,[t])}:void 0},_=()=>{let e=J();return{send:(t)=>{let n=JSON.stringify(t);if(e)e.postMessage(n);else window.parent.postMessage(n,"*")},listen:(t)=>{let n=(a)=>{let o=a instanceof MessageEvent?f(a.data):void 0;if(o)t(o)};window.addEventListener("message",n),document.addEventListener("message",n)}}},D=(e)=>{let r=Reflect.get(e,"setData");return typeof r==="function"?(t)=>Reflect.apply(r,e,[t]):void 0},R=(e,r)=>{let t=Reflect.get(e,"setValues");if(typeof t!=="function"||!Array.isArray(r))return;Reflect.apply(t,e,[Float32Array.from(r,(n)=>typeof n==="number"?n:Number.NaN)])};class S{map;held=new Map;constructor(e){this.map=e}set(e){let r=L(new Map([...this.held].map(([n,{json:a}])=>[n,a])),e);r.remove.forEach((n)=>{this.held.get(n)?.layer.remove(),this.held.delete(n)}),r.data.forEach((n)=>{let a=this.held.get(n.id),o=a&&D(a.layer);if(a&&o&&F(n.data))o(n.data),R(a.layer,n.values),a.json=JSON.stringify(c(n));else r.rebuild.push(n)}),r.rebuild.forEach((n)=>{this.held.get(n.id)?.layer.remove(),this.held.delete(n.id)}),[...r.add,...r.rebuild].forEach((n)=>{let{values:a,...o}=c(n),s=this.map.add(M({...o,type:n.type}));R(s,a),this.held.set(n.id,{layer:s,json:JSON.stringify(c(n))})});let t=e.flatMap(({id:n})=>this.held.get(n)?.layer??[]);t.slice(0,-1).forEach((n,a)=>{n.moveBefore(t[a+1])})}idOf(e){return[...this.held].find(([,r])=>r.layer===e)?.[0]}get all(){return[...this.held.values()].map(({layer:e})=>e)}}var F=(e)=>typeof e==="object"&&e!==null&&("type"in e),T=(e)=>{let{center:r,zoom:t,pitch:n,bearing:a}=e.getCamera();return{center:r,zoom:t,pitch:n,bearing:a}},I=(e)=>({setCamera:(r)=>e.setCamera(r),easeTo:(r,t)=>e.easeTo(r,t),flyTo:(r,t)=>e.flyTo(r,t===void 0?{}:{duration:t}),getCamera:()=>T(e),pickSurface:(r)=>e.pickSurface(r)}),V=(e,r,t)=>Reflect.apply(e[r],void 0,t),C=(e=_(),r)=>{let t={waiting:[]},n=(s)=>e.send({kind:"error",message:String(s)}),a=(s)=>{let{map:d,layers:p}=t;if(s.kind==="init"){if(t.map)return;o(s.options,s.layers).catch(n);return}if(!d||!p){t.waiting.push(s);return}if(s.kind==="layers"){try{p.set(s.layers)}catch(i){n(i)}return}try{e.send({kind:"result",id:s.id,value:V(I(d),s.method,s.args)??null})}catch(i){e.send({kind:"failed",id:s.id,message:String(i)})}},o=async(s,d)=>{let p=r??document.getElementById("map");if(!p)throw Error("the page has no #map");let i=await j.create(p,s);if(t.gone){i.destroy();return}let l=new S(i);t.map=i,t.layers=l,l.set(d),i.on("click",({point:g,lonLat:m})=>{let u=i.pickFeature(g,{layers:l.all}),h=u&&l.idOf(u.layer),E={point:g,lonLat:m,feature:h?{layer:h,properties:u.feature?.properties??{}}:void 0};e.send({kind:"click",click:E})}),i.on("moveend",()=>e.send({kind:"moveend",camera:T(i)})),e.send({kind:"ready"}),t.waiting.splice(0).forEach(a)};return e.listen(a),e.send({kind:"loaded"}),()=>{t.gone=!0,t.map?.destroy()}};var B=()=>{if(document.querySelector("script[data-isard-workers]"))return;let e=document.createElement("script");e.dataset.isardWorkers="",e.textContent=v,document.head.append(e)},G=(e,r)=>({send:(t)=>{if(t.kind==="moveend"){e.set("camera",t.camera),e.save_changes();return}if(t.kind==="error")console.error(`isard: ${t.message}`);e.send(t)},listen:(t)=>{let n=()=>t({kind:"layers",layers:e.get("layers")}),a=(o)=>{let s=f(typeof o==="string"?o:JSON.stringify(o));if(s?.kind==="call")t(s)};e.on("change:layers",n),e.on("msg:custom",a),r.push(()=>e.off("change:layers",n),()=>e.off("msg:custom",a)),t({kind:"init",options:e.get("options"),layers:e.get("layers")})}}),re={render({model:e,el:r}){B();let t=document.createElement("div");t.style.position="relative",t.style.width="100%",t.style.height=e.get("height"),r.append(t);let n=[],a=C(G(e,n),t);return()=>{n.forEach((o)=>{o()}),a()}}};export{re as default};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: isard-map
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: isard maps in Jupyter: the IGN's WebGL2 map engine (globe, relief, 3D Tiles, vector tiles) as a notebook widget
|
|
5
|
+
Project-URL: Homepage, https://github.com/IGNF/isard
|
|
6
|
+
Project-URL: Documentation, https://github.com/IGNF/isard/tree/main/packages/python
|
|
7
|
+
Author: IGN
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: 3d,gis,ign,jupyter,map,webgl,widget
|
|
10
|
+
Classifier: Framework :: Jupyter
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: anywidget>=0.9
|
|
16
|
+
Requires-Dist: traitlets>=5
|
|
17
|
+
Provides-Extra: geo
|
|
18
|
+
Requires-Dist: geopandas>=0.14; extra == 'geo'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# isard-map
|
|
22
|
+
|
|
23
|
+
[isard](https://github.com/IGNF/isard), the IGN's WebGL2 map engine, in Jupyter: a notebook widget (JupyterLab,
|
|
24
|
+
Notebook, VS Code, Colab) showing the globe or a flat map, the relief, vector tiles, 3D Tiles and the data of your
|
|
25
|
+
frames, its layers set from Python.
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
pip install isard-map # geopandas too: pip install "isard-map[geo]"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import geopandas as gpd
|
|
33
|
+
from isard_map import Map
|
|
34
|
+
|
|
35
|
+
m = Map(center=(2.35, 48.85), zoom=15, pitch=60, basemap="ign-plan")
|
|
36
|
+
m.add_circles(gpd.read_file("places.geojson"), color="#e8590c", radius=6, stroke_color="#fff", stroke_width=2)
|
|
37
|
+
m
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## The map
|
|
41
|
+
|
|
42
|
+
`Map(center, zoom, pitch, bearing, basemap, height="500px", **options)`: `options` are those of isard's
|
|
43
|
+
`GeoMap.create`, in snake_case or camelCase (`terrain={"model": "ign"}`, `projection="globe"`, `quality="medium"`…).
|
|
44
|
+
`m.camera` tells where the map stopped last.
|
|
45
|
+
|
|
46
|
+
## Layers
|
|
47
|
+
|
|
48
|
+
`m.add(type, data=None, id=None, **options)` adds a layer of isard's `map.add` (`"circles"`, `"lines"`, `"fills"`,
|
|
49
|
+
`"extrusions"`, `"tiles3d"`, `"models"`…) and returns its id; the same id replaces it. `data` is GeoJSON: a dict, a
|
|
50
|
+
JSON string, or anything with `__geo_interface__` (a GeoDataFrame, a GeoSeries, a shapely geometry), a frame with a
|
|
51
|
+
CRS brought to WGS 84 first. Shortcuts: `add_circles`, `add_lines`, `add_fills`, `add_extrusions`, `add_tiles3d`.
|
|
52
|
+
`m.set_data(id, data)` sets a layer's data anew (in place on the map), `m.remove(id)` takes it away.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
m.add_extrusions(buildings, height=prop("hauteur", 6), color="#d0d0d0") # from isard_map import prop
|
|
56
|
+
m.add_tiles3d("https://webimaging.lillemetropole.fr/externe/maillage/2020_mel_5cm/tileset.json")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Camera and clicks
|
|
60
|
+
|
|
61
|
+
`m.fly_to(center, zoom=…, pitch=…, bearing=…, duration=…)`, `m.ease_to(…)`, `m.set_camera(…)`.
|
|
62
|
+
`m.on_click(callback)`: each click as `{"point", "lon_lat", "feature"}`, `feature` (its layer's id and properties)
|
|
63
|
+
when one of the layers is under it.
|
|
64
|
+
|
|
65
|
+
## How it works
|
|
66
|
+
|
|
67
|
+
The widget ([anywidget](https://anywidget.dev)) runs the page of `isard-react-native`, the same messages as a React
|
|
68
|
+
Native app's: isard itself is loaded from jsDelivr (the version this package was made for), so the notebook needs the
|
|
69
|
+
network. The layers are sent as JSON when they change; a change of their data alone is set in place.
|
|
70
|
+
|
|
71
|
+
## Develop
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
uv sync && uv run pytest # in packages/python
|
|
75
|
+
bun scripts/build-widget.ts # the widget's script (src/isard_map/static/widget.js), from js/widget.ts
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
MIT licence.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
isard_map/__init__.py,sha256=dGL_-6OgFdtC4oNwKrZ-JHcXbGOw4kVj65LQzx-uXlE,295
|
|
2
|
+
isard_map/_data.py,sha256=6TeSPGcF3YzhPWD5N8jfjrLskRMuQfOL1iDQZpaF3mU,2083
|
|
3
|
+
isard_map/_geoplateforme.py,sha256=yi0VZ4UwLtDYYZ2OO_ql4n7Hk2iQGgq85Ul4O7y1YS0,1184
|
|
4
|
+
isard_map/_map.py,sha256=NXimk2i99ej4L-BGh7MOmTTSJrMDhABUDtFE7NBaNoU,6464
|
|
5
|
+
isard_map/static/widget.js,sha256=CwON3CeK8Jp1KriK3J6qTMAIcaZtWSS83eeP-LEHoQA,6059
|
|
6
|
+
isard_map-0.1.0.dist-info/METADATA,sha256=bjBD_rlEDy1QFxZveYGz-DqjoPBRyMD1YHZ04lNXE_E,3280
|
|
7
|
+
isard_map-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
8
|
+
isard_map-0.1.0.dist-info/RECORD,,
|