dc-l1-tokyo-ring-orientation

Status: done
Score: 1.000
Duration: 2:44
Cost: 0.28¢
Model: google/gemma-4-26b-a4b-it

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