Skip to content

Add add_vector_layer function #445

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion examples/Notebook.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,28 @@
"doc.add_geojson_layer(path=\"france_regions.json\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a1e75c62-db0d-49ec-b6ff-2c7c01dc9642",
"metadata": {},
"outputs": [],
"source": [
"url = \"https://github.com/opengeos/datasets/releases/download/world/countries.geojson\"\n",
"doc.add_vector_layer(path=url, name=\"GeoJSON\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "664dca2f-e4ee-4fcb-994c-9940143d4a78",
"metadata": {},
"outputs": [],
"source": [
"url = \"https://github.com/opengeos/datasets/releases/download/world/continents.zip\"\n",
"doc.add_vector_layer(path=url, name=\"Shapefile\")"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down Expand Up @@ -106,7 +128,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.4"
"version": "3.12.8"
}
},
"nbformat": 4,
Expand Down
185 changes: 185 additions & 0 deletions python/jupytergis_lab/jupytergis_lab/notebook/gis_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pydantic import BaseModel
from sidecar import Sidecar
from ypywidgets.comm import CommWidget
import os

from jupytergis_core.schema import (
IGeoJSONSource,
Expand Down Expand Up @@ -313,6 +314,62 @@ def add_geojson_layer(

return self._add_layer(OBJECT_FACTORY.create_layer(layer, self))

def add_vector_layer(
self,
path: Optional[Union[str, Path]] = None,
name: str = "Vector Layer",
type: str = "line",
opacity: float = 1.0,
logical_op: Optional[str] = None,
feature: Optional[str] = None,
operator: Optional[str] = None,
value: Optional[Union[str, int, float]] = None,
color_expr: Optional[str] = None,
**kwargs,
) -> None:
"""
Adds a vector layer to the map, intelligently handling GeoJSON vs vector formats.

Args:
path (Optional[Union[str, Path]]): The path to the vector file.
name (str): The name of the vector layer. Defaults to "Vector Layer".
type (str): The type of the vector layer. Defaults to "line".
opacity (float): The opacity of the vector layer. Defaults to 1.0.
logical_op (Optional[str]): The logical operation to apply. Defaults to None.
feature (Optional[str]): The feature to apply the logical operation on. Defaults to None.
operator (Optional[str]): The operator to use in the logical operation. Defaults to None.
value (Optional[Union[str, int, float]]): The value to use in the logical operation. Defaults to None.
color_expr (Optional[str]): The color expression to use for the vector layer. Defaults to None.
**kwargs: Additional keyword arguments.

Returns:
None
"""

if path and _looks_like_geojson(path):
self.add_geojson_layer(
path=str(path),
name=name,
opacity=opacity,
logical_op=logical_op,
feature=feature,
operator=operator,
value=value,
color_expr=color_expr,
)
else:
geojson = vector_to_geojson(path, **kwargs)
self.add_geojson_layer(
data=geojson,
name=name,
opacity=opacity,
logical_op=logical_op,
feature=feature,
operator=operator,
value=value,
color_expr=color_expr,
)

def add_image_layer(
self,
url: str,
Expand Down Expand Up @@ -900,6 +957,134 @@ def create_source(
return None


def _looks_like_geojson(path: Union[str, Path]) -> bool:
"""
Tries to determine whether the file or URL is a GeoJSON.
- For URLs, looks at file extension or Content-Type.
- For local files, tries to load and parse the content.
"""
path_str = str(path).lower()

if path_str.startswith("http://") or path_str.startswith("https://"):
if path_str.endswith(".geojson") or path_str.endswith(".json"):
return True

try:
head = requests.head(path, timeout=5)
content_type = head.headers.get("Content-Type", "")
return (
"application/geo+json" in content_type
or "application/json" in content_type
)
except requests.RequestException:
return False

elif os.path.isfile(path):
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return isinstance(data, dict) and data.get("type") in {
"FeatureCollection",
"Feature",
}
except Exception:
return False

return False


def vector_to_geojson(
filepath,
out_geojson=None,
bbox=None,
mask=None,
rows=None,
epsg="4326",
encoding="utf-8",
**kwargs,
):
"""Converts any geopandas-supported vector dataset to GeoJSON.

Args:
filepath (str): Either the absolute or relative path to the file or URL
to be opened, or any object with a read() method (such as an open
file or StringIO).
out_geojson (str, optional): The file path to the output GeoJSON.
Defaults to None.
bbox (tuple | GeoDataFrame or GeoSeries | shapely Geometry, optional):
Filter features by given bounding box, GeoSeries, GeoDataFrame or
a shapely geometry. CRS mis-matches are resolved if given a GeoSeries
or GeoDataFrame. Cannot be used with mask. Defaults to None.
mask (dict | GeoDataFrame or GeoSeries | shapely Geometry, optional):
Filter for features that intersect with the given dict-like geojson
geometry, GeoSeries, GeoDataFrame or shapely geometry. CRS mis-matches
are resolved if given a GeoSeries or GeoDataFrame. Cannot be used with
bbox. Defaults to None.
rows (int or slice, optional): Load in specific rows by passing an integer
(first n rows) or a slice() object.. Defaults to None.
epsg (str, optional): The EPSG number to convert to. Defaults to "4326".
encoding (str, optional): The encoding of the input file. Defaults to "utf-8".
kwargs: Additional arguments to pass to geopandas.read_file.


Raises:
ValueError: When the output file path is invalid.

Returns:
dict: A dictionary containing the GeoJSON.
"""

try:
import geopandas as gpd
except ImportError as err:
raise ImportError(
"geopandas is required for this function. Please install it using `pip install geopandas`."
) from err

if not filepath.startswith("http"):
filepath = os.path.abspath(filepath)
if filepath.endswith(".zip"):
filepath = "zip://" + filepath
ext = os.path.splitext(filepath)[1].lower()
if ext == ".kml":
try:
import fiona
except ImportError as err:
raise ImportError(
"fiona is required for this function. Please install it using `pip install fiona`."
) from err

fiona.drvsupport.supported_drivers["KML"] = "rw"
df = gpd.read_file(
filepath,
bbox=bbox,
mask=mask,
rows=rows,
driver="KML",
encoding=encoding,
**kwargs,
)
else:
df = gpd.read_file(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this will download the GeoJSON if I do something like:

url = "https://github.com/opengeos/datasets/releases/download/world/countries.geojson"
doc.add_vector_layer(path=url, name="GeoJSON")

This is unfortunate since it will embed the GeoJSON data into the JGIS file, which may be an unwanted behavior. Keeping the GeoJSON source pointed by URL may be better.

So I guess we'd want to consider two approaches in this function:

  • if it's a URL, we keep the URL as-is and don't embed the data
  • if it's a local file or some in-memory data in Python, we don't have the choice but to embed the data so we keep this logic here

filepath, bbox=bbox, mask=mask, rows=rows, encoding=encoding, **kwargs
)
gdf = df.to_crs(epsg=epsg)

if out_geojson is not None:
if not out_geojson.lower().endswith(".geojson"):
raise ValueError("The output file must have a geojson file extension.")

out_geojson = os.path.abspath(out_geojson)
out_dir = os.path.dirname(out_geojson)
if not os.path.exists(out_dir):
os.makedirs(out_dir)

gdf.to_file(out_geojson, driver="GeoJSON")

else:
return gdf.__geo_interface__


OBJECT_FACTORY = ObjectFactoryManager()

OBJECT_FACTORY.register_factory(LayerType.RasterLayer, IRasterLayer)
Expand Down
Loading