From b1b5f97a3de3bde69010baf7b9ebb8390f874c28 Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Sat, 20 Jul 2024 23:34:06 +0200 Subject: [PATCH] Add simple features source --- _experimental/add_multiple_layers_at_once.py | 24 ++++++ maplibre/map.py | 10 ++- maplibre/sources.py | 84 +++++++++++++++----- 3 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 _experimental/add_multiple_layers_at_once.py diff --git a/_experimental/add_multiple_layers_at_once.py b/_experimental/add_multiple_layers_at_once.py new file mode 100644 index 00000000..8e5b6ca0 --- /dev/null +++ b/_experimental/add_multiple_layers_at_once.py @@ -0,0 +1,24 @@ +from maplibre import Layer, LayerType, Map, MapOptions +from maplibre.sources import SimpleFeatures + +path = "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_1_states_provinces_shp.geojson" + +simple_features = SimpleFeatures(path, source_id="states") +# sources = {"states": simple_features.to_source()} +sources = simple_features.to_sources_dict() +layers = [ + Layer( + type=LayerType.FILL, + paint={"fill-color": "green"}, + source=simple_features.source_id, + ), + Layer( + type=LayerType.LINE, + paint={"line-color": "blue"}, + source=simple_features.source_id, + ), +] + +m = Map(MapOptions(bounds=simple_features.bounds)) +m.add_layers(layers, sources) +m.save("/tmp/py-maplibre-express.html") diff --git a/maplibre/map.py b/maplibre/map.py index 0ddc15a6..52c331d3 100644 --- a/maplibre/map.py +++ b/maplibre/map.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -import os.path import webbrowser from typing import Union @@ -9,7 +8,7 @@ from pydantic import ConfigDict, Field, field_validator from ._templates import html_template, js_template -from ._utils import BaseModel, get_output_dir, get_temp_filename, read_internal_file +from ._utils import BaseModel, get_temp_filename, read_internal_file from .basemaps import Carto, construct_carto_basemap_url from .controls import Control, ControlPosition, Marker from .layer import Layer @@ -160,6 +159,13 @@ def add_layer(self, layer: [Layer | dict], before_id: str = None) -> None: self.add_call("addLayer", layer, before_id) + def add_layers(self, layers: list, sources: dict = None): + for source_id, source in sources.items(): + self.add_source(source_id, source) + + for layer in layers: + self.add_layer(layer) + def add_marker(self, marker: Marker) -> None: """Add a marker to the map diff --git a/maplibre/sources.py b/maplibre/sources.py index 1fc78468..28675363 100644 --- a/maplibre/sources.py +++ b/maplibre/sources.py @@ -2,10 +2,19 @@ from enum import Enum from typing import Optional, Union +from uuid import uuid4 -from pydantic import ConfigDict, Field, computed_field +from pydantic import Field, computed_field, field_validator from ._utils import BaseModel +from .utils import geopandas_to_geojson + +try: + from geopandas import GeoDataFrame, read_file +except ImportError: + GeoDataFrame, read_file = None, None + +CRS = "EPSG:4326" class SourceType(Enum): @@ -21,12 +30,6 @@ class SourceType(Enum): class Source(BaseModel): pass - # model_config = ConfigDict(validate_assignment=True, extra="forbid") - - """ - def model_dump(self): - return super().model_dump(exclude_none=True, by_alias=True) - """ class GeoJSONSource(Source): @@ -40,19 +43,33 @@ class GeoJSONSource(Source): """ data: Union[str, dict] - attribution: str = None - buffer: int = None - cluster: bool = None - cluster_max_zoom: int = Field(None, serialization_alias="clusterMaxZoom") - cluster_min_points: int = Field(None, serialization_alias="clusterMinPoints") - cluster_properties: dict = Field(None, serialization_alias="clusterProperties") - cluster_radius: int = Field(None, serialization_alias="clusterRadius") - filter: list = None - generate_id: bool = Field(None, serialization_alias="generateId") - line_metrics: bool = Field(None, serialization_alias="lineMetrics") - maxzoom: int = None - promote_id: Union[str, dict] = Field(None, serialization_alias="promoteId") - tolerance: float = None + attribution: Optional[str] = None + buffer: Optional[int] = None + cluster: Optional[bool] = None + cluster_max_zoom: Optional[int] = Field(None, serialization_alias="clusterMaxZoom") + cluster_min_points: Optional[int] = Field( + None, serialization_alias="clusterMinPoints" + ) + cluster_properties: Optional[dict] = Field( + None, serialization_alias="clusterProperties" + ) + cluster_radius: Optional[int] = Field(None, serialization_alias="clusterRadius") + filter: Optional[list] = None + generate_id: Optional[bool] = Field(None, serialization_alias="generateId") + line_metrics: Optional[bool] = Field(None, serialization_alias="lineMetrics") + min_zoom: Optional[int] = Field(None, serialization_alias="minzoom") + max_zoom: Optional[int] = Field(None, serialization_alias="maxzoom") + promote_id: Union[str, dict, None] = Field(None, serialization_alias="promoteId") + tolerance: Optional[float] = None + + """ + @field_validator("data") + def validate_data(cls, v): + if isinstance(v, GeoDataFrame): + return geopandas_to_geojson(v) + + return v + """ @computed_field @property @@ -125,3 +142,30 @@ class VectorTileSource(Source): @property def type(self) -> str: return SourceType.VECTOR.value + + +class SimpleFeatures(object): + def __init__(self, data: GeoDataFrame | str, source_id: str = None): + if isinstance(data, str): + data = read_file(data) + + if str(data.crs) != CRS: + data = data.to_crs(CRS) + + self._data = data + self._source_id = source_id or str(uuid4()) + + @property + def bounds(self) -> tuple: + return self._data.total_bounds + + @property + def source_id(self) -> str: + return self._source_id + + def to_source(self, **kwargs) -> GeoJSONSource: + kwargs["data"] = geopandas_to_geojson(self._data) + return GeoJSONSource(**kwargs) + + def to_sources_dict(self, **kwargs) -> dict: + return {self.source_id: self.to_source(**kwargs)}