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:17
Bash
ls -l
total 92 -rw-r--r-- 1 agent agent 91050 Jun 16 10:33 tokyo_buildings_legacy.geojson
0:19
Bash
python3 -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] Polygon 100 Name: count, dtype: int64
0:38
Bash
python3 -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy...
python3 -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_legacy.geojson');
def check_ring_order(poly):
# Exterior ring: should be CCW for GeoJSON (RFC 7946)
# However, many older tools used CW.
# Let's check if the exterior ring is CW or CCW.
# In Shapely, exterior.is_ccw tells us.
# RFC 7946 says: 'exterior ring is counter-clockwise, and holes are clockwise'
from shapely.geometry import Polygon
if not isinstance(poly, Polygon): return True
ext = poly.exterior
# Check exterior
if ext.is_ccw:
pass # Good for RFC 7946
else:
return False # Bad for RFC 7946
# Check holes
for ring in poly.interiors:
if ring.is_ccw:
return False # Holes should be CW
return True
gdf['valid_order'] = gdf.geometry.apply(check_ring_order)
print(gdf['valid_order'].value_counts())"
valid_order False 100 Name: count, dtype: int64
1:46
Write
/work/solve.py
content · 76 lines · py
import geopandas as gpd
from shapely.geometry import Polygon
def fix_polygon_ring_order(poly):
if not isinstance(poly, Polygon):
return poly
# Get the exterior ring coordinates
ext_coords = list(poly.exterior.coords)
# Get the interior rings coordinates
int_coords = [list(ring.coords) for ring in poly.interiors]
# RFC 7946: exterior ring is counter-clockwise, holes are clockwise.
# In Shapely/GEOS/JTS:
# .is_ccw is True if the ring is counter-clockwise.
# We create a new Polygon with the rings oriented correctly.
# First, let's ensure the exterior is CCW.
# We can use the orientation to check and then reverse if necessary.
# Actually, Shapely's Polygon constructor doesn't guarantee orientation,
# but we can use orient() from shapely.geometry.polygon.orient
from shapely.geometry.polygon import orient
# orient(polygon, sign=1.0)
# sign=1.0 means exterior CCW, interiors CW (RFC 7946 standard)
return orient(poly, sign=1.0)
def main():
input_file = 'tokyo_buildings_legacy.geojson'
output_file = 'tokyo_buildings_fixed.geojson'
print(f"Reading {input_file}...")
gdf = gpd.read_file(input_file)
print("Fixing ring orientations...")
# Apply the orientation fix
gdf.geometry = gdf.geometry.apply(fix_polygon_order_wrapper)
# In case there are invalid geometries (self-intersections etc)
# often associated with "geometry warnings" in tile servers.
# We should use buffer(0) to clean up topology.
print("Cleaning up topology (buffer(0))...")
gdf.geometry = gdf.geometry.buffer(0)
# Re-applying orientation after buffer(0) just to be safe
gdf.geometry = gdf.geometry.apply(fix_polygon_order_wrapper)
print(f"Saving to {output_file}...")
gdf.to_file(output_file, driver='GeoJSON')
print("Done.")
def fix_polygon_order_wrapper(poly):
# We use the robust orient function from shapely
from shapely.geometry.polygon import orient
if poly.is_empty:
return poly
return orient(poly, sign=1.0)
if __name__ == "__main__":
# Re-defining to ensure everything is in one script
from shapely.geometry.polygon import orient
gdf = gpd.read_file('tokyo_buildings_legacy.geojson')
# 1. Fix ring orientation (RFC 7946)
gdf.geometry = gdf.geometry.apply(lambda p: orient(p, sign=1.0) if isinstance(p, Polygon) else p)
# 2. Fix topology (self-intersections) which often causes "geometry warnings"
gdf.geometry = gdf.geometry.buffer(0)
# 3. Re-fix orientation after buffer (buffer can change orientation)
gdf.geometry = gdf.geometry.apply(lambda p: orient(p, sign=1.0) if isinstance(p, Polygon) else p)
gdf.to_file('tokyo_buildings_fixed.geojson', driver='GeoJSON')
Wrote /work/solve.py (2833 chars).
1:47
Bash
python3 /work/solve.py
(no output)
1:59
Bash
python3 -c "import geopandas as gpd;
python3 -c "import geopandas as gpd;
def check_ring_order(poly):
from shapely.geometry import Polygon
if not isinstance(poly, Polygon): return True
ext = poly.exterior
if not ext.is_ccw: return False
for ring in poly.interiors:
if ring.is_ccw: return False
return True
gdf = gpd.read_file('tokyo_buildings_fixed.geojson');
print(gdf['valid_order'] if 'valid_order' in gdf else 'Check manually')
print('All valid order:', all(gdf.geometry.apply(check_ring_order)))
"
Check manually All valid order: True
2:06
Bash
python3 -c "import geopandas as gpd; gdf = gpd.read_file('tokyo_buildings_fixed....
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] Polygon 100 Name: count, dtype: int64
2:08
Bash
# Verify attribute preservation
# Verify attribute preservation
python3 -c "import geopandas as gpd;
old = gpd.read_file('tokyo_buildings_legacy.geojson');
new = gpd.read_file('tokyo_buildings_fixed.geojson');
print('Old columns:', old.columns.tolist());
print('New columns:', new.columns.tolist());
print('Columns match:', old.columns.tolist() == new.columns.tolist());
print('Feature IDs match:', old['feature_id'].tolist() == new['feature_id'].tolist())"
Old columns: ['feature_id', 'overture_id', 'name_primary', 'building_class', 'height', 'geometry'] New columns: ['feature_id', 'overture_id', 'name_primary', 'building_class', 'height', 'geometry'] Columns match: True Feature IDs match: True
2:43