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:00
Bash
ls -lh tokyo_buildings_legacy.geojson
-rw-r--r-- 1 agent agent 89K Jun 17 20:04 tokyo_buildings_legacy.geojson
0:01
Bash
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy....
0 POLYGON ((139.70291 35.65924, 139.7028 35.6592... 1 POLYGON ((139.70109 35.6602, 139.70109 35.6600... 2 POLYGON ((139.70112 35.66167, 139.70099 35.661... 3 POLYGON ((139.70327 35.66018, 139.7033 35.6601... 4 POLYGON ((139.70158 35.6592, 139.70161 35.6592... Name: geometry, dtype: geometry True 100 Name: count, dtype: int64
0:04
Bash
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy....
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]
0:05
Bash
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy....
POLYGON ((139.7029081 35.6592372, 139.7027971 35.659202, 139.7027755 35.6592471, 139.7028864 35.6592823, 139.7029081 35.6592372)) <shapely.geometry.polygon.InteriorRingSequence object at 0x7f4ab2f82900>
0:08
Bash
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy....
Polygon 100 Name: count, dtype: int64
0:18
Bash
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy....
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy.geojson');
def check_winding(poly):
# Check exterior ring winding (RFC 7946: exterior should be CCW)
# Shapely's is_ccw property handles this.
return poly.exterior.is_ccw
# Checking first 5
for i in range(5):
print(f'ID {i}: exterior CCW? {check_winding(gdf.iloc[i].geometry)}')
if len(gdf.iloc[i].geometry.interiors) > 0:
print(f' ID {i}: first interior CCW? {gdf.iloc[i].geometry.interiors[0].is_ccw}')
else:
print(f' ID {i}: no interiors')
"
ID 0: exterior CCW? False ID 0: no interiors ID 1: exterior CCW? False ID 1: no interiors ID 2: exterior CCW? False ID 2: no interiors ID 3: exterior CCW? False ID 3: no interiors ID 4: exterior CCW? False ID 4: no interiors
0:21
Bash
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy....
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy.geojson');
def check_winding(poly):
# RFC 7946: Exterior ring MUST be counter-clockwise (CCW)
# Interior rings MUST be clockwise (CW)
ext_ccw = poly.exterior.is_ccw
int_cw = all(not ring.is_ccw for ring in poly.interiors)
return ext_ccw, int_cw
for i in range(5):
ext, ints = check_winding(gdf.iloc[i].geometry)
print(f'ID {i}: Ext CCW: {ext}, Ints CW: {ints}')
"
ID 0: Ext CCW: False, Ints CW: True ID 1: Ext CCW: False, Ints CW: True ID 2: Ext CCW: False, Ints CW: True ID 3: Ext CCW: False, Ints CW: True ID 4: Ext CCW: False, Ints CW: True
0:24
Bash
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy....
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy.geojson');
from shapely.geometry import Polygon
def fix_polygon(poly):
if not isinstance(poly, Polygon):
return poly
# Get all rings
ext_coords = list(poly.exterior.coords)
# RFC 7946: Exterior CCW. Shapely is_ccw: True if CCW.
# However, we can just use orient() from shapely.geometry.polygon
from shapely.geometry import polygon
# orient(polygon, sign=1.0) makes exterior CCW and interiors CW
return polygon.orient(poly, sign=1.0)
gdf['geometry'] = gdf['geometry'].apply(fix_polygon)
gdf.to_file('tokyo_buildings_fixed.geojson', driver='GeoJSON')
print('Fixed and saved.')
"
Fixed and saved.
0:28
Bash
python -c "import geopandas as gpd; gdf_old = gpd.read_file('tokyo_buildings_leg...
python -c "import geopandas as gpd; gdf_old = gpd.read_file('tokyo_buildings_legacy.geojson'); gdf_new = gpd.read_file('tokyo_buildings_fixed.geojson');
print('--- Old ---')
for i in range(3):
p = gdf_old.iloc[i].geometry
print(f'ID {i} Ext CCW: {p.exterior.is_ccw}, Ints CW: {all(not r.is_ccw for r in p.interiors)}')
print('\n--- New ---')
for i in range(3):
p = gdf_new.iloc[i].geometry
print(f'ID {i} Ext CCW: {p.exterior.is_ccw}, Ints CW: {all(not r.is_ccw for r in p.interiors)}')
# Check attributes
print('\nAttributes match:', gdf_old.drop(columns='geometry').equals(gdf_new.drop(columns='geometry')))
"
--- Old --- ID 0 Ext CCW: False, Ints CW: True ID 1 Ext CCW: False, Ints CW: True ID 2 Ext CCW: False, Ints CW: True --- New --- ID 0 Ext CCW: True, Ints CW: True ID 1 Ext CCW: True, Ints CW: True ID 2 Ext CCW: True, Ints CW: True Attributes match: True
0:34
Bash
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy....
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy.geojson');
import shapely.validation
def fix_geometry(geom):
if not geom.is_valid:
# Attempt to fix invalid geometries using buffer(0)
geom = geom.buffer(0)
# Now fix orientation
from shapely.geometry import polygon
return polygon.orient(geom, sign=1.0)
gdf['geometry'] = gdf['geometry'].apply(fix_geometry)
gdf.to_file('tokyo_buildings_fixed.geojson', driver='GeoJSON')
print('Validation check:')
print(gdf.geometry.is_valid.value_counts())
"
Validation check: True 100 Name: count, dtype: int64
0:39
(no output)
Bash
# Check if any interior rings were lost or became exteriors (shouldn't happen wi...
# Check if any interior rings were lost or became exteriors (shouldn't happen with orient)
python -c "import geopandas as gpd;
gdf_old = gpd.read_file('tokyo_buildings_legacy.geojson');
gdf_new = gpd.read_file('tokyo_buildings_fixed.geojson');
for i in range(len(gdf_old)):
old_ints = len(gdf_old.iloc[i].geometry.interiors)
new_ints = len(gdf_new.iloc[i].geometry.interiors)
if old_ints != new_ints:
print(f'Mismatch at index {i}: old {old_ints} ints, new {new_ints} ints')
"
0:41
Bash
# Final check of the files
# Final check of the files
ls -lh tokyo_buildings_fixed.geojson
python -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_fixed.geojson'); print(gdf.head()); print(gdf.geometry.is_valid.all())"
-rw-r--r-- 1 agent agent 44K Jun 17 20:04 tokyo_buildings_fixed.geojson feature_id ... geometry 0 1 ... POLYGON ((139.70291 35.65924, 139.70289 35.659... 1 2 ... POLYGON ((139.70109 35.6602, 139.70092 35.6602... 2 3 ... POLYGON ((139.70112 35.66167, 139.70111 35.661... 3 4 ... POLYGON ((139.70327 35.66018, 139.70319 35.660... 4 5 ... POLYGON ((139.70158 35.6592, 139.70154 35.6591... [5 rows x 6 columns] True
0:46
Write
/work/solve.py
content · 40 lines · py
import geopandas as gpd
from shapely.geometry import polygon
def fix_geometry(geom):
"""
Fixes geometry by ensuring it is valid and correctly oriented
according to RFC 7946 (Exterior CCW, Interior CW).
"""
if not geom.is_valid:
# buffer(0) is a common trick to fix self-intersections/invalidities
geom = geom.buffer(0)
# orient(polygon, sign=1.0) ensures exterior is CCW and interiors are CW
return polygon.orient(geom, sign=1.0)
def main():
input_file = 'tokyo_buildings_legacy.geojson'
output_file = 'tokyo_buildings_fixed.geojson'
# Load the dataset
gdf = gpd.read_file(input_file)
# Apply the fix to the geometry column
# This preserves all attributes as we are modifying the geometry column in place
gdf['geometry'] = gdf['geometry'].apply(fix_geometry)
# Check if everything is valid now
if not gdf.geometry.is_valid.all():
print("Warning: Some geometries are still invalid after processing.")
print(gdf.geometry.is_valid.value_counts())
else:
print("All geometries are valid.")
# Export to GeoJSON
gdf.to_file(output_file, driver='GeoJSON')
print(f"Successfully fixed geometries and saved to {output_file}")
if __name__ == "__main__":
main()
Wrote /work/solve.py (1298 chars).
0:47
Bash
python /work/solve.py
All geometries are valid. Successfully fixed geometries and saved to tokyo_buildings_fixed.geojson
0:53