drone-route-planner / src /step1_analyze_map.py
harphool17's picture
Drone Route Planner - Jaipur Airspace
5876410
"""
============================================================
Step 1: Analyze & Validate map.geojson
============================================================
This script reads the zone data from map.geojson and produces
a comprehensive analysis report including:
- Zone counts (Red vs Yellow)
- Data quality issues (missing names, inconsistent keys)
- Zone area calculations
- Bounding box of the operational area
- Visual map of all zones
============================================================
"""
import json
import os
import sys
import geopandas as gpd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from shapely.geometry import shape
# ──────────────────────────────────────────────
# CONFIG
# ──────────────────────────────────────────────
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.path.join(BASE_DIR, "data")
OUTPUT_DIR = os.path.join(BASE_DIR, "output")
MAP_FILE = os.path.join(DATA_DIR, "map.geojson")
def load_geojson(filepath):
"""Load GeoJSON file and return raw dict + GeoDataFrame."""
with open(filepath, "r", encoding="utf-8") as f:
raw = json.load(f)
gdf = gpd.read_file(filepath)
return raw, gdf
def classify_zones(raw_data):
"""
Classify each feature into Red, Yellow, Boundary, or Unknown.
Handles inconsistent property keys (zone_id vs zone-id, type with trailing space).
"""
zones = {
"red": [],
"yellow": [],
"boundary": [],
"empty": [],
"unknown": []
}
issues = []
for i, feature in enumerate(raw_data["features"]):
props = feature.get("properties", {})
# Get zone_id (handle both 'zone_id' and 'zone-id')
zone_id = props.get("zone_id") or props.get("zone-id")
# Get type (handle trailing space: 'type ' vs 'type')
zone_type = props.get("type") or props.get("type ")
if zone_type:
zone_type = zone_type.strip().lower()
name = props.get("name") or props.get("nam") or props.get("name ")
# ── Check for data issues ──
if props.get("zone-id") and not props.get("zone_id"):
issues.append(f" ⚠️ Feature {i}: Uses 'zone-id' instead of 'zone_id' β†’ {zone_id}")
if props.get("type "):
issues.append(f" ⚠️ Feature {i}: 'type ' has trailing space β†’ {zone_id}")
if props.get("nam"):
issues.append(f" ⚠️ Feature {i}: Uses 'nam' instead of 'name' β†’ {zone_id}")
if zone_id and not name:
issues.append(f" ⚠️ Feature {i}: Missing name for zone β†’ {zone_id}")
# ── Classify ──
if not props or len(props) == 0:
zones["empty"].append({"index": i, "zone_id": None, "name": None})
elif props.get("name") == "Jaipur_Wide_Drone_World":
zones["boundary"].append({"index": i, "zone_id": None, "name": "Operational Boundary"})
elif zone_type == "red" or (zone_id and "Red" in str(zone_id)):
zones["red"].append({"index": i, "zone_id": zone_id, "name": name or "Unnamed"})
elif zone_type == "yellow" or (zone_id and "Yellow" in str(zone_id)):
zones["yellow"].append({"index": i, "zone_id": zone_id, "name": name or "Unnamed"})
elif zone_id:
zones["unknown"].append({"index": i, "zone_id": zone_id, "name": name})
else:
zones["empty"].append({"index": i, "zone_id": None, "name": None})
return zones, issues
def calculate_areas(raw_data):
"""Calculate area of each zone in square meters (approximate)."""
zone_areas = []
for feature in raw_data["features"]:
props = feature.get("properties", {})
zone_id = props.get("zone_id") or props.get("zone-id")
if not zone_id:
continue
try:
geom = shape(feature["geometry"])
# Convert degrees to approximate meters (at Jaipur latitude ~26.85Β°N)
# 1 degree lat β‰ˆ 111,320 m, 1 degree lon β‰ˆ 99,855 m at this latitude
area_deg2 = geom.area
area_m2 = area_deg2 * 111320 * 99855 # Rough approximation
zone_areas.append({
"zone_id": zone_id,
"name": props.get("name") or props.get("nam") or "?",
"area_m2": area_m2,
"area_km2": area_m2 / 1e6
})
except Exception:
pass
return zone_areas
def get_bounding_box(raw_data):
"""Get the operational area bounding box."""
for feature in raw_data["features"]:
props = feature.get("properties", {})
if props.get("name") == "Jaipur_Wide_Drone_World":
coords = feature["geometry"]["coordinates"][0]
lons = [c[0] for c in coords]
lats = [c[1] for c in coords]
return {
"min_lon": min(lons),
"max_lon": max(lons),
"min_lat": min(lats),
"max_lat": max(lats),
"width_km": (max(lons) - min(lons)) * 99.855,
"height_km": (max(lats) - min(lats)) * 111.32
}
return None
def plot_zones(raw_data, zones, output_dir):
"""Create a visual map of all zones."""
fig, ax = plt.subplots(1, 1, figsize=(14, 12))
ax.set_facecolor("#1a1a2e")
fig.patch.set_facecolor("#0d0d1a")
# Draw boundary
for feature in raw_data["features"]:
props = feature.get("properties", {})
if props.get("name") == "Jaipur_Wide_Drone_World":
geom = shape(feature["geometry"])
x, y = geom.exterior.xy
ax.fill(x, y, alpha=0.1, color="#00ff88", linewidth=2, edgecolor="#00ff88")
ax.plot(x, y, color="#00ff88", linewidth=2, linestyle="--", alpha=0.6)
# Draw Red zones
for z in zones["red"]:
feature = raw_data["features"][z["index"]]
geom = shape(feature["geometry"])
x, y = geom.exterior.xy
ax.fill(x, y, alpha=0.5, color="#ff3333", edgecolor="#ff0000", linewidth=1.5)
cx, cy = geom.centroid.coords[0]
ax.annotate(z["zone_id"], (cx, cy), fontsize=5, ha="center",
color="white", fontweight="bold",
bbox=dict(boxstyle="round,pad=0.2", facecolor="#ff0000", alpha=0.7))
# Draw Yellow zones
for z in zones["yellow"]:
feature = raw_data["features"][z["index"]]
geom = shape(feature["geometry"])
try:
x, y = geom.exterior.xy
ax.fill(x, y, alpha=0.4, color="#ffcc00", edgecolor="#ff9900", linewidth=1)
cx, cy = geom.centroid.coords[0]
ax.annotate(z["zone_id"], (cx, cy), fontsize=4, ha="center",
color="black", fontweight="bold",
bbox=dict(boxstyle="round,pad=0.15", facecolor="#ffcc00", alpha=0.6))
except Exception:
pass
# Legend
red_patch = mpatches.Patch(color="#ff3333", alpha=0.6, label=f'πŸ”΄ Red Zones ({len(zones["red"])})')
yellow_patch = mpatches.Patch(color="#ffcc00", alpha=0.6, label=f'🟑 Yellow Zones ({len(zones["yellow"])})')
green_patch = mpatches.Patch(color="#00ff88", alpha=0.3, label="🟒 Operational Area")
ax.legend(handles=[red_patch, yellow_patch, green_patch], loc="upper left",
fontsize=10, facecolor="#1a1a2e", edgecolor="#00ff88",
labelcolor="white")
ax.set_xlabel("Longitude", color="white", fontsize=12)
ax.set_ylabel("Latitude", color="white", fontsize=12)
ax.set_title("πŸ›Έ Jaipur Drone Airspace Map β€” Zone Analysis", color="#00ff88",
fontsize=16, fontweight="bold", pad=20)
ax.tick_params(colors="white")
for spine in ax.spines.values():
spine.set_color("#00ff88")
plt.tight_layout()
save_path = os.path.join(output_dir, "zone_analysis_map.png")
plt.savefig(save_path, dpi=150, bbox_inches="tight", facecolor=fig.get_facecolor())
plt.close()
print(f"\n πŸ“Š Map saved to: {save_path}")
def main():
print("=" * 60)
print(" πŸ›Έ DRONE AIRSPACE MAP ANALYZER β€” Step 1")
print("=" * 60)
# ── Load data ──
if not os.path.exists(MAP_FILE):
print(f"\n ❌ ERROR: map.geojson not found at: {MAP_FILE}")
sys.exit(1)
print(f"\n πŸ“‚ Loading: {MAP_FILE}")
raw_data, gdf = load_geojson(MAP_FILE)
print(f" βœ… Loaded {len(raw_data['features'])} features")
# ── Bounding Box ──
bbox = get_bounding_box(raw_data)
if bbox:
print(f"\n πŸ“ Operational Area:")
print(f" Longitude: {bbox['min_lon']:.4f} β†’ {bbox['max_lon']:.4f}")
print(f" Latitude: {bbox['min_lat']:.4f} β†’ {bbox['max_lat']:.4f}")
print(f" Size: {bbox['width_km']:.1f} km Γ— {bbox['height_km']:.1f} km")
# ── Classify zones ──
zones, issues = classify_zones(raw_data)
print(f"\n πŸ“Š Zone Classification:")
print(f" πŸ”΄ Red Zones: {len(zones['red'])}")
print(f" 🟑 Yellow Zones: {len(zones['yellow'])}")
print(f" 🟒 Boundary: {len(zones['boundary'])}")
print(f" ❓ Empty/Unknown: {len(zones['empty']) + len(zones['unknown'])}")
# ── List all zones ──
print(f"\n {'─' * 50}")
print(f" πŸ”΄ RED ZONES (No-Fly):")
print(f" {'─' * 50}")
for z in zones["red"]:
print(f" {z['zone_id']:>10s} β”‚ {z['name']}")
print(f"\n {'─' * 50}")
print(f" 🟑 YELLOW ZONES (Permission Required):")
print(f" {'─' * 50}")
for z in zones["yellow"]:
print(f" {z['zone_id']:>12s} β”‚ {z['name']}")
# ── Data Quality Issues ──
if issues:
print(f"\n {'─' * 50}")
print(f" ⚠️ DATA QUALITY ISSUES ({len(issues)} found):")
print(f" {'─' * 50}")
for issue in issues:
print(issue)
else:
print(f"\n βœ… No data quality issues found!")
# ── Area calculations ──
zone_areas = calculate_areas(raw_data)
zone_areas.sort(key=lambda x: x["area_m2"], reverse=True)
print(f"\n {'─' * 50}")
print(f" πŸ“ TOP 10 LARGEST ZONES BY AREA:")
print(f" {'─' * 50}")
print(f" {'Zone ID':>14s} β”‚ {'Name':<30s} β”‚ {'Area':>10s}")
print(f" {'─' * 14}─┼─{'─' * 30}─┼─{'─' * 10}")
for z in zone_areas[:10]:
if z["area_km2"] >= 0.01:
area_str = f"{z['area_km2']:.3f} kmΒ²"
else:
area_str = f"{z['area_m2']:.0f} mΒ²"
print(f" {z['zone_id']:>14s} β”‚ {z['name']:<30s} β”‚ {area_str:>10s}")
# ── Plot zones ──
os.makedirs(OUTPUT_DIR, exist_ok=True)
print(f"\n 🎨 Generating zone visualization map...")
plot_zones(raw_data, zones, OUTPUT_DIR)
# ── Summary ──
total_red_area = sum(z["area_km2"] for z in zone_areas if "Red" in z["zone_id"])
total_yellow_area = sum(z["area_km2"] for z in zone_areas if "Yellow" in z["zone_id"])
total_area = bbox["width_km"] * bbox["height_km"] if bbox else 0
green_area = total_area - total_red_area - total_yellow_area
print(f"\n {'═' * 50}")
print(f" πŸ“‹ SUMMARY")
print(f" {'═' * 50}")
print(f" Total operational area: {total_area:.1f} kmΒ²")
print(f" Red zone coverage: {total_red_area:.3f} kmΒ² ({total_red_area/total_area*100:.1f}%)")
print(f" Yellow zone coverage: {total_yellow_area:.3f} kmΒ² ({total_yellow_area/total_area*100:.1f}%)")
print(f" Green (open) area: {green_area:.1f} kmΒ² ({green_area/total_area*100:.1f}%)")
print(f"\n βœ… Step 1 Complete! Ready for Step 2 (Building Extraction).")
print(f" {'═' * 50}")
if __name__ == "__main__":
main()