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 -l
ls -l
total 92 -rw-r--r-- 1 agent agent 91050 Jun 15 23:03 tokyo_buildings_legacy.geojson
0:03
Bash
head -n 20 tokyo_buildings_legacy.geojson
head -n 20 tokyo_buildings_legacy.geojson
{
"type": "FeatureCollection",
"name": "tokyo_buildings_legacy",
"crs": {
"type": "name",
"properties": {
"name": "urn:ogc:def:crs:OGC:1.3:CRS84"
}
},
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.7029081,
35.6592372
],
0:20
Write
/work/solve.py
content · 77 lines · py
import geopandas as gpd
import json
def check_geometry(feature):
geom = feature['geometry']
if geom['type'] != 'Polygon':
return False
coords = geom['coordinates']
if len(coords) < 1:
return False
# The first ring is the exterior
exterior = coords[0]
if len(exterior) < 4: # Minimum 4 points for a closed ring (including repeat)
return False
# Check if exterior is clockwise or counter-clockwise
# Standard GeoJSON (RFC 7946) expects exterior to be counter-clockwise
# and holes to be clockwise.
# However, many tools use different conventions.
# If holes appear filled and exteriors empty, it's likely the winding orders are swapped.
# Let's use Shapely to fix winding order and validity.
return True
def fix_geometries():
print("Loading file...")
gdf = gpd.read_file('tokyo_buildings_legacy.geojson')
print(f"Loaded {len(gdf)} features.")
# The issue "holes appear filled and exteriors render as empty space"
# strongly suggests the winding order of the rings is incorrect according to RFC 7946.
# RFC 7946: Exterior ring should be counter-clockwise (CCW), interior rings clockwise (CW).
# Let's use shapely to ensure valid geometries and correct orientation.
from shapely.geometry import shape, mapping
from shapely.ops import orient
fixed_features = []
print("Fixing geometries...")
for _, row in gdf.iterrows():
geom = shape(row.geometry)
# orient(geom, sign=1.0) ensures the exterior is CCW and holes are CW
# This follows the RFC 7946 standard.
fixed_geom = orient(geom, sign=1.0)
# Create a new feature dictionary to preserve attributes
feature = {
"type": "Feature",
"properties": row.drop('geometry').to_dict(),
"geometry": mapping(fixed_geom)
}
fixed_features.append(feature)
# Reconstruct FeatureCollection
output = {
"type": "FeatureCollection",
"features": fixed_features
}
# Note: gdf.to_file might lose some exact property types if not careful,
# but standard GeoJSON features should be fine.
# However, to be safe and strictly preserve attributes, we use the dictionary approach.
print("Saving to tokyo_buildings_fixed.geojson...")
with open('tokyo_buildings_fixed.geojson', 'w') as f:
json.dump(output, f)
print("Done.")
if __name__ == "__main__":
fix_geometries()
Wrote /work/solve.py (2562 chars).
0:22
Bash
python solve.py
python solve.py
Loading file... Loaded 100 features. Fixing geometries... Saving to tokyo_buildings_fixed.geojson... Done.
0:30
Bash
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d1 = json.lo...
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d1 = json.load(f); f.close(); f = open('tokyo_buildings_fixed.geojson'); d2 = json.load(f); f.close(); print('Legacy ring 0:', d1['features'][0]['geometry']['coordinates'][0][0]); print('Fixed ring 0:', d2['features'][0]['geometry']['coordinates'][0][0]);"
Legacy ring 0: [139.7029081, 35.6592372] Fixed ring 0: [139.7029081, 35.6592372]
0:36
Bash
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d1 = json.lo...
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d1 = json.load(f); f.close(); f = open('tokyo_buildings_fixed.geojson'); d2 = json.load(f); f.close();
def ring_area(ring):
area = 0
for i in range(len(ring) - 1):
area += (ring[i][0] * ring[i+1][1]) - (ring[i+1][0] * ring[i][1])
return area
print('Legacy exterior area:', ring_area(d1['features'][0]['geometry']['coordinates'][0]));
print('Fixed exterior area:', ring_area(d2['features'][0]['geometry']['coordinates'][0]));
"
Legacy exterior area: -1.153239281848073e-08 Fixed exterior area: 1.153239281848073e-08
0:51
Bash
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d1 = json.lo...
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d1 = json.load(f); f.close(); f = open('tokyo_buildings_fixed.geojson'); d2 = json.load(f); f.close();
print('Legacy number of rings in feature 0:', len(d1['features'][0]['geometry']['coordinates']));
print('Fixed number of rings in feature 0:', len(d2['features'][0]['geometry']['coordinates']));
"
Legacy number of rings in feature 0: 1 Fixed number of rings in feature 0: 1
0:54
Bash
# Let's find a feature that actually has a hole in the legacy file.
# Let's find a feature that actually has a hole in the legacy file.
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d = json.load(f); f.close();
for i, feat in enumerate(d['features']):
if len(feat['geometry']['coordinates']) > 1:
print(f'Found feature with holes at index {i}');
print(f'Number of rings: {len(feat[\"geometry\"][\"coordinates\"])};');
break;
else:
print('No features with holes found in legacy file.')"
Found feature with holes at index 5 Number of rings: 2;
1:01
Bash
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d = json.loa...
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d = json.load(f); f.close(); feat = d['features'][5];
def ring_area(ring):
area = 0
for i in range(len(ring) - 1):
area += (ring[i][0] * ring[i+1][1]) - (ring[i+1][0] * ring[i][1])
return area
print('Legacy exterior area:', ring_area(feat['geometry']['coordinates'][0]));
print('Legacy interior area:', ring_area(feat['geometry']['coordinates'][1]));
f = open('tokyo_buildings_fixed.geojson'); d2 = json.load(f); f.close(); feat2 = d2['features'][5];
print('Fixed exterior area:', ring_area(feat2['geometry']['coordinates'][0]));
print('Fixed interior area:', ring_area(feat2['geometry']['coordinates'][1]));
"
Legacy exterior area: -4.653702490031719e-08 Legacy interior area: 1.6370904631912708e-10 Fixed exterior area: 4.653702490031719e-08 Fixed interior area: -1.6370904631912708e-10
1:06
(no output)
Bash
# Check if there are any features that are NOT polygons (e.g. MultiPolygons)
# Check if there are any features that are NOT polygons (e.g. MultiPolygons)
# and check for invalidity.
python -c "import json; from shapely.geometry import shape; f = open('tokyo_buildings_legacy.geojson'); d = json.load(f); f.close();
for i, feat in enumerate(d['features']):
geom = shape(feat['geometry'])
if not geom.is_valid:
print(f'Feature {i} is invalid: {geom.reason}')
if feat['geometry']['type'] != 'Polygon':
print(f'Feature {i} is {feat[\"geometry\"][\"type\"]}')
"
1:21
Bash
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d = json.loa...
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d = json.load(f); f.close(); feat = d['features'][5];
print('Legacy exterior area:', feat['geometry']['coordinates'][0]);
print('Legacy interior area:', feat['geometry']['coordinates'][1]);
f = open('tokyo_buildings_fixed.geojson'); d2 = json.load(f); f.close(); feat2 = d2['features'][5];
print('Fixed exterior area:', feat2['geometry']['coordinates'][0]);
print('Fixed interior area:', feat2['geometry']['coordinates'][1]);
"
Legacy exterior area: [[139.7025726, 35.661211], [139.7025732, 35.6612697], [139.7027844, 35.6612685], [139.7027837, 35.6611824], [139.7025917, 35.6611836], [139.7025916, 35.6611717], [139.7024481, 35.6611726], [139.7024485, 35.6612117], [139.7025726, 35.661211]] Legacy interior area: [[139.70261625, 35.6612158], [139.70263306500001, 35.6612158], [139.70263306500001, 35.6612207], [139.70261625, 35.6612207], [139.70261625, 35.6612158]] Fixed exterior area: [[139.7025726, 35.661211], [139.7024485, 35.6612117], [139.7024481, 35.6611726], [139.7025916, 35.6611717], [139.7025917, 35.6611836], [139.7027837, 35.6611824], [139.7027844, 35.6612685], [139.7025732, 35.6612697], [139.7025726, 35.661211]] Fixed interior area: [[139.70261625, 35.6612158], [139.70261625, 35.6612207], [139.70263306500001, 35.6612207], [139.70263306500001, 35.6612158], [139.70261625, 35.6612158]]
1:26
Bash
# Check if the first feature's attributes are preserved.
# Check if the first feature's attributes are preserved.
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d1 = json.load(f); f.close(); f = open('tokyo_buildings_fixed.geojson'); d2 = json.load(f); f.close(); print('Legacy props:', d1['features'][0]['properties']); print('Fixed props:', d2['features'][0]['properties']);"
Legacy props: {'feature_id': 1, 'overture_id': '01302ced-3141-4b64-b630-98019352a9fd', 'name_primary': '渋谷協和ビル', 'building_class': '', 'height': None}
Fixed props: {'feature_id': 1, 'overture_id': '01302ced-3141-4b64-b630-98019352a9fd', 'name_primary': '渋谷協和ビル', 'building_class': '', 'height': nan}
1:43
Write
/work/solve.py
content · 57 lines · py
import geopandas as gpd
import json
from shapely.geometry import shape, mapping
from shapely.ops import orient
import numpy as np
def fix_geometries():
print("Loading file...")
# Using standard json load to avoid geopandas type conversions issues for attributes
with open('tokyo_buildings_legacy.geojson', 'r') as f:
data = json.load(f)
print(f"Loaded {len(data['features'])} features.")
fixed_features = []
print("Fixing geometries...")
for feat in data['features']:
# Keep properties as they are, but we need to handle NaN if we converted to DF
# Since we are reading from JSON directly, they should stay as is.
props = feat['properties']
# Fix geometry
geom = shape(feat['geometry'])
# orient(geom, sign=1.0) ensures exterior is CCW and holes are CW
# This follows RFC 7946 standard.
fixed_geom = orient(geom, sign=1.0)
new_feat = {
"type": "Feature",
"properties": props,
"geometry": mapping(fixed_geom)
}
fixed_features.append(new_feat)
# Reconstruct FeatureCollection
output = {
"type": "FeatureCollection",
"features": fixed_features
}
# If the original file had a name or CRS, preserve them if possible
if 'name' in data:
output['name'] = data['name']
if 'crs' in data:
output['crs'] = data['crs']
print("Saving to tokyo_buildings_fixed.geojson...")
with open('tokyo_buildings_fixed.geojson', 'w') as f:
json.dump(output, f)
print("Done.")
if __name__ == "__main__":
fix_geometries()
Wrote /work/solve.py (1692 chars).
1:45
Bash
python solve.py
python solve.py
Loading file... Loaded 100 features. Fixing geometries... Saving to tokyo_buildings_fixed.geojson... Done.
1:51
Bash
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d1 = json.lo...
python -c "import json; f = open('tokyo_buildings_legacy.geojson'); d1 = json.load(f); f.close(); f = open('tokyo_buildings_fixed.geojson'); d2 = json.load(f); f.close(); print('Legacy props:', d1['features'][0]['properties']); print('Fixed props:', d2['features'][0]['properties']);"
Legacy props: {'feature_id': 1, 'overture_id': '01302ced-3141-4b64-b630-98019352a9fd', 'name_primary': '渋谷協和ビル', 'building_class': '', 'height': None}
Fixed props: {'feature_id': 1, 'overture_id': '01302ced-3141-4b64-b630-98019352a9fd', 'name_primary': '渋谷協和ビル', 'building_class': '', 'height': None}
2:50