jupytergis-lab 0.1.1__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.
- jupytergis_lab/__init__.py +14 -0
- jupytergis_lab/_version.py +4 -0
- jupytergis_lab/notebook/__init__.py +1 -0
- jupytergis_lab/notebook/gis_document.py +616 -0
- jupytergis_lab/notebook/objects/__init__.py +14 -0
- jupytergis_lab/notebook/objects/_schema/__init__.py +3 -0
- jupytergis_lab/notebook/objects/_schema/geoTiffSource.py +24 -0
- jupytergis_lab/notebook/objects/_schema/geojsonsource.py +511 -0
- jupytergis_lab/notebook/objects/_schema/hillshadeLayer.py +19 -0
- jupytergis_lab/notebook/objects/_schema/imageLayer.py +19 -0
- jupytergis_lab/notebook/objects/_schema/imageSource.py +26 -0
- jupytergis_lab/notebook/objects/_schema/jgis.py +138 -0
- jupytergis_lab/notebook/objects/_schema/rasterDemSource.py +28 -0
- jupytergis_lab/notebook/objects/_schema/rasterlayer.py +19 -0
- jupytergis_lab/notebook/objects/_schema/rastersource.py +33 -0
- jupytergis_lab/notebook/objects/_schema/shapefileSource.py +31 -0
- jupytergis_lab/notebook/objects/_schema/vectorTileLayer.py +29 -0
- jupytergis_lab/notebook/objects/_schema/vectorlayer.py +29 -0
- jupytergis_lab/notebook/objects/_schema/vectortilesource.py +27 -0
- jupytergis_lab/notebook/objects/_schema/videoSource.py +30 -0
- jupytergis_lab/notebook/objects/_schema/webGlLayer.py +22 -0
- jupytergis_lab/notebook/tests/__init__.py +0 -0
- jupytergis_lab/notebook/tests/test_api.py +18 -0
- jupytergis_lab/notebook/utils.py +43 -0
- jupytergis_lab/notebook/y_connector.py +31 -0
- jupytergis_lab-0.1.1.data/data/share/jupyter/labextensions/@jupytergis/jupytergis-lab/install.json +5 -0
- jupytergis_lab-0.1.1.data/data/share/jupyter/labextensions/@jupytergis/jupytergis-lab/package.json +132 -0
- jupytergis_lab-0.1.1.data/data/share/jupyter/labextensions/@jupytergis/jupytergis-lab/static/373.1c3d89f9ed56880711bd.js +1 -0
- jupytergis_lab-0.1.1.data/data/share/jupyter/labextensions/@jupytergis/jupytergis-lab/static/432.28aaec36233a73d1589c.js +1 -0
- jupytergis_lab-0.1.1.data/data/share/jupyter/labextensions/@jupytergis/jupytergis-lab/static/484.7cd11c502baf51c3285e.js +1 -0
- jupytergis_lab-0.1.1.data/data/share/jupyter/labextensions/@jupytergis/jupytergis-lab/static/remoteEntry.15c6baaafaa709cc98c0.js +1 -0
- jupytergis_lab-0.1.1.data/data/share/jupyter/labextensions/@jupytergis/jupytergis-lab/static/style.js +4 -0
- jupytergis_lab-0.1.1.data/data/share/jupyter/labextensions/@jupytergis/jupytergis-lab/static/third-party-licenses.json +40 -0
- jupytergis_lab-0.1.1.dist-info/METADATA +67 -0
- jupytergis_lab-0.1.1.dist-info/RECORD +37 -0
- jupytergis_lab-0.1.1.dist-info/WHEEL +4 -0
- jupytergis_lab-0.1.1.dist-info/licenses/LICENSE +29 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from ._version import __version__
|
|
3
|
+
except ImportError:
|
|
4
|
+
# Fallback when using the package in dev mode without installing
|
|
5
|
+
# in editable mode with pip. It is highly recommended to install
|
|
6
|
+
# the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs
|
|
7
|
+
import warnings
|
|
8
|
+
__version__ = "dev"
|
|
9
|
+
|
|
10
|
+
from .notebook import GISDocument # noqa
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _jupyter_labextension_paths():
|
|
14
|
+
return [{"src": "labextension", "dest": "@jupytergis/jupytergis-lab"}]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .gis_document import GISDocument # noqa
|
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
7
|
+
|
|
8
|
+
from pycrdt import Array, Doc, Map
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from ypywidgets.comm import CommWidget
|
|
11
|
+
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
from .utils import normalize_path, get_source_layer_names
|
|
15
|
+
|
|
16
|
+
from .objects import (
|
|
17
|
+
LayerType,
|
|
18
|
+
SourceType,
|
|
19
|
+
IHillshadeLayer,
|
|
20
|
+
IImageLayer,
|
|
21
|
+
IRasterLayer,
|
|
22
|
+
IRasterSource,
|
|
23
|
+
IVectorTileSource,
|
|
24
|
+
IVectorLayer,
|
|
25
|
+
IVectorTileLayer,
|
|
26
|
+
IGeoJSONSource,
|
|
27
|
+
IImageSource,
|
|
28
|
+
IVideoSource,
|
|
29
|
+
IWebGlLayer
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__file__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GISDocument(CommWidget):
|
|
36
|
+
"""
|
|
37
|
+
Create a new GISDocument object.
|
|
38
|
+
|
|
39
|
+
:param path: the path to the file that you would like to open.
|
|
40
|
+
If not provided, a new empty document will be created.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
path: Optional[str] = None,
|
|
46
|
+
latitude: Optional[float] = None,
|
|
47
|
+
longitude: Optional[float] = None,
|
|
48
|
+
zoom: Optional[float] = None,
|
|
49
|
+
extent: Optional[List[float]] = None,
|
|
50
|
+
bearing: Optional[float] = None,
|
|
51
|
+
pitch: Optional[float] = None,
|
|
52
|
+
projection: Optional[str] = None
|
|
53
|
+
):
|
|
54
|
+
comm_metadata = GISDocument._path_to_comm(path)
|
|
55
|
+
|
|
56
|
+
ydoc = Doc()
|
|
57
|
+
|
|
58
|
+
super().__init__(
|
|
59
|
+
comm_metadata=dict(ymodel_name="@jupytergis:widget", **comm_metadata),
|
|
60
|
+
ydoc=ydoc,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.ydoc["layers"] = self._layers = Map()
|
|
64
|
+
self.ydoc["sources"] = self._sources = Map()
|
|
65
|
+
self.ydoc["options"] = self._options = Map()
|
|
66
|
+
self.ydoc["layerTree"] = self._layerTree = Array()
|
|
67
|
+
|
|
68
|
+
if path is None:
|
|
69
|
+
if latitude is not None:
|
|
70
|
+
self._options["latitude"] = latitude
|
|
71
|
+
if longitude is not None:
|
|
72
|
+
self._options["longitude"] = longitude
|
|
73
|
+
if extent is not None:
|
|
74
|
+
self._options["extent"] = extent
|
|
75
|
+
if zoom is not None:
|
|
76
|
+
self._options["zoom"] = zoom
|
|
77
|
+
if bearing is not None:
|
|
78
|
+
self._options["bearing"] = bearing
|
|
79
|
+
if pitch is not None:
|
|
80
|
+
self._options["pitch"] = pitch
|
|
81
|
+
if projection is not None:
|
|
82
|
+
self._options['projection'] = projection
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def layers(self) -> Dict:
|
|
86
|
+
"""
|
|
87
|
+
Get the layer list
|
|
88
|
+
"""
|
|
89
|
+
return self._layers.to_py()
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def layer_tree(self) -> List[str | Dict]:
|
|
93
|
+
"""
|
|
94
|
+
Get the layer tree
|
|
95
|
+
"""
|
|
96
|
+
return self._layerTree.to_py()
|
|
97
|
+
|
|
98
|
+
def add_raster_layer(
|
|
99
|
+
self,
|
|
100
|
+
url: str,
|
|
101
|
+
name: str = "Raster Layer",
|
|
102
|
+
attribution: str = "",
|
|
103
|
+
opacity: float = 1,
|
|
104
|
+
):
|
|
105
|
+
"""
|
|
106
|
+
Add a Raster Layer to the document.
|
|
107
|
+
|
|
108
|
+
:param name: The name that will be used for the object in the document.
|
|
109
|
+
:param url: The tiles url.
|
|
110
|
+
:param attribution: The attribution.
|
|
111
|
+
:param opacity: The opacity, between 0 and 1.
|
|
112
|
+
"""
|
|
113
|
+
source = {
|
|
114
|
+
"type": SourceType.RasterSource,
|
|
115
|
+
"name": f"{name} Source",
|
|
116
|
+
"parameters": {
|
|
117
|
+
"url": url,
|
|
118
|
+
"minZoom": 0,
|
|
119
|
+
"maxZoom": 24,
|
|
120
|
+
"attribution": attribution,
|
|
121
|
+
"htmlAttribution": attribution,
|
|
122
|
+
"provider": "",
|
|
123
|
+
"bounds": [],
|
|
124
|
+
"urlParameters": {},
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
source_id = self._add_source(OBJECT_FACTORY.create_source(source, self))
|
|
129
|
+
|
|
130
|
+
layer = {
|
|
131
|
+
"type": LayerType.RasterLayer,
|
|
132
|
+
"name": name,
|
|
133
|
+
"visible": True,
|
|
134
|
+
"parameters": {"source": source_id, "opacity": opacity},
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return self._add_layer(OBJECT_FACTORY.create_layer(layer, self))
|
|
138
|
+
|
|
139
|
+
def add_vectortile_layer(
|
|
140
|
+
self,
|
|
141
|
+
url: str,
|
|
142
|
+
name: str = "Vector Tile Layer",
|
|
143
|
+
source_layer: str | None = None,
|
|
144
|
+
attribution: str = "",
|
|
145
|
+
min_zoom: int = 0,
|
|
146
|
+
max_zoom: int = 24,
|
|
147
|
+
type: Literal["circle", "fill", "line"] = "line",
|
|
148
|
+
color: str = "#FF0000",
|
|
149
|
+
opacity: float = 1,
|
|
150
|
+
logical_op:str | None = None,
|
|
151
|
+
feature:str | None = None,
|
|
152
|
+
operator:str | None = None,
|
|
153
|
+
value:Union[str, float, float] | None = None
|
|
154
|
+
):
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
Add a Vector Tile Layer to the document.
|
|
158
|
+
|
|
159
|
+
:param name: The name that will be used for the object in the document.
|
|
160
|
+
:param url: The tiles url.
|
|
161
|
+
:param source_layer: The source layer to use.
|
|
162
|
+
:param attribution: The attribution.
|
|
163
|
+
:param opacity: The opacity, between 0 and 1.
|
|
164
|
+
"""
|
|
165
|
+
source_layers = get_source_layer_names(url)
|
|
166
|
+
if source_layer is None and len(source_layers) == 1:
|
|
167
|
+
source_layer = source_layers[0]
|
|
168
|
+
if source_layer not in source_layers:
|
|
169
|
+
raise ValueError(f'source_layer should be one of {source_layers}')
|
|
170
|
+
|
|
171
|
+
source = {
|
|
172
|
+
"type": SourceType.VectorTileSource,
|
|
173
|
+
"name": f"{name} Source",
|
|
174
|
+
"parameters": {
|
|
175
|
+
"url": url,
|
|
176
|
+
"minZoom": min_zoom,
|
|
177
|
+
"maxZoom": max_zoom,
|
|
178
|
+
"attribution": attribution,
|
|
179
|
+
"htmlAttribution": attribution,
|
|
180
|
+
"provider": "",
|
|
181
|
+
"bounds": [],
|
|
182
|
+
"urlParameters": {},
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
source_id = self._add_source(OBJECT_FACTORY.create_source(source, self))
|
|
187
|
+
|
|
188
|
+
layer = {
|
|
189
|
+
"type": LayerType.VectorTileLayer,
|
|
190
|
+
"name": name,
|
|
191
|
+
"visible": True,
|
|
192
|
+
"parameters": {
|
|
193
|
+
"source": source_id,
|
|
194
|
+
"type": type,
|
|
195
|
+
"opacity": opacity,
|
|
196
|
+
"sourceLayer": source_layer,
|
|
197
|
+
"color": color,
|
|
198
|
+
"opacity": opacity,
|
|
199
|
+
},
|
|
200
|
+
"filters": {
|
|
201
|
+
"appliedFilters": [
|
|
202
|
+
{
|
|
203
|
+
"feature": feature,
|
|
204
|
+
"operator": operator,
|
|
205
|
+
"value": value
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
"logicalOp": logical_op
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return self._add_layer(OBJECT_FACTORY.create_layer(layer, self))
|
|
213
|
+
|
|
214
|
+
def add_geojson_layer(
|
|
215
|
+
self,
|
|
216
|
+
path: str | None = None,
|
|
217
|
+
data: Dict | None = None,
|
|
218
|
+
name: str = "GeoJSON Layer",
|
|
219
|
+
type: "circle" | "fill" | "line" = "line",
|
|
220
|
+
color: str = "#FF0000",
|
|
221
|
+
opacity: float = 1,
|
|
222
|
+
logical_op:str | None = None,
|
|
223
|
+
feature:str | None = None,
|
|
224
|
+
operator:str | None = None,
|
|
225
|
+
value:Union[str, number, float] | None = None
|
|
226
|
+
):
|
|
227
|
+
"""
|
|
228
|
+
Add a GeoJSON Layer to the document.
|
|
229
|
+
|
|
230
|
+
:param name: The name that will be used for the object in the document.
|
|
231
|
+
:param path: The path to the JSON file to embed into the jGIS file.
|
|
232
|
+
:param data: The raw GeoJSON data to embed into the jGIS file.
|
|
233
|
+
:param type: The type of the vector layer to create.
|
|
234
|
+
:param color: The color to apply to features.
|
|
235
|
+
:param opacity: The opacity, between 0 and 1.
|
|
236
|
+
"""
|
|
237
|
+
if path is None and data is None:
|
|
238
|
+
raise ValueError("Cannot create a GeoJSON layer without data")
|
|
239
|
+
|
|
240
|
+
if path is not None and data is not None:
|
|
241
|
+
raise ValueError("Cannot set GeoJSON layer data and path at the same time")
|
|
242
|
+
|
|
243
|
+
if path is not None:
|
|
244
|
+
# We cannot put the path to the file in the model
|
|
245
|
+
# We don't know where the kernel runs/live
|
|
246
|
+
# The front-end would have no way of finding the file reliably
|
|
247
|
+
# TODO Support urls to JSON files, in that case, don't embed the data
|
|
248
|
+
with open(path, "r") as fobj:
|
|
249
|
+
parameters = {"data": json.loads(fobj.read())}
|
|
250
|
+
|
|
251
|
+
if data is not None:
|
|
252
|
+
parameters = {"data": data}
|
|
253
|
+
|
|
254
|
+
source = {
|
|
255
|
+
"type": SourceType.GeoJSONSource,
|
|
256
|
+
"name": f"{name} Source",
|
|
257
|
+
"parameters": parameters,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
source_id = self._add_source(OBJECT_FACTORY.create_source(source, self))
|
|
261
|
+
|
|
262
|
+
layer = {
|
|
263
|
+
"type": LayerType.VectorLayer,
|
|
264
|
+
"name": name,
|
|
265
|
+
"visible": True,
|
|
266
|
+
"parameters": {
|
|
267
|
+
"source": source_id,
|
|
268
|
+
"type": type,
|
|
269
|
+
"color": color,
|
|
270
|
+
"opacity": opacity,
|
|
271
|
+
},
|
|
272
|
+
"filters": {
|
|
273
|
+
"appliedFilters": [
|
|
274
|
+
{
|
|
275
|
+
"feature": feature,
|
|
276
|
+
"operator": operator,
|
|
277
|
+
"value": value
|
|
278
|
+
}
|
|
279
|
+
],
|
|
280
|
+
"logicalOp": logical_op
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return self._add_layer(OBJECT_FACTORY.create_layer(layer, self))
|
|
285
|
+
|
|
286
|
+
def add_image_layer(
|
|
287
|
+
self,
|
|
288
|
+
url: str,
|
|
289
|
+
coordinates: [],
|
|
290
|
+
name: str = "Image Layer",
|
|
291
|
+
opacity: float = 1,
|
|
292
|
+
):
|
|
293
|
+
"""
|
|
294
|
+
Add a Image Layer to the document.
|
|
295
|
+
|
|
296
|
+
:param name: The name that will be used for the object in the document.
|
|
297
|
+
:param url: The image url.
|
|
298
|
+
:param coordinates: Corners of image specified in longitude, latitude pairs.
|
|
299
|
+
:param opacity: The opacity, between 0 and 1.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
if url is None or coordinates is None:
|
|
303
|
+
raise ValueError("URL and Coordinates are required")
|
|
304
|
+
|
|
305
|
+
source = {
|
|
306
|
+
"type": SourceType.ImageSource,
|
|
307
|
+
"name": f"{name} Source",
|
|
308
|
+
"parameters": {
|
|
309
|
+
"url": url,
|
|
310
|
+
"coordinates": coordinates
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
source_id = self._add_source(OBJECT_FACTORY.create_source(source, self))
|
|
315
|
+
|
|
316
|
+
layer = {
|
|
317
|
+
"type": LayerType.RasterLayer,
|
|
318
|
+
"name": name,
|
|
319
|
+
"visible": True,
|
|
320
|
+
"parameters": {"source": source_id, "opacity": opacity},
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return self._add_layer(OBJECT_FACTORY.create_layer(layer, self))
|
|
324
|
+
|
|
325
|
+
def add_video_layer(
|
|
326
|
+
self,
|
|
327
|
+
urls: [],
|
|
328
|
+
name: str = "Image Layer",
|
|
329
|
+
coordinates: [] = [],
|
|
330
|
+
opacity: float = 1,
|
|
331
|
+
):
|
|
332
|
+
"""
|
|
333
|
+
Add a Video Layer to the document.
|
|
334
|
+
|
|
335
|
+
:param name: The name that will be used for the object in the document.
|
|
336
|
+
:param urls: URLs to video content in order of preferred format.
|
|
337
|
+
:param coordinates: Corners of video specified in longitude, latitude pairs.
|
|
338
|
+
:param opacity: The opacity, between 0 and 1.
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
if urls is None or coordinates is None:
|
|
342
|
+
raise ValueError("URLs and Coordinates are required")
|
|
343
|
+
|
|
344
|
+
source = {
|
|
345
|
+
"type": SourceType.VideoSource,
|
|
346
|
+
"name": f"{name} Source",
|
|
347
|
+
"parameters": {
|
|
348
|
+
"urls": urls,
|
|
349
|
+
"coordinates": coordinates
|
|
350
|
+
},
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
source_id = self._add_source(OBJECT_FACTORY.create_source(source, self))
|
|
354
|
+
|
|
355
|
+
layer = {
|
|
356
|
+
"type": LayerType.RasterLayer,
|
|
357
|
+
"name": name,
|
|
358
|
+
"visible": True,
|
|
359
|
+
"parameters": {"source": source_id, "opacity": opacity},
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return self._add_layer(OBJECT_FACTORY.create_layer(layer, self))
|
|
363
|
+
|
|
364
|
+
def add_filter(self, layer_id: str, logical_op:str, feature:str, operator:str, value:Union[str, number, float]):
|
|
365
|
+
"""
|
|
366
|
+
Add a filter to a layer
|
|
367
|
+
|
|
368
|
+
:param str layer_id: The ID of the layer to filter
|
|
369
|
+
:param str logical_op: The logical combination to apply to filters. Must be "any" or "all"
|
|
370
|
+
:param str feature: The feature to be filtered on
|
|
371
|
+
:param str operator: The operator used to compare the feature and value
|
|
372
|
+
:param Union[str, number, float] value: The value to be filtered on
|
|
373
|
+
"""
|
|
374
|
+
layer = self._layers.get(layer_id)
|
|
375
|
+
|
|
376
|
+
# Check if the layer exists
|
|
377
|
+
if layer is None:
|
|
378
|
+
raise ValueError(f"No layer found with ID: {layer_id}")
|
|
379
|
+
|
|
380
|
+
# Initialize filters if it doesn't exist
|
|
381
|
+
if 'filters' not in layer:
|
|
382
|
+
layer['filters'] = {
|
|
383
|
+
'appliedFilters': [
|
|
384
|
+
{
|
|
385
|
+
'feature': feature,
|
|
386
|
+
'operator': operator,
|
|
387
|
+
'value': value
|
|
388
|
+
}
|
|
389
|
+
],
|
|
390
|
+
'logicalOp': logical_op}
|
|
391
|
+
|
|
392
|
+
self._layers[layer_id] = layer
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
# Add new filter
|
|
396
|
+
filters = layer['filters']
|
|
397
|
+
filters['appliedFilters'].append({'feature': feature, 'operator': operator, 'value': value})
|
|
398
|
+
|
|
399
|
+
# update the logical operation
|
|
400
|
+
filters['logicalOp'] = logical_op
|
|
401
|
+
|
|
402
|
+
self._layers[layer_id] = layer
|
|
403
|
+
|
|
404
|
+
def update_filter(self, layer_id: str, logical_op:str, feature:str, operator:str, value:Union[str, number, float]):
|
|
405
|
+
"""
|
|
406
|
+
Update a filter applied to a layer
|
|
407
|
+
|
|
408
|
+
:param str layer_id: The ID of the layer to filter
|
|
409
|
+
:param str logical_op: The logical combination to apply to filters. Must be "any" or "all"
|
|
410
|
+
:param str feature: The feature to update the value for
|
|
411
|
+
:param str operator: The operator used to compare the feature and value
|
|
412
|
+
:param Union[str, number, float] value: The new value to be filtered on
|
|
413
|
+
"""
|
|
414
|
+
layer = self._layers.get(layer_id)
|
|
415
|
+
|
|
416
|
+
# Check if the layer exists
|
|
417
|
+
if layer is None:
|
|
418
|
+
raise ValueError(f"No layer found with ID: {layer_id}")
|
|
419
|
+
|
|
420
|
+
if 'filters' not in layer:
|
|
421
|
+
raise ValueError(f"No filters applied to layer: {layer_id}")
|
|
422
|
+
|
|
423
|
+
# Find the feature within the layer
|
|
424
|
+
feature = next((f for f in layer['filters']['appliedFilters'] if f['feature'] == feature), None)
|
|
425
|
+
if feature is None:
|
|
426
|
+
raise ValueError(f"No feature found with ID: {feature} in layer: {layer_id}")
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
# Update the feature value
|
|
430
|
+
feature['value'] = value
|
|
431
|
+
|
|
432
|
+
# update the logical operation
|
|
433
|
+
layer['filters']['logicalOp'] = logical_op
|
|
434
|
+
|
|
435
|
+
self._layers[layer_id] = layer
|
|
436
|
+
|
|
437
|
+
def clear_filters(self, layer_id: str):
|
|
438
|
+
"""
|
|
439
|
+
Clear filters on a layer
|
|
440
|
+
|
|
441
|
+
:param str layer_id: The ID of the layer to clear filters from
|
|
442
|
+
"""
|
|
443
|
+
layer = self._layers.get(layer_id)
|
|
444
|
+
|
|
445
|
+
# Check if the layer exists
|
|
446
|
+
if layer is None:
|
|
447
|
+
raise ValueError(f"No layer found with ID: {layer_id}")
|
|
448
|
+
|
|
449
|
+
if 'filters' not in layer:
|
|
450
|
+
raise ValueError(f"No filters applied to layer: {layer_id}")
|
|
451
|
+
|
|
452
|
+
layer['filters']['appliedFilters'] = []
|
|
453
|
+
self._layers[layer_id] = layer
|
|
454
|
+
|
|
455
|
+
def _add_source(self, new_object: "JGISObject"):
|
|
456
|
+
_id = str(uuid4())
|
|
457
|
+
obj_dict = json.loads(new_object.json())
|
|
458
|
+
self._sources[_id] = obj_dict
|
|
459
|
+
return _id
|
|
460
|
+
|
|
461
|
+
def _add_layer(self, new_object: "JGISObject"):
|
|
462
|
+
_id = str(uuid4())
|
|
463
|
+
obj_dict = json.loads(new_object.json())
|
|
464
|
+
self._layers[_id] = obj_dict
|
|
465
|
+
self._layerTree.append(_id)
|
|
466
|
+
return _id
|
|
467
|
+
|
|
468
|
+
@classmethod
|
|
469
|
+
def _path_to_comm(cls, filePath: Optional[str]) -> Dict:
|
|
470
|
+
path = None
|
|
471
|
+
format = None
|
|
472
|
+
contentType = None
|
|
473
|
+
|
|
474
|
+
if filePath is not None:
|
|
475
|
+
path = normalize_path(filePath)
|
|
476
|
+
file_name = Path(path).name
|
|
477
|
+
try:
|
|
478
|
+
ext = file_name.split(".")[1].lower()
|
|
479
|
+
except Exception:
|
|
480
|
+
raise ValueError("Can not detect file extension!")
|
|
481
|
+
if ext == "jgis":
|
|
482
|
+
format = "text"
|
|
483
|
+
contentType = "jgis"
|
|
484
|
+
elif ext == "qgz":
|
|
485
|
+
format = "base64"
|
|
486
|
+
contentType = "QGZ"
|
|
487
|
+
elif ext == "qgs":
|
|
488
|
+
format = "base64"
|
|
489
|
+
contentType = "QGS"
|
|
490
|
+
else:
|
|
491
|
+
raise ValueError("File extension is not supported!")
|
|
492
|
+
return dict(
|
|
493
|
+
path=path, format=format, contentType=contentType, createydoc=path is None
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class JGISLayer(BaseModel):
|
|
498
|
+
class Config:
|
|
499
|
+
arbitrary_types_allowed = True
|
|
500
|
+
extra = "allow"
|
|
501
|
+
|
|
502
|
+
name: str
|
|
503
|
+
type: LayerType
|
|
504
|
+
visible: bool
|
|
505
|
+
parameters: Union[
|
|
506
|
+
IRasterLayer,
|
|
507
|
+
IVectorLayer,
|
|
508
|
+
IVectorTileLayer,
|
|
509
|
+
IHillshadeLayer,
|
|
510
|
+
IImageLayer,
|
|
511
|
+
IWebGlLayer
|
|
512
|
+
]
|
|
513
|
+
_parent = Optional[GISDocument]
|
|
514
|
+
|
|
515
|
+
def __init__(__pydantic_self__, parent, **data: Any) -> None: # noqa
|
|
516
|
+
super().__init__(**data)
|
|
517
|
+
__pydantic_self__._parent = parent
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class JGISSource(BaseModel):
|
|
521
|
+
class Config:
|
|
522
|
+
arbitrary_types_allowed = True
|
|
523
|
+
extra = "allow"
|
|
524
|
+
|
|
525
|
+
name: str
|
|
526
|
+
type: SourceType
|
|
527
|
+
parameters: Union[
|
|
528
|
+
IRasterSource,
|
|
529
|
+
IVectorTileSource,
|
|
530
|
+
IGeoJSONSource,
|
|
531
|
+
IImageSource,
|
|
532
|
+
IVideoSource
|
|
533
|
+
]
|
|
534
|
+
_parent = Optional[GISDocument]
|
|
535
|
+
|
|
536
|
+
def __init__(__pydantic_self__, parent, **data: Any) -> None: # noqa
|
|
537
|
+
super().__init__(**data)
|
|
538
|
+
__pydantic_self__._parent = parent
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
class SingletonMeta(type):
|
|
542
|
+
_instances = {}
|
|
543
|
+
|
|
544
|
+
def __call__(cls, *args, **kwargs):
|
|
545
|
+
if cls not in cls._instances:
|
|
546
|
+
instance = super().__call__(*args, **kwargs)
|
|
547
|
+
cls._instances[cls] = instance
|
|
548
|
+
return cls._instances[cls]
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
class ObjectFactoryManager(metaclass=SingletonMeta):
|
|
552
|
+
def __init__(self):
|
|
553
|
+
self._factories: Dict[str, type[BaseModel]] = {}
|
|
554
|
+
|
|
555
|
+
def register_factory(self, shape_type: str, cls: type[BaseModel]) -> None:
|
|
556
|
+
if shape_type not in self._factories:
|
|
557
|
+
self._factories[shape_type] = cls
|
|
558
|
+
|
|
559
|
+
def create_layer(
|
|
560
|
+
self, data: Dict, parent: Optional[GISDocument] = None
|
|
561
|
+
) -> Optional[JGISLayer]:
|
|
562
|
+
object_type = data.get("type", None)
|
|
563
|
+
name: str = data.get("name", None)
|
|
564
|
+
visible: str = data.get("visible", True)
|
|
565
|
+
filters = data.get("filters", None)
|
|
566
|
+
if object_type and object_type in self._factories:
|
|
567
|
+
Model = self._factories[object_type]
|
|
568
|
+
args = {}
|
|
569
|
+
params = data["parameters"]
|
|
570
|
+
for field in Model.__fields__:
|
|
571
|
+
args[field] = params.get(field, None)
|
|
572
|
+
obj_params = Model(**args)
|
|
573
|
+
return JGISLayer(
|
|
574
|
+
parent=parent,
|
|
575
|
+
name=name,
|
|
576
|
+
visible=visible,
|
|
577
|
+
type=object_type,
|
|
578
|
+
parameters=obj_params,
|
|
579
|
+
filters=filters
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
return None
|
|
583
|
+
|
|
584
|
+
def create_source(
|
|
585
|
+
self, data: Dict, parent: Optional[GISDocument] = None
|
|
586
|
+
) -> Optional[JGISSource]:
|
|
587
|
+
object_type = data.get("type", None)
|
|
588
|
+
name: str = data.get("name", None)
|
|
589
|
+
if object_type and object_type in self._factories:
|
|
590
|
+
Model = self._factories[object_type]
|
|
591
|
+
args = {}
|
|
592
|
+
params = data["parameters"]
|
|
593
|
+
for field in Model.__fields__:
|
|
594
|
+
args[field] = params.get(field, None)
|
|
595
|
+
obj_params = Model(**args)
|
|
596
|
+
return JGISSource(
|
|
597
|
+
parent=parent, name=name, type=object_type, parameters=obj_params
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
return None
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
OBJECT_FACTORY = ObjectFactoryManager()
|
|
604
|
+
|
|
605
|
+
OBJECT_FACTORY.register_factory(LayerType.RasterLayer, IRasterLayer)
|
|
606
|
+
OBJECT_FACTORY.register_factory(LayerType.VectorLayer, IVectorLayer)
|
|
607
|
+
OBJECT_FACTORY.register_factory(LayerType.VectorTileLayer, IVectorTileLayer)
|
|
608
|
+
OBJECT_FACTORY.register_factory(LayerType.HillshadeLayer, IHillshadeLayer)
|
|
609
|
+
OBJECT_FACTORY.register_factory(LayerType.WebGlLayer, IWebGlLayer)
|
|
610
|
+
OBJECT_FACTORY.register_factory(LayerType.ImageLayer, IImageLayer)
|
|
611
|
+
|
|
612
|
+
OBJECT_FACTORY.register_factory(SourceType.VectorTileSource, IVectorTileSource)
|
|
613
|
+
OBJECT_FACTORY.register_factory(SourceType.RasterSource, IRasterSource)
|
|
614
|
+
OBJECT_FACTORY.register_factory(SourceType.GeoJSONSource, IGeoJSONSource)
|
|
615
|
+
OBJECT_FACTORY.register_factory(SourceType.ImageSource, IImageSource)
|
|
616
|
+
OBJECT_FACTORY.register_factory(SourceType.VideoSource, IVideoSource)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from ._schema.jgis import * # noqa
|
|
2
|
+
|
|
3
|
+
from ._schema.rasterlayer import IRasterLayer # noqa
|
|
4
|
+
from ._schema.vectorlayer import IVectorLayer # noqa
|
|
5
|
+
from ._schema.vectorTileLayer import IVectorTileLayer # noqa
|
|
6
|
+
from ._schema.hillshadeLayer import IHillshadeLayer # noqa
|
|
7
|
+
from ._schema.imageLayer import IImageLayer # noqa
|
|
8
|
+
from ._schema.webGlLayer import IWebGlLayer # noqa
|
|
9
|
+
|
|
10
|
+
from ._schema.vectortilesource import IVectorTileSource # noqa
|
|
11
|
+
from ._schema.rastersource import IRasterSource # noqa
|
|
12
|
+
from ._schema.geojsonsource import IGeoJSONSource # noqa
|
|
13
|
+
from ._schema.videoSource import IVideoSource # noqa
|
|
14
|
+
from ._schema.imageSource import IImageSource # noqa
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# generated by datamodel-codegen:
|
|
2
|
+
# filename: geoTiffSource.json
|
|
3
|
+
# timestamp: 2024-09-13T09:04:30+00:00
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Url(BaseModel):
|
|
13
|
+
url: Optional[str] = None
|
|
14
|
+
min: Optional[float] = None
|
|
15
|
+
max: Optional[float] = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IGeoTiffSource(BaseModel):
|
|
19
|
+
model_config = ConfigDict(
|
|
20
|
+
extra='forbid',
|
|
21
|
+
)
|
|
22
|
+
urls: List[Url] = Field(..., description='URLs', min_length=1)
|
|
23
|
+
normalize: Optional[bool] = True
|
|
24
|
+
wrapX: Optional[bool] = False
|