This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/geo-service/services/osm_extractor.py
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

218 lines
6.7 KiB
Python

"""
OSM Extractor Service
Extracts OpenStreetMap features from PostGIS or vector tiles
"""
import os
import json
from typing import Optional
import structlog
from shapely.geometry import shape, mapping
from config import settings
logger = structlog.get_logger(__name__)
class OSMExtractorService:
"""
Service for extracting OSM features from a geographic area.
Can extract from:
- PostGIS database (imported OSM data)
- PMTiles archive
"""
def __init__(self):
self.database_url = settings.database_url
async def extract_features(self, polygon: dict) -> dict:
"""
Extract OSM features within a polygon.
Returns a GeoJSON FeatureCollection with categorized features.
"""
geom = shape(polygon)
bounds = geom.bounds # (minx, miny, maxx, maxy)
# Feature collection structure
features = {
"type": "FeatureCollection",
"features": [],
"metadata": {
"source": "OpenStreetMap",
"license": "ODbL",
"bounds": {
"west": bounds[0],
"south": bounds[1],
"east": bounds[2],
"north": bounds[3],
},
},
}
# Try to extract from database
try:
db_features = await self._extract_from_database(geom)
if db_features:
features["features"].extend(db_features)
return features
except Exception as e:
logger.warning("Database extraction failed", error=str(e))
# Fall back to mock data for development
features["features"] = self._generate_mock_features(geom)
features["metadata"]["source"] = "Mock Data (OSM data not imported)"
return features
async def _extract_from_database(self, geom) -> list:
"""Extract features from PostGIS database."""
# This would use asyncpg to query the database
# For now, return empty list to trigger mock data
# Example query structure (not executed):
# SELECT ST_AsGeoJSON(way), name, building, highway, natural, waterway
# FROM planet_osm_polygon
# WHERE ST_Intersects(way, ST_GeomFromGeoJSON($1))
return []
def _generate_mock_features(self, geom) -> list:
"""Generate mock OSM features for development."""
from shapely.geometry import Point, LineString, Polygon as ShapelyPolygon
import random
bounds = geom.bounds
features = []
# Generate some mock buildings
for i in range(5):
x = random.uniform(bounds[0], bounds[2])
y = random.uniform(bounds[1], bounds[3])
# Small building polygon
size = 0.0002 # ~20m
building = ShapelyPolygon([
(x, y),
(x + size, y),
(x + size, y + size),
(x, y + size),
(x, y),
])
if geom.contains(building.centroid):
features.append({
"type": "Feature",
"geometry": mapping(building),
"properties": {
"category": "building",
"building": "yes",
"name": f"Gebäude {i + 1}",
},
})
# Generate some mock roads
for i in range(3):
x1 = random.uniform(bounds[0], bounds[2])
y1 = random.uniform(bounds[1], bounds[3])
x2 = random.uniform(bounds[0], bounds[2])
y2 = random.uniform(bounds[1], bounds[3])
road = LineString([(x1, y1), (x2, y2)])
features.append({
"type": "Feature",
"geometry": mapping(road),
"properties": {
"category": "road",
"highway": random.choice(["primary", "secondary", "residential"]),
"name": f"Straße {i + 1}",
},
})
# Generate mock water feature
cx = (bounds[0] + bounds[2]) / 2
cy = (bounds[1] + bounds[3]) / 2
size = min(bounds[2] - bounds[0], bounds[3] - bounds[1]) * 0.2
water = ShapelyPolygon([
(cx - size, cy - size / 2),
(cx + size, cy - size / 2),
(cx + size, cy + size / 2),
(cx - size, cy + size / 2),
(cx - size, cy - size / 2),
])
if geom.intersects(water):
features.append({
"type": "Feature",
"geometry": mapping(water.intersection(geom)),
"properties": {
"category": "water",
"natural": "water",
"name": "See",
},
})
# Generate mock forest
forest_size = size * 1.5
forest = ShapelyPolygon([
(bounds[0], bounds[1]),
(bounds[0] + forest_size, bounds[1]),
(bounds[0] + forest_size, bounds[1] + forest_size),
(bounds[0], bounds[1] + forest_size),
(bounds[0], bounds[1]),
])
if geom.intersects(forest):
features.append({
"type": "Feature",
"geometry": mapping(forest.intersection(geom)),
"properties": {
"category": "vegetation",
"landuse": "forest",
"name": "Wald",
},
})
return features
async def get_feature_statistics(self, polygon: dict) -> dict:
"""Get statistics about features in an area."""
features = await self.extract_features(polygon)
categories = {}
for feature in features.get("features", []):
category = feature.get("properties", {}).get("category", "other")
categories[category] = categories.get(category, 0) + 1
return {
"total_features": len(features.get("features", [])),
"by_category": categories,
}
async def search_features(
self,
polygon: dict,
category: str,
name_filter: Optional[str] = None,
) -> list:
"""Search for specific features within an area."""
all_features = await self.extract_features(polygon)
filtered = []
for feature in all_features.get("features", []):
props = feature.get("properties", {})
if props.get("category") != category:
continue
if name_filter:
name = props.get("name", "")
if name_filter.lower() not in name.lower():
continue
filtered.append(feature)
return filtered