dc-l1-tokyo-ring-orientation
Map (reference ← swipe → agent)
0:00
tokyo_buildings_legacy.geojson is a footprint export from an old in-house tool. When we load it into web-based GeoJSON viewers, building interiors are shaded incorrectly: holes appear filled and exteriors render as empty space. Our tile-server import script also rejects every feature with geometry warnings. Fix the geometries so that standard GeoJSON web viewers render them correctly. Attributes must be untouched. Output to tokyo_buildings_fixed.geojson.
Every feature must remain a Polygon with interior rings and holes preserved exactly as they are; do not flatten them. feature_id is the join key.
0:00
System info
{
"adapter": "openrouter",
"model": "google/gemma-4-26b-a4b-it",
"base_url": "https://openrouter.ai/api/v1"
}
0:01
Bash
ls -F
ls -F
tokyo_buildings_legacy.geojson
0:06
Write
/work/inspect_data.py
content · 14 lines · py
import geopandas as gpd
# Load the data to see what we are dealing with
try:
gdf = gpd.read_file('tokyo_buildings_legacy.geojson')
print("Columns:", gdf.columns.tolist())
print("First few rows:")
print(gdf.head())
print("\nGeometry types:")
print(gdf.geom_type.value_counts())
print("\nTotal features:", len(gdf))
except Exception as e:
print(f"Error loading file: {e}")
Wrote /work/inspect_data.py (404 chars).
0:07
Bash
python /work/inspect_data.py
python /work/inspect_data.py
Columns: ['feature_id', 'overture_id', 'name_primary', 'building_class', 'height', 'geometry'] First few rows: feature_id ... geometry 0 1 ... POLYGON ((139.70291 35.65924, 139.7028 35.6592... 1 2 ... POLYGON ((139.70109 35.6602, 139.70109 35.6600... 2 3 ... POLYGON ((139.70112 35.66167, 139.70099 35.661... 3 4 ... POLYGON ((139.70327 35.66018, 139.7033 35.6601... 4 5 ... POLYGON ((139.70158 35.6592, 139.70161 35.6592... [5 rows x 6 columns] Geometry types: Polygon 100 Name: count, dtype: int64 Total features: 100
0:29
Write
/work/fix_geometries.py
content · 84 lines · py
import geopandas as gpd
from shapely.geometry import Polygon, MultiPolygon
def check_ring_orientation(polygon):
"""
In GeoJSON (RFC 7946), exterior rings should be counter-clockwise
and interior rings should be clockwise.
Note: Some tools/older standards had this reversed.
The user says 'interiors are shaded incorrectly: holes appear filled and
exteriors render as empty space'.
This strongly suggests the orientation is reversed (exterior is CW, interior is CCW).
"""
if not isinstance(polygon, Polygon):
return []
# Check exterior
# A polygon is CCW if its signed area is positive.
# Shapely uses a right-handed system where CCW is positive area?
# Let's use the .is_ccw property or check signed area.
# Actually, let's use the 'orient' function from shapely which is designed for this.
return []
def fix_geometries(gdf):
# We use shapely.geometry.polygon.orient(polygon, sign=1.0)
# sign=1.0 means exterior is CCW, interior is CW.
# sign=-1.0 means exterior is CW, interior is CCW.
# Standard GeoJSON (RFC 7946) says exterior should be CCW.
from shapely.geometry import Polygon, MultiPolygon
from shapely.ops import orient
def fix_single_geom(geom):
if geom.is_empty:
return geom
if isinstance(geom, Polygon):
# orient(polygon, sign=1.0) makes exterior CCW and interior CW.
return orient(geom, sign=1.0)
elif isinstance(geom, MultiPolygon):
return MultiPolygon([orient(p, sign=1.0) for p in geom.geoms])
else:
return geom
gdf['geometry'] = gdf['geometry'].apply(fix_single_geom)
return gdf
# Try to reproduce the "incorrect" state by checking orientations of current rings
def analyze_orientations(gdf):
print("\nAnalyzing ring orientations (assuming current state is wrong):")
for idx, row in gdf.iterrows():
geom = row['geometry']
if isinstance(geom, Polygon):
# Exterior
ext = list(geom.exterior.coords)
# Compute signed area of exterior
area_ext = 0
for i in range(len(ext)-1):
area_ext += (ext[i][0] * ext[i+1][1] - ext[i+1][0] * ext[i][1])
print(f"ID {row['feature_id']} - Ext Area Sign: {1 if area_ext > 0 else -1}")
# Interiors
for i, ring in enumerate(geom.interiors):
coords = list(ring.coords)
area_int = 0
for j in range(len(coords)-1):
area_int += (coords[j][0] * coords[j+1][1] - coords[j+1][0] * coords[j][1])
print(f" ID {row['feature_id']} - Int Ring {i} Area Sign: {1 if area_int > 0 else -1}")
# Load
gdf = gpd.read_file('tokyo_buildings_legacy.geojson')
analyze_orientations(gdf)
# Fix
print("\nApplying fix (orienting to RFC 7946)...")
gdf_fixed = fix_geometries(gdf)
# Save
gdf_fixed.to_file('tokyo_buildings_fixed.geojson', driver='GeoJSON')
print("Saved to tokyo_buildings_fixed.geojson")
# Verify the fix
print("\nVerifying fixed orientations:")
analyze_orientations(gdf_fixed)
Wrote /work/fix_geometries.py (3165 chars).
0:30
Bash
python /work/fix_geometries.py
python /work/fix_geometries.py
Analyzing ring orientations (assuming current state is wrong): ID 1 - Ext Area Sign: -1 ID 2 - Ext Area Sign: -1 ID 3 - Ext Area Sign: -1 ID 4 - Ext Area Sign: -1 ID 5 - Ext Area Sign: -1 ID 6 - Ext Area Sign: -1 ID 6 - Int Ring 0 Area Sign: 1 ID 7 - Ext Area Sign: -1 ID 8 - Ext Area Sign: -1 ID 9 - Ext Area Sign: -1 ID 10 - Ext Area Sign: -1 ID 11 - Ext Area Sign: -1 ID 12 - Ext Area Sign: -1 ID 13 - Ext Area Sign: -1 ID 14 - Ext Area Sign: -1 ID 15 - Ext Area Sign: -1 ID 16 - Ext Area Sign: -1 ID 17 - Ext Area Sign: -1 ID 18 - Ext Area Sign: -1 ID 18 - Int Ring 0 Area Sign: 1 ID 19 - Ext Area Sign: -1 ID 20 - Ext Area Sign: -1 ID 21 - Ext Area Sign: -1 ID 22 - Ext Area Sign: -1 ID 23 - Ext Area Sign: -1 ID 24 - Ext Area Sign: -1 ID 25 - Ext Area Sign: -1 ID 26 - Ext Area Sign: -1 ID 27 - Ext Area Sign: -1 ID 28 - Ext Area Sign: -1 ID 29 - Ext Area Sign: -1 ID 30 - Ext Area Sign: -1 ID 31 - Ext Area Sign: -1 ID 32 - Ext Area Sign: -1 ID 32 - Int Ring 0 Area Sign: 1 ID 33 - Ext Area Sign: -1 ID 34 - Ext Area Sign: -1 ID 35 - Ext Area Sign: -1 ID 36 - Ext Area Sign: -1 ID 37 - Ext Area Sign: -1 ID 38 - Ext Area Sign: -1 ID 39 - Ext Area Sign: -1 ID 40 - Ext Area Sign: -1 ID 41 - Ext Area Sign: -1 ID 42 - Ext Area Sign: -1 ID 43 - Ext Area Sign: -1 ID 44 - Ext Area Sign: -1 ID 45 - Ext Area Sign: -1 ID 46 - Ext Area Sign: -1 ID 47 - Ext Area Sign: -1 ID 48 - Ext Area Sign: -1 ID 49 - Ext Area Sign: -1 ID 50 - Ext Area Sign: -1 ID 50 - Int Ring 0 Area Sign: 1 ID 51 - Ext Area Sign: -1 ID 52 - Ext Area Sign: -1 ID 53 - Ext Area Sign: -1 ID 54 - Ext Area Sign: -1 ID 55 - Ext Area Sign: -1 ID 56 - Ext Area Sign: -1 ID 57 - Ext Area Sign: -1 ID 58 - Ext Area Sign: -1 ID 59 - Ext Area Sign: -1 ID 60 - Ext Area Sign: -1 ID 61 - Ext Area Sign: -1 ID 62 - Ext Area Sign: -1 ID 63 - Ext Area Sign: -1 ID 64 - Ext Area Sign: -1 ID 65 - Ext Area Sign: -1 ID 66 - Ext Area Sign: -1 ID 67 - Ext Area Sign: -1 ID 68 - Ext Area Sign: -1 ID 69 - Ext Area Sign: -1 ID 70 - Ext Area Sign: -1 ID 71 - Ext Area Sign: -1 ID 72 - Ext Area Sign: -1 ID 73 - Ext Area Sign: -1 ID 74 - Ext Area Sign: -1 ID 74 - Int Ring 0 Area Sign: 1 ID 75 - Ext Area Sign: -1 ID 76 - Ext Area Sign: -1 ID 77 - Ext Area Sign: -1 ID 78 - Ext Area Sign: -1 ID 79 - Ext Area Sign: -1 ID 80 - Ext Area Sign: -1 ID 81 - Ext Area Sign: -1 ID 82 - Ext Area Sign: -1 ID 83 - Ext Area Sign: -1 ID 84 - Ext Area Sign: -1 ID 85 - Ext Area Sign: -1 ID 86 - Ext Area Sign: -1 ID 87 - Ext Area Sign: -1 ID 88 - Ext Area Sign: -1 ID 89 - Ext Area Sign: -1 ID 90 - Ext Area Sign: -1 ID 91 - Ext Area Sign: -1 ID 92 - Ext Area Sign: -1 ID 93 - Ext Area Sign: -1 ID 94 - Ext Area Sign: -1 ID 95 - Ext Area Sign: -1 ID 96 - Ext Area Sign: -1 ID 97 - Ext Area Sign: -1 ID 98 - Ext Area Sign: -1 ID 99 - Ext Area Sign: -1 ID 100 - Ext Area Sign: -1 Applying fix (orienting to RFC 7946)... Saved to tokyo_buildings_fixed.geojson Verifying fixed orientations: Analyzing ring orientations (assuming current state is wrong): ID 1 - Ext Area Sign: 1 ID 2 - Ext Area Sign: 1 ID 3 - Ext Area Sign: 1 ID 4 - Ext Area Sign: 1 ID 5 - Ext Area Sign: 1 ID 6 - Ext Area Sign: 1 ID 6 - Int Ring 0 Area Sign: -1 ID 7 - Ext Area Sign: 1 ID 8 - Ext Area Sign: 1 ID 9 - Ext Area Sign: 1 ID 10 - Ext Area Sign: 1 ID 11 - Ext Area Sign: 1 ID 12 - Ext Area Sign: 1 ID 13 - Ext Area Sign: 1 ID 14 - Ext Area Sign: 1 ID 15 - Ext Area Sign: 1 ID 16 - Ext Area Sign: 1 ID 17 - Ext Area Sign: 1 ID 18 - Ext Area Sign: 1 ID 18 - Int Ring 0 Area Sign: -1 ID 19 - Ext Area Sign: 1 ID 20 - Ext Area Sign: 1 ID 21 - Ext Area Sign: 1 ID 22 - Ext Area Sign: 1 ID 23 - Ext Area Sign: 1 ID 24 - Ext Area Sign: 1 ID 25 - Ext Area Sign: 1 ID 26 - Ext Area Sign: 1 ID 27 - Ext Area Sign: 1 ID 28 - Ext Area Sign: 1 ID 29 - Ext Area Sign: 1 ID 30 - Ext Area Sign: 1 ID 31 - Ext Area Sign: 1 ID 32 - Ext Area Sign: 1 ID 32 - Int Ring 0 Area Sign: -1 ID 33 - Ext Area Sign: 1 ID 34 - Ext Area Sign: 1 ID 35 - Ext Area Sign: 1 ID 36 - Ext Area Sign: 1 ID 37 - Ext Area Sign: 1 ID 38 - Ext Area Sign: 1 ID 39 - Ext Area Sign: 1 ID 40 - Ext Area Sign: 1 ID 41 - Ext Area Sign: 1 ID 42 - Ext Area Sign: 1 ID 43 - Ext Area Sign: 1 ID 44 - Ext Area Sign: 1 ID 45 - Ext Area Sign: 1 ID 46 - Ext Area Sign: 1 ID 47 - Ext Area Sign: 1 ID 48 - Ext Area Sign: 1 ID 49 - Ext Area Sign: 1 ID 50 - Ext Area Sign: 1 ID 50 - Int Ring 0 Area Sign: -1 ID 51 - Ext Area Sign: 1 ID 52 - Ext Area Sign: 1 ID 53 - Ext Area Sign: 1 ID 54 - Ext Area Sign: 1 ID 55 - Ext Area Sign: 1 ID 56 - Ext Area Sign: 1 ID 57 - Ext Area Sign: 1 ID 58 - Ext Area Sign: 1 ID 59 - Ext Area Sign: 1 ID 60 - Ext Area Sign: 1 ID 61 - Ext Area Sign: 1 ID 62 - Ext Area Sign: 1 ID 63 - Ext Area Sign: 1 ID 64 - Ext Area Sign: 1 ID 65 - Ext Area Sign: 1 ID 66 - Ext Area Sign: 1 ID 67 - Ext Area Sign: 1 ID 68 - Ext Area Sign: 1 ID 69 - Ext Area Sign: 1 ID 70 - Ext Area Sign: 1 ID 71 - Ext Area Sign: 1 ID 72 - Ext Area Sign: 1 ID 73 - Ext Area Sign: 1 ID 74 - Ext Area Sign: 1 ID 74 - Int Ring 0 Area Sign: -1 ID 75 - Ext Area Sign: 1 ID 76 - Ext Area Sign: 1 ID 77 - Ext Area Sign: 1 ID 78 - Ext Area Sign: 1 ID 79 - Ext Area Sign: 1 ID 80 - Ext Area Sign: 1 ID 81 - Ext Area Sign: 1 ID 82 - Ext Area Sign: 1 ID 83 - Ext Area Sign: 1 ID 84 - Ext Area Sign: 1 ID 85 - Ext Area Sign: 1 ID 86 - Ext Area Sign: 1 ID 87 - Ext Area Sign: 1 ID 88 - Ext Area Sign: 1 ID 89 - Ext Area Sign: 1 ID 90 - Ext Area Sign: 1 ID 91 - Ext Area Sign: 1 ID 92 - Ext Area Sign: 1 ID 93 - Ext Area Sign: 1 ID 94 - Ext Area Sign: 1 ID 95 - Ext Area Sign: 1 ID 96 - Ext Area Sign: 1 ID 97 - Ext Area Sign: 1 ID 98 - Ext Area Sign: 1 ID 99 - Ext Area Sign: 1 ID 100 - Ext Area Sign: 1
0:34
Bash
# I'll also do a quick check to see if there are any invalid geometries like sel...
# I'll also do a quick check to see if there are any invalid geometries like self-intersections
# that might be causing the tile-server to reject them.
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy.geojson'); print(gdf.is_valid.value_counts())"
True 100 Name: count, dtype: int64
0:42
Write
/work/final_fix.py
content · 30 lines · py
import geopandas as gpd
from shapely.geometry import Polygon, MultiPolygon
from shapely.ops import orient
def fix_geometries(gdf):
def fix_single_geom(geom):
if geom.is_empty:
return geom
if isinstance(geom, Polygon):
# RFC 7946: Exterior is CCW (sign=1.0), Interior is CW
return orient(geom, sign=1.0)
elif isinstance(geom, MultiPolygon):
return MultiPolygon([orient(p, sign=1.0) for p in geom.geoms])
else:
return geom
gdf['geometry'] = gdf['geometry'].apply(fix_single_geom)
return gdf
if __name__ == "__main__":
# Load the data
gdf = gpd.read_file('tokyo_buildings_legacy.geojson')
# Apply the fix
gdf_fixed = fix_geometries(gdf)
# Save the result
gdf_fixed.to_file('tokyo_buildings_fixed.geojson', driver='GeoJSON')
print("Successfully fixed geometries and saved to tokyo_buildings_fixed.geojson")
Wrote /work/final_fix.py (948 chars).
0:43
Bash
python /work/final_fix.py
python /work/final_fix.py
Successfully fixed geometries and saved to tokyo_buildings_fixed.geojson
0:46
Bash
# Check if any geometries became invalid after the fix (should not happen with o...
# Check if any geometries became invalid after the fix (should not happen with orient)
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_fixed.geojson'); print(gdf.is_valid.value_counts())"
True 100 Name: count, dtype: int64
0:55