gispulse 2.2.0__tar.gz → 2.2.2__tar.gz
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.
- {gispulse-2.2.0/src/gispulse.egg-info → gispulse-2.2.2}/PKG-INFO +1 -1
- {gispulse-2.2.0 → gispulse-2.2.2}/pyproject.toml +1 -1
- gispulse-2.2.2/src/gispulse/capabilities/vector/snap_points.py +259 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/datamart.py +61 -14
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/loader.py +84 -4
- {gispulse-2.2.0 → gispulse-2.2.2/src/gispulse.egg-info}/PKG-INFO +1 -1
- gispulse-2.2.0/src/gispulse/capabilities/vector/snap_points.py +0 -185
- {gispulse-2.2.0 → gispulse-2.2.2}/LICENSE +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/LICENSE-COMMERCIAL.md +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/README.md +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/setup.cfg +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/_compat.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/_pyogrio_warnings.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/apicarto.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/action_dispatcher.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/bus_message.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/circuit_breaker.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/dlq.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/enums.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/event_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/pg_notify.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/pool.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/predicate_evaluator.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/state_store.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/trigger_manager.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/workers/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/workers/base_worker.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/workers/dispatch_worker.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/esb/workers/identify_worker.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/app.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/auth.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/dataset_ops.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/dependencies.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/error_handlers.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/event_hub.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/layer_utils.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/middleware/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/middleware/audit_middleware.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/middleware/metrics_middleware.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/middleware/read_only.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/portal_app.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/rate_limit.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/_upload_utils.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/auth_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/capabilities_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/catalog_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/datasets_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/esb_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/examples_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/filter_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/jobs_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/marketplace_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/ogc_features_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/pipelines_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/portal_datasets_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/portal_features_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/portal_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/portal_sql_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/portal_upload_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/projects_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/relations_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/rules_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/scenarios_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/schedules_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/sessions_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/system_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/templates_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/tiles_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/triggers_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/viewer_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/watchers_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/routers/ws_router.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/schemas.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/http/serve_app.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/mcp/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/mcp/dryrun.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/mcp/server.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/mcp/workdir.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/metrics.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/ogc/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/ogc/auth.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/ogc/loader.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/ogc/wfs_client.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/ogc/wfs_fetcher.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/rest/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/rest/rest_fetcher.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/rest/rest_table_fetcher.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/stac/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/stac/stac_fetcher.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/webhooks/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/adapters/webhooks/http_client.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/app.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/_attribute_sql.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/_geometry_sql.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/base.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/classification.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/clustering.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/density.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/network.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/network_components.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/network_graph.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/network_topology.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/overlay.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/palettes.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/pointcloud.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/polygon_topology.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/postgis_sql.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/raster.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/registry.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/relation_detector.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/schema.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/selection.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/spatial_stats.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/sql_pushdown.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/strategy.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/temporal.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/transforms.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/validation.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/aggregate.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/assign_projection.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/boundary.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/buffer.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/calculate.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/centroid_area.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/chaikin.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/classify.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/clip.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/concave_hull.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/diff.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/dissolve.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/extract_holes.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/extract_ops.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/filter.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/force_geometry_type.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/intersects.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/line_merge.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/line_ops.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/merge.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/nearest.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/offset_curve.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/parts.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/polygonize.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/reproject.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/shape_ops_advanced.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/shape_ops_basic.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/simplify.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/snap_grid.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/spatial_join.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/split_lines.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/union.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/capabilities/vector/voronoi.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/data/basemaps.json +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/data/epsg_common.json +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/models.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/base.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/basemaps.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/flux_ign.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/flux_osm.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/opendata_datagouv.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/opendata_hub.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/opendata_ign.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/projections.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/providers/stac_client.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/registry.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/catalog/source_bridge.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli_mcp.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli_portal.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli_track.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli_triggers.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli_triggers_watch.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/cli_watch.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/config.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/assertions.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/bulk_ingest.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/bulk_runner.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/cache.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/capability_params.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/conditions.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/config.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/crs.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/dag.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/data/worldwide_catalog.yml +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/data_pack_signature.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/dispatcher.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/enums.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/explain.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/base.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/geoparquet_s3.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/http_file.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/ogc_client.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/ogc_features.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/stac.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/fetchers/table_file.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/cache.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/chain.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/expression.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/expression_converter.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/result.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/service.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/filter/types.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/graph.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/io/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/io/geoparquet.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/licence_format.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/logging.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/manifest_v3.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/models.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/network_graph_handle.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/observability.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/pipeline.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/pipeline_schema.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/plugin_contracts.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/plugin_hub.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/plugin_model.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/predicates.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/pricing_catalog.yml +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/registry.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/regulatory_zoning_entry.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/relations.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/session.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/sources.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/spatial_index.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/sql_safety.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/ssrf.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/core/zoning_normalizer.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/diagnostics/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/diagnostics/system.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/dsl/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/dsl/expression_parser.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/dsl/geom_fcts.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/capability_executor.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/graph_executor.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/job_queue.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/job_queue_factory.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/metering.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/pipeline_executor.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/runner.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/scenario_runner.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/scheduler.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/session_manager.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/trigger_bridge.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/orchestration/worker.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/audit.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/auth_models.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/auth_repository.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/bridge.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/change_log_watcher.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/changelog_doctor.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/changelog_reader.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/duckdb_change_detector.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/duckdb_diff_engine.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/duckdb_engine.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/duckdb_engine_adapter.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/duckdb_relations.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/engine.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/engine_factory.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/file_blob_cdc.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/geonode.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/gpkg.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/gpkg_connection.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/gpkg_engine.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/gpkg_repository.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/gpkg_schema.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/gpkg_spatial.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/io.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/licence.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/postgis.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/project_io.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/raster_io.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/repository.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/schedule_repository.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/schema.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/session_provisioner.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/sld_converter.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/source_watcher.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/spatial_queries.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/spatialite_engine.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/spatialite_session.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/sql_dialect.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/sql_guardrails.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/sqlite_repository.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/storage.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/style_converter.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/style_sidecar.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/tier.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/virtual_dataset.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/persistence/watcher_registry.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/api.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/datagouv_refresh.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/mcp_pilot.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/pipeline.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/sources.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/spatial.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/plugins/worldwide_source.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/engine.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/loader.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/operation_executor.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/predicates.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/trigger_evaluator.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/rules/validation.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/__init__.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/config_loader.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/dialect_scanner.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/duckdb_engine.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/engine_inference.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/headless_runtime.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/layer_registry.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/manifest_runner.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/predicate_dsl.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/source_watch.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/sqlite_retry.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/runtime/validation_runner.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse/telemetry.py +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse.egg-info/SOURCES.txt +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse.egg-info/dependency_links.txt +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse.egg-info/entry_points.txt +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse.egg-info/requires.txt +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/src/gispulse.egg-info/top_level.txt +0 -0
- {gispulse-2.2.0 → gispulse-2.2.2}/tests/test_read_only_middleware.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gispulse
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: Modular geospatial engine with business rules, triggers, and dual operating modes (DuckDB/PostGIS)
|
|
5
5
|
Author-email: ImagoData <contact@imagodata.com>
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gispulse"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.2"
|
|
8
8
|
description = "Modular geospatial engine with business rules, triggers, and dual operating modes (DuckDB/PostGIS)"
|
|
9
9
|
license = "AGPL-3.0-or-later"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Snap points to a line network with stable edge ids (capability B).
|
|
2
|
+
|
|
3
|
+
Where ``line_locate_point`` returns only a measure plus a *positional*
|
|
4
|
+
``ref_index``, this capability returns everything needed to attach an event
|
|
5
|
+
to a routable edge:
|
|
6
|
+
|
|
7
|
+
* ``edge_id`` — the matched line's id (from ``ref_id_col``), stable
|
|
8
|
+
across runs (not a row position);
|
|
9
|
+
* ``measure`` — distance along the matched line, in meters, from its
|
|
10
|
+
start (in ``[0, line_length]``); null when unsnapped;
|
|
11
|
+
* ``offset_distance`` — perpendicular distance from the point to its nearest
|
|
12
|
+
line (always reported, snapped or not);
|
|
13
|
+
* ``snapped`` — ``True`` when ``offset_distance <= max_distance_m``
|
|
14
|
+
(or ``max_distance_m is None``), ``False`` otherwise;
|
|
15
|
+
* ``geometry`` — for snapped rows, the **projected** point on the
|
|
16
|
+
nearest line; for unsnapped rows the original point is kept.
|
|
17
|
+
|
|
18
|
+
A point beyond ``max_distance_m`` is reported with ``snapped=False``,
|
|
19
|
+
``edge_id``/``measure`` null and its original geometry — only
|
|
20
|
+
``offset_distance`` records how far its nearest line lies, so the consumer
|
|
21
|
+
can decide what to do. A null/empty input geometry has no nearest line and
|
|
22
|
+
stays ``edge_id=None``, ``snapped=False`` with its geometry untouched.
|
|
23
|
+
|
|
24
|
+
Candidate lines are found through a :class:`~gispulse.core.spatial_index.SpatialIndex`
|
|
25
|
+
(STRtree, no O(n·m) scan); each point projects directly onto its segment.
|
|
26
|
+
Ties — several lines exactly equidistant from one point — break
|
|
27
|
+
deterministically on the smallest ``edge_id`` so the same inputs always
|
|
28
|
+
produce the same outputs.
|
|
29
|
+
|
|
30
|
+
All metric quantities (``measure``, ``offset_distance``, ``max_distance_m``)
|
|
31
|
+
are computed in ``crs_meters`` (a projected CRS); inputs in another CRS are
|
|
32
|
+
reprojected in and the result is reprojected back to the points' original
|
|
33
|
+
CRS. ``crs_meters`` must be a metric/projected CRS — never feed it degrees.
|
|
34
|
+
|
|
35
|
+
This capability is deliberately **generic**: it carries no business notion
|
|
36
|
+
(site, fibre edge, river reach…). Accidents on a road network, meters on a
|
|
37
|
+
utility line and discharges on a watercourse are all the same operation —
|
|
38
|
+
project points onto identified lines — and none is special-cased here.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import geopandas as gpd
|
|
44
|
+
import numpy as np
|
|
45
|
+
|
|
46
|
+
from gispulse.capabilities.base import Capability
|
|
47
|
+
from gispulse.capabilities.registry import register
|
|
48
|
+
from gispulse.core.spatial_index import SpatialIndex
|
|
49
|
+
|
|
50
|
+
# How many nearest lines to inspect per point when breaking ties. Genuine
|
|
51
|
+
# geometric ties (several lines exactly equidistant from one point) are rare;
|
|
52
|
+
# 8 candidates is ample headroom while keeping each lookup O(k·log n) — far
|
|
53
|
+
# cheaper (and bounded) than a radius query whose buffer could match the whole
|
|
54
|
+
# network for a far-off point.
|
|
55
|
+
_TIE_CANDIDATES = 8
|
|
56
|
+
# Distances within this many CRS units (meters) are treated as equal for the
|
|
57
|
+
# purpose of tie-breaking on edge_id.
|
|
58
|
+
_TIE_TOL_M = 1e-9
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _id_sort_key(value: object) -> "tuple[int, object]":
|
|
62
|
+
"""Deterministic, total ordering key for tie-breaking on edge ids.
|
|
63
|
+
|
|
64
|
+
Real ids in a single ``ref_id_col`` are homogeneous (all ints or all
|
|
65
|
+
strings), where this reduces to natural order. The two-level key only
|
|
66
|
+
guards the pathological mixed-type column so ``min`` never raises and the
|
|
67
|
+
outcome stays reproducible.
|
|
68
|
+
"""
|
|
69
|
+
if isinstance(value, bool): # bool is an int subclass — keep it on the str side
|
|
70
|
+
return (1, str(value))
|
|
71
|
+
if isinstance(value, (int, float)):
|
|
72
|
+
return (0, value)
|
|
73
|
+
return (1, str(value))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@register
|
|
77
|
+
class SnapPointsToLinesCapability(Capability):
|
|
78
|
+
"""Projects points onto a line network, tagging each with its edge id."""
|
|
79
|
+
|
|
80
|
+
name = "snap_points_to_lines"
|
|
81
|
+
description = (
|
|
82
|
+
"Snaps each point to the nearest line, adding edge_id (from "
|
|
83
|
+
"ref_id_col), measure, offset_distance and a snapped flag; geometry "
|
|
84
|
+
"becomes the projected point on the nearest line."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def execute(
|
|
88
|
+
self,
|
|
89
|
+
gdf: gpd.GeoDataFrame,
|
|
90
|
+
ref_gdf: gpd.GeoDataFrame | None = None,
|
|
91
|
+
ref_id_col: str | None = None,
|
|
92
|
+
max_distance_m: float | None = None,
|
|
93
|
+
crs_meters: str = "EPSG:3857",
|
|
94
|
+
edge_id_col: str = "edge_id",
|
|
95
|
+
measure_col: str = "measure",
|
|
96
|
+
offset_col: str = "offset_distance",
|
|
97
|
+
snapped_col: str = "snapped",
|
|
98
|
+
**_,
|
|
99
|
+
) -> gpd.GeoDataFrame:
|
|
100
|
+
"""Project each point onto its nearest reference line.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
gdf: Input points (non-points use their centroid). All
|
|
104
|
+
input columns are preserved on the output.
|
|
105
|
+
ref_gdf: Reference line layer (injected via ``ref_layer``).
|
|
106
|
+
ref_id_col: Column on ``ref_gdf`` providing the stable
|
|
107
|
+
``edge_id``. **Required** — its absence raises a
|
|
108
|
+
clear ``ValueError`` rather than silently falling
|
|
109
|
+
back to a positional index.
|
|
110
|
+
max_distance_m: Snap threshold in meters. A point whose nearest
|
|
111
|
+
line is farther than this is left unsnapped:
|
|
112
|
+
``snapped=False``, ``edge_id``/``measure`` null
|
|
113
|
+
and the original geometry kept, with only
|
|
114
|
+
``offset_distance`` reporting the true nearest
|
|
115
|
+
distance. ``None`` snaps every point to its
|
|
116
|
+
nearest line.
|
|
117
|
+
crs_meters: Metric (projected) CRS used for all distances.
|
|
118
|
+
edge_id_col: Output column for the matched edge id.
|
|
119
|
+
measure_col: Output column for the along-line measure (m).
|
|
120
|
+
offset_col: Output column for the perpendicular offset (m).
|
|
121
|
+
snapped_col: Output boolean column.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Copy of ``gdf`` (input columns intact) with the four columns added
|
|
125
|
+
and ``geometry`` replaced by the projected point, in the input CRS.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ValueError: if ``ref_gdf`` is missing/empty, or if ``ref_id_col``
|
|
129
|
+
is not a column of ``ref_gdf``.
|
|
130
|
+
"""
|
|
131
|
+
if ref_gdf is None or ref_gdf.empty:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
"snap_points_to_lines requires a non-empty reference line layer "
|
|
134
|
+
"(ref_gdf). Pass the lines to project the points onto."
|
|
135
|
+
)
|
|
136
|
+
if ref_id_col is None or ref_id_col not in ref_gdf.columns:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
"snap_points_to_lines: ref_id_col="
|
|
139
|
+
f"{ref_id_col!r} is not a column of ref_gdf "
|
|
140
|
+
f"(available columns: {list(ref_gdf.columns)}). "
|
|
141
|
+
"Pass ref_id_col=<the stable id column on your lines> so every "
|
|
142
|
+
"snapped point can carry a durable edge_id."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
original_crs = gdf.crs
|
|
146
|
+
left = gdf.to_crs(crs_meters) if original_crs is not None else gdf.copy()
|
|
147
|
+
right = (
|
|
148
|
+
ref_gdf.to_crs(crs_meters)
|
|
149
|
+
if ref_gdf.crs is not None and str(ref_gdf.crs) != str(crs_meters)
|
|
150
|
+
else ref_gdf.copy()
|
|
151
|
+
)
|
|
152
|
+
right = right.reset_index(drop=True)
|
|
153
|
+
|
|
154
|
+
line_geoms = list(right.geometry)
|
|
155
|
+
ref_ids = list(right[ref_id_col])
|
|
156
|
+
index = SpatialIndex(line_geoms)
|
|
157
|
+
|
|
158
|
+
n = len(left)
|
|
159
|
+
edge_ids: list[object] = [None] * n
|
|
160
|
+
measures = np.full(n, np.nan, dtype=float)
|
|
161
|
+
offsets = np.full(n, np.nan, dtype=float)
|
|
162
|
+
snapped = np.zeros(n, dtype=bool)
|
|
163
|
+
new_geoms = list(left.geometry)
|
|
164
|
+
|
|
165
|
+
for row_i, geom in enumerate(left.geometry):
|
|
166
|
+
if geom is None or geom.is_empty:
|
|
167
|
+
continue
|
|
168
|
+
pt = geom if geom.geom_type == "Point" else geom.centroid
|
|
169
|
+
|
|
170
|
+
pos = self._nearest_line(index, line_geoms, ref_ids, pt)
|
|
171
|
+
if pos is None: # ref layer had no usable geometry
|
|
172
|
+
continue
|
|
173
|
+
line = line_geoms[pos]
|
|
174
|
+
offset = line.distance(pt)
|
|
175
|
+
offsets[row_i] = float(offset) # nearest distance is always reported
|
|
176
|
+
if max_distance_m is not None and offset > max_distance_m:
|
|
177
|
+
# Beyond the threshold: leave edge_id/measure null and the
|
|
178
|
+
# original geometry in place (snapped stays False).
|
|
179
|
+
continue
|
|
180
|
+
measure = line.project(pt)
|
|
181
|
+
measures[row_i] = float(measure)
|
|
182
|
+
edge_ids[row_i] = ref_ids[pos]
|
|
183
|
+
new_geoms[row_i] = line.interpolate(measure)
|
|
184
|
+
snapped[row_i] = True
|
|
185
|
+
|
|
186
|
+
out = left.copy()
|
|
187
|
+
out[edge_id_col] = edge_ids
|
|
188
|
+
out[measure_col] = measures
|
|
189
|
+
out[offset_col] = offsets
|
|
190
|
+
out[snapped_col] = snapped
|
|
191
|
+
out = out.set_geometry(
|
|
192
|
+
gpd.GeoSeries(new_geoms, index=out.index, crs=crs_meters)
|
|
193
|
+
)
|
|
194
|
+
if original_crs is not None:
|
|
195
|
+
out = out.to_crs(original_crs)
|
|
196
|
+
return out.reset_index(drop=True)
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _nearest_line(
|
|
200
|
+
index: SpatialIndex,
|
|
201
|
+
line_geoms: list,
|
|
202
|
+
ref_ids: list,
|
|
203
|
+
pt,
|
|
204
|
+
) -> "int | None":
|
|
205
|
+
"""Position of the nearest line to ``pt``, ties broken by smallest id.
|
|
206
|
+
|
|
207
|
+
Inspects the ``_TIE_CANDIDATES`` nearest lines (O(k·log n)), keeps
|
|
208
|
+
those within ``_TIE_TOL_M`` of the minimum distance, and returns the
|
|
209
|
+
one with the smallest ``edge_id`` — a stable, deterministic choice.
|
|
210
|
+
"""
|
|
211
|
+
near = index.nearest(pt, k=min(_TIE_CANDIDATES, len(line_geoms)))
|
|
212
|
+
if not near:
|
|
213
|
+
return None
|
|
214
|
+
dists = {pos: line_geoms[pos].distance(pt) for pos in near}
|
|
215
|
+
best_dist = min(dists.values())
|
|
216
|
+
tied = [pos for pos, d in dists.items() if d <= best_dist + _TIE_TOL_M]
|
|
217
|
+
return min(tied, key=lambda pos: _id_sort_key(ref_ids[pos]))
|
|
218
|
+
|
|
219
|
+
def get_schema(self) -> dict:
|
|
220
|
+
return {
|
|
221
|
+
"type": "object",
|
|
222
|
+
"properties": {
|
|
223
|
+
"ref_layer": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"description": "Reference line layer to snap onto.",
|
|
226
|
+
},
|
|
227
|
+
"ref_id_col": {
|
|
228
|
+
"type": "string",
|
|
229
|
+
"description": (
|
|
230
|
+
"Required. Column on the lines providing the stable "
|
|
231
|
+
"edge_id; its absence raises a ValueError."
|
|
232
|
+
),
|
|
233
|
+
},
|
|
234
|
+
"max_distance_m": {
|
|
235
|
+
"type": ["number", "null"],
|
|
236
|
+
"description": (
|
|
237
|
+
"Snap threshold (m). Beyond it, snapped=False with "
|
|
238
|
+
"edge_id/measure null and the original geometry kept "
|
|
239
|
+
"(only offset_distance reported). None snaps every "
|
|
240
|
+
"point to its nearest line."
|
|
241
|
+
),
|
|
242
|
+
},
|
|
243
|
+
"crs_meters": {"type": "string", "default": "EPSG:3857"},
|
|
244
|
+
"edge_id_col": {"type": "string", "default": "edge_id"},
|
|
245
|
+
"measure_col": {"type": "string", "default": "measure"},
|
|
246
|
+
"offset_col": {"type": "string", "default": "offset_distance"},
|
|
247
|
+
"snapped_col": {"type": "string", "default": "snapped"},
|
|
248
|
+
},
|
|
249
|
+
# ``ref_id_col`` is genuinely required and is not plumbing, so it
|
|
250
|
+
# can validate early. ``ref_layer`` must NOT appear in ``required``:
|
|
251
|
+
# it is pipeline plumbing (resolved to ``ref_gdf`` and stripped
|
|
252
|
+
# before schema validation), so requiring it would fail every v2
|
|
253
|
+
# call before the capability runs. The runtime still raises a clear
|
|
254
|
+
# ValueError when ``ref_gdf`` itself is missing.
|
|
255
|
+
"required": ["ref_id_col"],
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
__all__ = ["SnapPointsToLinesCapability"]
|
|
@@ -19,8 +19,14 @@ from the ``GISPULSE_DATAMARTS`` environment variable (a JSON object), e.g.::
|
|
|
19
19
|
|
|
20
20
|
GISPULSE_DATAMARTS='{
|
|
21
21
|
"referentiel": {"location": "s3://bucket/marts/referentiel"},
|
|
22
|
-
"local": {"location": "/data/marts/local", "table_pattern": "{table}.parquet"}
|
|
22
|
+
"local": {"location": "/data/marts/local", "table_pattern": "{table}.parquet"},
|
|
23
|
+
"warehouse": {"location": "/data/marts/warehouse.duckdb", "kind": "duckdb"}
|
|
23
24
|
}'
|
|
25
|
+
|
|
26
|
+
A ``kind: "duckdb"`` mart points :attr:`Datamart.location` at a single
|
|
27
|
+
DuckDB database file; ``datamart://warehouse/<table>`` then reads the table
|
|
28
|
+
of that name from inside the file (attached read-only, bbox push-down still
|
|
29
|
+
applies via an ``ST_Intersects`` predicate on a ``geom`` column).
|
|
24
30
|
"""
|
|
25
31
|
|
|
26
32
|
from __future__ import annotations
|
|
@@ -42,17 +48,33 @@ DATAMARTS_ENV = "GISPULSE_DATAMARTS"
|
|
|
42
48
|
DATAMART_SCHEME = "datamart"
|
|
43
49
|
|
|
44
50
|
|
|
51
|
+
#: Supported datamart backends.
|
|
52
|
+
DATAMART_KINDS = ("parquet", "duckdb")
|
|
53
|
+
|
|
54
|
+
|
|
45
55
|
@dataclass(frozen=True)
|
|
46
56
|
class Datamart:
|
|
47
57
|
"""Declaration of one curated table store.
|
|
48
58
|
|
|
59
|
+
Two backends are supported, selected by :attr:`kind`:
|
|
60
|
+
|
|
61
|
+
* ``"parquet"`` (default) — *location* is a directory / ``s3://`` /
|
|
62
|
+
``https://`` prefix holding **one file per table**; the file is
|
|
63
|
+
addressed by joining *location* with *table_pattern* and read as an
|
|
64
|
+
ordinary source (DuckDB-scannable, bbox push-down applies).
|
|
65
|
+
* ``"duckdb"`` — *location* is the path to a **single DuckDB database
|
|
66
|
+
file** (``.duckdb``); a table is a relation *inside* that file, read by
|
|
67
|
+
attaching the database read-only and selecting the table.
|
|
68
|
+
|
|
49
69
|
Args:
|
|
50
70
|
name: Logical mart name used in ``datamart://<name>/…``.
|
|
51
|
-
location: Base location
|
|
52
|
-
|
|
53
|
-
``
|
|
54
|
-
table_pattern: Filename pattern for a table, ``{table}``
|
|
55
|
-
table name. Defaults to ``"{table}.parquet"``.
|
|
71
|
+
location: Base location: a directory / ``s3://`` / ``https://``
|
|
72
|
+
prefix (``parquet``), or the path to a ``.duckdb``
|
|
73
|
+
file (``duckdb``).
|
|
74
|
+
table_pattern: Filename pattern for a ``parquet`` table, ``{table}``
|
|
75
|
+
being the table name. Defaults to ``"{table}.parquet"``.
|
|
76
|
+
Ignored for ``duckdb`` marts.
|
|
77
|
+
kind: Backend, ``"parquet"`` (default) or ``"duckdb"``.
|
|
56
78
|
crs: Optional CRS to force on tables that declare none.
|
|
57
79
|
metadata: Free-form descriptive metadata.
|
|
58
80
|
"""
|
|
@@ -60,13 +82,31 @@ class Datamart:
|
|
|
60
82
|
name: str
|
|
61
83
|
location: str
|
|
62
84
|
table_pattern: str = "{table}.parquet"
|
|
85
|
+
kind: str = "parquet"
|
|
63
86
|
crs: str | None = None
|
|
64
87
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
65
88
|
|
|
89
|
+
def __post_init__(self) -> None:
|
|
90
|
+
if self.kind not in DATAMART_KINDS:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"datamart '{self.name}': unknown kind {self.kind!r} "
|
|
93
|
+
f"(expected one of {DATAMART_KINDS})"
|
|
94
|
+
)
|
|
95
|
+
|
|
66
96
|
def uri_for(self, table: str) -> str:
|
|
67
|
-
"""Return the concrete source URI for *table*
|
|
97
|
+
"""Return the concrete source URI for *table* (``parquet`` marts).
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
ValueError: for a ``duckdb`` mart — a table there is not a file
|
|
101
|
+
URI; resolve it via the loader's DuckDB-table path instead.
|
|
102
|
+
"""
|
|
68
103
|
if not table:
|
|
69
104
|
raise ValueError(f"datamart '{self.name}': empty table name")
|
|
105
|
+
if self.kind != "parquet":
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"datamart '{self.name}': uri_for is only valid for parquet "
|
|
108
|
+
f"marts, not kind={self.kind!r}"
|
|
109
|
+
)
|
|
70
110
|
filename = self.table_pattern.format(table=table)
|
|
71
111
|
base = self.location.rstrip("/")
|
|
72
112
|
return f"{base}/{filename}"
|
|
@@ -141,13 +181,19 @@ class DatamartRegistry:
|
|
|
141
181
|
if not isinstance(spec, dict) or "location" not in spec:
|
|
142
182
|
log.warning("datamart_env_bad_decl", name=name)
|
|
143
183
|
continue
|
|
144
|
-
|
|
145
|
-
name=
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
184
|
+
try:
|
|
185
|
+
self._marts[name] = Datamart(
|
|
186
|
+
name=name,
|
|
187
|
+
location=str(spec["location"]),
|
|
188
|
+
table_pattern=str(
|
|
189
|
+
spec.get("table_pattern", "{table}.parquet")
|
|
190
|
+
),
|
|
191
|
+
kind=str(spec.get("kind", "parquet")),
|
|
192
|
+
crs=spec.get("crs"),
|
|
193
|
+
metadata=dict(spec.get("metadata") or {}),
|
|
194
|
+
)
|
|
195
|
+
except ValueError as exc:
|
|
196
|
+
log.warning("datamart_env_bad_decl", name=name, error=str(exc))
|
|
151
197
|
|
|
152
198
|
|
|
153
199
|
def parse_datamart_uri(uri: str) -> tuple[str, str]:
|
|
@@ -177,6 +223,7 @@ DATAMARTS = DatamartRegistry()
|
|
|
177
223
|
__all__ = [
|
|
178
224
|
"DATAMARTS",
|
|
179
225
|
"DATAMARTS_ENV",
|
|
226
|
+
"DATAMART_KINDS",
|
|
180
227
|
"DATAMART_SCHEME",
|
|
181
228
|
"Datamart",
|
|
182
229
|
"DatamartRegistry",
|
|
@@ -119,12 +119,30 @@ class LazyDataset:
|
|
|
119
119
|
return _scan_to_gdf(self.scan_sql, crs=self.crs, bbox=bbox)
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class _DuckDBTableRef:
|
|
124
|
+
"""Reference to a table living inside a single DuckDB database file.
|
|
125
|
+
|
|
126
|
+
Produced by :func:`resolve_source` for a ``duckdb``-kind datamart
|
|
127
|
+
(``datamart://<mart>/<table>`` where the mart's location is a
|
|
128
|
+
``.duckdb`` file). Unlike a Parquet table it is not a file URI the
|
|
129
|
+
loader can hand to ``read_vector`` / a remote-table scan, so the loader
|
|
130
|
+
reads it via :func:`_duckdb_table_to_gdf` (attach read-only, select).
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
db_path: str
|
|
134
|
+
table: str
|
|
135
|
+
crs: str | None = None
|
|
136
|
+
|
|
137
|
+
|
|
122
138
|
# ---------------------------------------------------------------------------
|
|
123
139
|
# Source resolution
|
|
124
140
|
# ---------------------------------------------------------------------------
|
|
125
141
|
|
|
126
142
|
|
|
127
|
-
def resolve_source(
|
|
143
|
+
def resolve_source(
|
|
144
|
+
source: "str | Path | AccessSpec | dict[str, Any]",
|
|
145
|
+
) -> "Path | AccessSpec | _DuckDBTableRef":
|
|
128
146
|
"""Resolve a user-supplied source into a local path or an access spec.
|
|
129
147
|
|
|
130
148
|
Accepted forms:
|
|
@@ -162,12 +180,19 @@ def resolve_source(source: "str | Path | AccessSpec | dict[str, Any]") -> "Path
|
|
|
162
180
|
scheme = head.lower()
|
|
163
181
|
|
|
164
182
|
# Datamart indirection: resolve datamart://<mart>/<table> to its
|
|
165
|
-
# concrete source
|
|
183
|
+
# concrete source. A "parquet" mart yields a file URI resolved
|
|
184
|
+
# recursively; a "duckdb" mart yields a table reference inside a
|
|
185
|
+
# database file, read via _duckdb_table_to_gdf.
|
|
166
186
|
if scheme == DATAMART_SCHEME:
|
|
167
187
|
from gispulse.persistence.datamart import DATAMARTS, parse_datamart_uri
|
|
168
188
|
|
|
169
|
-
|
|
170
|
-
|
|
189
|
+
mart_name, table = parse_datamart_uri(raw)
|
|
190
|
+
mart = DATAMARTS.get(mart_name)
|
|
191
|
+
if mart.kind == "duckdb":
|
|
192
|
+
if not table:
|
|
193
|
+
raise ValueError(f"datamart '{mart_name}': empty table name")
|
|
194
|
+
return _DuckDBTableRef(db_path=mart.location, table=table, crs=mart.crs)
|
|
195
|
+
return resolve_source(mart.uri_for(table))
|
|
171
196
|
|
|
172
197
|
# GeoNode: geonode://<instance>/<dataset> reads via the instance's
|
|
173
198
|
# GeoServer WFS endpoint (reuses the OGC fetcher).
|
|
@@ -330,6 +355,52 @@ def _scan_to_gdf(
|
|
|
330
355
|
return gdf
|
|
331
356
|
|
|
332
357
|
|
|
358
|
+
def _duckdb_table_to_gdf(
|
|
359
|
+
db_path: str, table: str, *, crs: str | None, bbox: BBox | None
|
|
360
|
+
) -> gpd.GeoDataFrame:
|
|
361
|
+
"""Read a table from a DuckDB database file into a GeoDataFrame.
|
|
362
|
+
|
|
363
|
+
Attaches *db_path* **read-only** (so concurrent readers don't contend on
|
|
364
|
+
a write lock) onto an ephemeral in-memory session, then selects *table*.
|
|
365
|
+
When *bbox* is given an ``ST_Intersects`` envelope predicate is pushed
|
|
366
|
+
into the query against a ``geom`` column (matching the convention of
|
|
367
|
+
:func:`_scan_to_gdf`).
|
|
368
|
+
"""
|
|
369
|
+
from gispulse.persistence.duckdb_engine import DuckDBSession
|
|
370
|
+
|
|
371
|
+
# ATTACH cannot be wrapped in the SELECT sub-query DuckDBSession.sql
|
|
372
|
+
# builds, so run it separately on the connection, then SELECT the table
|
|
373
|
+
# — that SELECT *is* wrappable, so geometry/CRS decoding still works.
|
|
374
|
+
db_lit = db_path.replace("'", "''")
|
|
375
|
+
rel = f"_mart.{_quote_ident(table)}"
|
|
376
|
+
query = f"SELECT * FROM {rel}"
|
|
377
|
+
if bbox is not None:
|
|
378
|
+
minx, miny, maxx, maxy = bbox
|
|
379
|
+
query = (
|
|
380
|
+
f"SELECT * FROM {rel} "
|
|
381
|
+
f"WHERE ST_Intersects(geom, ST_MakeEnvelope("
|
|
382
|
+
f"{minx}, {miny}, {maxx}, {maxy}))"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
session = DuckDBSession()
|
|
386
|
+
session.open()
|
|
387
|
+
try:
|
|
388
|
+
session.conn.execute(f"ATTACH '{db_lit}' AS _mart (READ_ONLY)")
|
|
389
|
+
gdf = session.sql(query)
|
|
390
|
+
finally:
|
|
391
|
+
session.close()
|
|
392
|
+
|
|
393
|
+
if crs and gdf.crs is None:
|
|
394
|
+
gdf = gdf.set_crs(crs)
|
|
395
|
+
return gdf
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _quote_ident(identifier: str) -> str:
|
|
399
|
+
"""Quote a SQL identifier (double-quote, doubling embedded quotes)."""
|
|
400
|
+
escaped = identifier.replace('"', '""')
|
|
401
|
+
return f'"{escaped}"'
|
|
402
|
+
|
|
403
|
+
|
|
333
404
|
# ---------------------------------------------------------------------------
|
|
334
405
|
# Public entry point
|
|
335
406
|
# ---------------------------------------------------------------------------
|
|
@@ -380,6 +451,15 @@ def load(
|
|
|
380
451
|
|
|
381
452
|
resolved = resolve_source(source)
|
|
382
453
|
|
|
454
|
+
# --- DuckDB-file datamart table ---------------------------------------
|
|
455
|
+
if isinstance(resolved, _DuckDBTableRef):
|
|
456
|
+
if lazy:
|
|
457
|
+
log.debug("loader_lazy_ignored_for_duckdb_table", table=resolved.table)
|
|
458
|
+
gdf = _duckdb_table_to_gdf(
|
|
459
|
+
resolved.db_path, resolved.table, crs=crs or resolved.crs, bbox=bbox
|
|
460
|
+
)
|
|
461
|
+
return gdf
|
|
462
|
+
|
|
383
463
|
# --- Local file path ---------------------------------------------------
|
|
384
464
|
if isinstance(resolved, Path):
|
|
385
465
|
if lazy:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gispulse
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: Modular geospatial engine with business rules, triggers, and dual operating modes (DuckDB/PostGIS)
|
|
5
5
|
Author-email: ImagoData <contact@imagodata.com>
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|