GENISIS / map_handler.py
ChiragHariprasad
Deploy latest GENISIS
bcdb2af
Raw
History Blame Contribute Delete
9.41 kB
"""
Map Handler for Geographic Drawing
Supports drawing custom shapes on interactive maps
"""
import folium
from folium.plugins import Draw
import streamlit as st
from streamlit_folium import st_folium
from typing import List, Tuple, Dict, Any, Optional
import json
import numpy as np
from shapely.geometry import Polygon, shape
from shapely.ops import transform
import pyproj
from functools import partial
class MapDrawingHandler:
"""Handle map-based shape drawing and coordinate conversion"""
# Default center (Delhi, India for Indian Building Regulations context)
DEFAULT_CENTER = [28.6139, 77.2090]
DEFAULT_ZOOM = 15
def __init__(self, center: List[float] = None, zoom: int = None):
self.center = center or self.DEFAULT_CENTER
self.zoom = zoom or self.DEFAULT_ZOOM
self.drawn_shapes = []
def create_drawing_map(self,
existing_shape: List[Tuple[float, float]] = None,
height: int = 500) -> folium.Map:
"""Create a Folium map with drawing controls"""
m = folium.Map(
location=self.center,
zoom_start=self.zoom,
tiles='OpenStreetMap'
)
# Add alternative tile layers
folium.TileLayer('cartodbpositron', name='Light Map').add_to(m)
folium.TileLayer('cartodbdark_matter', name='Dark Map').add_to(m)
# Add satellite imagery option
folium.TileLayer(
tiles='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
attr='Esri',
name='Satellite'
).add_to(m)
# Add draw control
draw = Draw(
export=True,
position='topleft',
draw_options={
'polyline': False,
'rectangle': True,
'polygon': True,
'circle': False,
'marker': False,
'circlemarker': False
},
edit_options={
'edit': True,
'remove': True
}
)
draw.add_to(m)
# Add existing shape if provided
if existing_shape:
# Convert to lat/lng for display
geo_coords = self.meters_to_latlng(existing_shape)
folium.Polygon(
locations=geo_coords,
color='blue',
fill=True,
fill_opacity=0.3,
popup='Existing Site Boundary'
).add_to(m)
# Add layer control
folium.LayerControl().add_to(m)
return m
def extract_drawn_shape(self, map_data: Dict) -> Optional[List[Tuple[float, float]]]:
"""Extract drawn shape from map data and convert to meters"""
if not map_data or 'all_drawings' not in map_data:
return None
drawings = map_data.get('all_drawings', [])
if not drawings:
return None
# Get the last drawn polygon
for drawing in reversed(drawings):
geom = drawing.get('geometry', {})
if geom.get('type') in ['Polygon', 'Rectangle']:
coords = geom.get('coordinates', [[]])[0]
if coords:
# Convert from [lng, lat] to [lat, lng] then to meters
latlng_coords = [(c[1], c[0]) for c in coords]
return self.latlng_to_meters(latlng_coords)
return None
def latlng_to_meters(self,
coords: List[Tuple[float, float]],
reference: Tuple[float, float] = None) -> List[Tuple[float, float]]:
"""
Convert lat/lng coordinates to local meter coordinates
Uses UTM projection centered on the shape
"""
if not coords:
return []
# Get reference point (centroid of shape)
if reference is None:
lats = [c[0] for c in coords]
lngs = [c[1] for c in coords]
reference = (sum(lats) / len(lats), sum(lngs) / len(lngs))
# Determine UTM zone
utm_zone = int((reference[1] + 180) / 6) + 1
is_northern = reference[0] >= 0
# Create projection
proj_string = f"+proj=utm +zone={utm_zone} +{'north' if is_northern else 'south'} +ellps=WGS84 +datum=WGS84 +units=m +no_defs"
try:
wgs84 = pyproj.CRS('EPSG:4326')
utm = pyproj.CRS(proj_string)
transformer = pyproj.Transformer.from_crs(wgs84, utm, always_xy=True)
# Transform coordinates
meter_coords = []
for lat, lng in coords:
x, y = transformer.transform(lng, lat)
meter_coords.append((x, y))
# Normalize to origin
xs = [c[0] for c in meter_coords]
ys = [c[1] for c in meter_coords]
min_x, min_y = min(xs), min(ys)
normalized = [(x - min_x, y - min_y) for x, y in meter_coords]
return normalized
except Exception as e:
print(f"Projection error: {e}")
# Fallback: simple equirectangular approximation
lat_scale = 111320 # meters per degree latitude
lng_scale = lat_scale * np.cos(np.radians(reference[0]))
meter_coords = []
ref_lat, ref_lng = reference
for lat, lng in coords:
x = (lng - ref_lng) * lng_scale
y = (lat - ref_lat) * lat_scale
meter_coords.append((x, y))
# Normalize to origin
xs = [c[0] for c in meter_coords]
ys = [c[1] for c in meter_coords]
min_x, min_y = min(xs), min(ys)
return [(x - min_x, y - min_y) for x, y in meter_coords]
def meters_to_latlng(self,
coords: List[Tuple[float, float]],
reference: Tuple[float, float] = None) -> List[Tuple[float, float]]:
"""Convert meter coordinates back to lat/lng (approximate)"""
if reference is None:
reference = self.center
lat_scale = 111320
lng_scale = lat_scale * np.cos(np.radians(reference[0]))
latlng_coords = []
for x, y in coords:
lat = reference[0] + y / lat_scale
lng = reference[1] + x / lng_scale
latlng_coords.append((lat, lng))
return latlng_coords
def validate_shape(self, coords: List[Tuple[float, float]]) -> Tuple[bool, str]:
"""Validate drawn shape"""
if not coords or len(coords) < 3:
return False, "Shape must have at least 3 points"
try:
polygon = Polygon(coords)
if not polygon.is_valid:
polygon = polygon.buffer(0)
if not polygon.is_valid:
return False, "Invalid polygon shape"
area = polygon.area
if area < 1000: # Less than 1000 sq meters
return False, f"Shape too small ({area:.0f} sq.m). Minimum 1000 sq.m required."
if area > 10000000: # More than 10 sq km
return False, f"Shape too large ({area/1000000:.2f} sq.km). Maximum 10 sq.km allowed."
return True, f"Valid shape: {area:.0f} sq.m"
except Exception as e:
return False, f"Validation error: {str(e)}"
class ShapePreviewRenderer:
"""Render shape previews for UI"""
@staticmethod
def render_shape_svg(coords: List[Tuple[float, float]],
width: int = 200,
height: int = 150) -> str:
"""Generate SVG preview of shape"""
if not coords:
return ""
# Normalize coordinates to fit SVG viewport
xs = [c[0] for c in coords]
ys = [c[1] for c in coords]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
shape_width = max_x - min_x
shape_height = max_y - min_y
if shape_width == 0 or shape_height == 0:
return ""
# Scale and translate
padding = 10
scale_x = (width - 2 * padding) / shape_width
scale_y = (height - 2 * padding) / shape_height
scale = min(scale_x, scale_y)
offset_x = padding + (width - 2 * padding - shape_width * scale) / 2
offset_y = padding + (height - 2 * padding - shape_height * scale) / 2
# Generate path
path_points = []
for x, y in coords:
sx = offset_x + (x - min_x) * scale
sy = height - (offset_y + (y - min_y) * scale) # Flip Y
path_points.append(f"{sx:.1f},{sy:.1f}")
path_d = "M " + " L ".join(path_points) + " Z"
svg = f"""
<svg width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f0f0f0" rx="5"/>
<path d="{path_d}" fill="#4CAF50" fill-opacity="0.3" stroke="#2E7D32" stroke-width="2"/>
</svg>
"""
return svg