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": "deepseek/deepseek-v4-flash",
"base_url": "https://openrouter.ai/api/v1"
}
0:03
Let me start by reading the input file to understand its structure and geometry issues.
0:03
Bash
head -c 5000 /work/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
],
[
139.7027971,
35.659202
],
[
139.7027755,
35.6592471
],
[
139.7028864,
35.6592823
],
[
139.7029081,
35.6592372
]
]
]
},
"properties": {
"feature_id": 1,
"overture_id": "01302ced-3141-4b64-b630-98019352a9fd",
"name_primary": "渋谷協和ビル",
"building_class": "",
"height": null
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.7010896,
35.6601981
],
[
139.7010851,
35.660084
],
[
139.7010559,
35.660088
],
[
139.7010511,
35.6600707
],
[
139.7009769,
35.6600834
],
[
139.7009923,
35.6601243
],
[
139.7009007,
35.6601417
],
[
139.7009195,
35.6602237
],
[
139.7010896,
35.6601981
]
]
]
},
"properties": {
"feature_id": 2,
"overture_id": "046ab130-a9a2-4a0e-9142-b7ea36374fdb",
"name_primary": "丸大ビル",
"building_class": "",
"height": null
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.7011168,
35.6616718
],
[
139.7009949,
35.6616685
],
[
139.7009926,
35.6617229
],
[
139.7011145,
35.6617263
],
[
139.7011168,
35.6616718
]
]
]
},
"properties": {
"feature_id": 3,
"overture_id": "063348bb-5559-4839-bf7d-8c6fca154867",
"name_primary": "",
"building_class": "",
"height": null
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.7032678,
35.6601827
],
[
139.7032982,
35.660114
],
[
139.7032205,
35.6600913
],
[
139.7031901,
35.66016
],
[
139.7032678,
35.6601827
]
]
]
},
"properties": {
"feature_id": 4,
"overture_id": "0a1085e0-b946-40d4-8b22-17f5110beee6",
"name_primary": "",
"building_class": "",
"height": null
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.7015768,
35.6592033
],
[
139.701614,
35.6592118
],
[
139.7016492,
35.6591105
],
[
139.7015748,
35.6590934
],
[
139.7015395,
35.6591947
],
[
139.7015768,
35.6592033
]
]
]
},
"properties": {
"feature_id": 5,
"overture_id": "0c2ac2ae-6836-429c-888f-b33dba5cd074",
"name_primary": "",
"building_class": "",
"height": null
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
139.7025726,
35.661211
],
[
139.7025732,
35.6612697
],
[
139.7027844,
35.6612685
],
[
139.7027837,
35.6611824
],
[
139.7025917,
35.6611836
],
[
139.7025916,
35.6611717
],
0:03
Bash
wc -l /work/tokyo_buildings_legacy.geojson
4430 /work/tokyo_buildings_legacy.geojson
0:08
Let me look at the file more carefully to understand the geometry issues. The problem is likely about ring orientation - GeoJSON requires exterior rings to be counter-clockwise and interior rings (holes) to be clockwise. But the description says "building interiors are shaded incorrectly: holes appear filled and exteriors render as empty space" - this sounds like the ring orientation is reversed. Let me look at a few more features and check if there are any features with holes (interior rings).
0:08
Bash
python3 -c "
python3 -c "
import json
with open('/work/tokyo_buildings_legacy.geojson') as f:
data = json.load(f)
print(f'Total features: {len(data[\"features\"])}')
holes = 0
for feat in data['features']:
geom = feat['geometry']
if geom['type'] == 'Polygon' and len(geom['coordinates']) > 1:
holes += 1
print(f'Feature {feat[\"properties\"][\"feature_id\"]}: Polygon with {len(geom[\"coordinates\"])-1} hole(s)')
print(f'Total features with holes: {holes}')
"
Total features: 100 Feature 6: Polygon with 1 hole(s) Feature 18: Polygon with 1 hole(s) Feature 32: Polygon with 1 hole(s) Feature 50: Polygon with 1 hole(s) Feature 74: Polygon with 1 hole(s) Total features with holes: 5
0:16
The user says that the building interiors are shaded incorrectly - holes appear filled and exteriors render as empty space. This is a classic symptom of rings being oriented the wrong way. In GeoJSON, the **exterior ring** of a Polygon must follow a **counterclockwise** order (right-hand rule), and **interior rings** (holes) must follow a **clockwise** order. If the orientation is reversed, the shading (winding rule) will be wrong, and the tile server may reject them as invalid. Let me check the orientation of the existing rings.
0:16
Bash
python3 -c "
python3 -c "
import json
def ring_orientation(ring):
'''Positive area = clockwise (in lon/lat). Since GeoJSON uses [lon, lat],
we compute signed area. Positive = clockwise.'''
area = 0.0
n = len(ring)
for i in range(n):
x1, y1 = ring[i]
x2, y2 = ring[(i+1) % n]
area += (x2 - x1) * (y2 + y1)
return area # positive = clockwise
with open('/work/tokyo_buildings_legacy.geojson') as f:
data = json.load(f)
for feat in data['features']:
geom = feat['geometry']
if geom['type'] == 'Polygon':
rings = geom['coordinates']
e_orient = ring_orientation(rings[0])
print(f'Feature {feat[\"properties\"][\"feature_id\"]}: exterior_ring_orient={\"CW\" if e_orient > 0 else \"CCW\"} (area_signed={e_orient:.6e})')
for i, ring in enumerate(rings[1:], 1):
h_orient = ring_orientation(ring)
print(f' Hole {i}: orient={\"CW\" if h_orient > 0 else \"CCW\"} (area_signed={h_orient:.6e})')
"
Feature 1: exterior_ring_orient=CW (area_signed=1.153185e-08) Feature 2: exterior_ring_orient=CW (area_signed=3.896285e-08) Feature 3: exterior_ring_orient=CW (area_signed=1.329032e-08) Feature 4: exterior_ring_orient=CW (area_signed=1.205614e-08) Feature 5: exterior_ring_orient=CW (area_signed=1.629199e-08) Feature 6: exterior_ring_orient=CW (area_signed=4.653684e-08) Hole 1: orient=CCW (area_signed=-1.647870e-10) Feature 7: exterior_ring_orient=CW (area_signed=3.059120e-09) Feature 8: exterior_ring_orient=CW (area_signed=7.865950e-09) Feature 9: exterior_ring_orient=CW (area_signed=3.122250e-09) Feature 10: exterior_ring_orient=CW (area_signed=1.377381e-08) Feature 11: exterior_ring_orient=CW (area_signed=1.889582e-08) Feature 12: exterior_ring_orient=CW (area_signed=2.188700e-08) Feature 13: exterior_ring_orient=CW (area_signed=1.845470e-09) Feature 14: exterior_ring_orient=CW (area_signed=3.661791e-08) Feature 15: exterior_ring_orient=CW (area_signed=4.687659e-08) Feature 16: exterior_ring_orient=CW (area_signed=3.560065e-08) Feature 17: exterior_ring_orient=CW (area_signed=5.643198e-08) Feature 18: exterior_ring_orient=CW (area_signed=5.041214e-08) Hole 1: orient=CCW (area_signed=-1.818460e-10) Feature 19: exterior_ring_orient=CW (area_signed=2.864750e-08) Feature 20: exterior_ring_orient=CW (area_signed=1.224049e-08) Feature 21: exterior_ring_orient=CW (area_signed=2.294071e-07) Feature 22: exterior_ring_orient=CW (area_signed=5.251830e-09) Feature 23: exterior_ring_orient=CW (area_signed=2.467901e-08) Feature 24: exterior_ring_orient=CW (area_signed=5.766456e-08) Feature 25: exterior_ring_orient=CW (area_signed=2.725276e-08) Feature 26: exterior_ring_orient=CW (area_signed=6.605934e-08) Feature 27: exterior_ring_orient=CW (area_signed=1.787730e-08) Feature 28: exterior_ring_orient=CW (area_signed=5.242020e-09) Feature 29: exterior_ring_orient=CW (area_signed=1.816893e-08) Feature 30: exterior_ring_orient=CW (area_signed=2.418654e-08) Feature 31: exterior_ring_orient=CW (area_signed=6.073671e-08) Feature 32: exterior_ring_orient=CW (area_signed=4.110060e-09) Hole 1: orient=CCW (area_signed=-1.533555e-11) Feature 33: exterior_ring_orient=CW (area_signed=1.873819e-08) Feature 34: exterior_ring_orient=CW (area_signed=1.108260e-08) Feature 35: exterior_ring_orient=CW (area_signed=7.398394e-08) Feature 36: exterior_ring_orient=CW (area_signed=3.922542e-08) Feature 37: exterior_ring_orient=CW (area_signed=4.910509e-08) Feature 38: exterior_ring_orient=CW (area_signed=3.598451e-08) Feature 39: exterior_ring_orient=CW (area_signed=5.810154e-08) Feature 40: exterior_ring_orient=CW (area_signed=5.242107e-07) Feature 41: exterior_ring_orient=CW (area_signed=2.811194e-07) Feature 42: exterior_ring_orient=CW (area_signed=5.065018e-08) Feature 43: exterior_ring_orient=CW (area_signed=1.051100e-07) Feature 44: exterior_ring_orient=CW (area_signed=4.491215e-08) Feature 45: exterior_ring_orient=CW (area_signed=7.152400e-09) Feature 46: exterior_ring_orient=CW (area_signed=6.316450e-09) Feature 47: exterior_ring_orient=CW (area_signed=5.498810e-09) Feature 48: exterior_ring_orient=CW (area_signed=4.004977e-08) Feature 49: exterior_ring_orient=CW (area_signed=4.997069e-08) Feature 50: exterior_ring_orient=CW (area_signed=2.276902e-08) Hole 1: orient=CCW (area_signed=-8.457090e-11) Feature 51: exterior_ring_orient=CW (area_signed=5.784720e-09) Feature 52: exterior_ring_orient=CW (area_signed=1.666942e-08) Feature 53: exterior_ring_orient=CW (area_signed=3.310697e-08) Feature 54: exterior_ring_orient=CW (area_signed=1.288763e-07) Feature 55: exterior_ring_orient=CW (area_signed=5.151547e-08) Feature 56: exterior_ring_orient=CW (area_signed=1.220165e-07) Feature 57: exterior_ring_orient=CW (area_signed=5.249670e-09) Feature 58: exterior_ring_orient=CW (area_signed=5.643610e-09) Feature 59: exterior_ring_orient=CW (area_signed=1.313053e-08) Feature 60: exterior_ring_orient=CW (area_signed=1.011662e-08) Feature 61: exterior_ring_orient=CW (area_signed=6.194590e-09) Feature 62: exterior_ring_orient=CW (area_signed=7.050366e-08) Feature 63: exterior_ring_orient=CW (area_signed=8.627820e-09) Feature 64: exterior_ring_orient=CW (area_signed=2.950470e-09) Feature 65: exterior_ring_orient=CW (area_signed=1.445278e-07) Feature 66: exterior_ring_orient=CW (area_signed=9.936860e-09) Feature 67: exterior_ring_orient=CW (area_signed=2.596178e-08) Feature 68: exterior_ring_orient=CW (area_signed=2.592963e-08) Feature 69: exterior_ring_orient=CW (area_signed=2.506424e-08) Feature 70: exterior_ring_orient=CW (area_signed=5.128440e-09) Feature 71: exterior_ring_orient=CW (area_signed=2.915180e-09) Feature 72: exterior_ring_orient=CW (area_signed=1.145450e-08) Feature 73: exterior_ring_orient=CW (area_signed=6.366571e-08) Feature 74: exterior_ring_orient=CW (area_signed=1.746860e-08) Hole 1: orient=CCW (area_signed=-8.483500e-11) Feature 75: exterior_ring_orient=CW (area_signed=1.858974e-08) Feature 76: exterior_ring_orient=CW (area_signed=2.232011e-07) Feature 77: exterior_ring_orient=CW (area_signed=1.436443e-07) Feature 78: exterior_ring_orient=CW (area_signed=2.075014e-07) Feature 79: exterior_ring_orient=CW (area_signed=4.681839e-08) Feature 80: exterior_ring_orient=CW (area_signed=7.943220e-08) Feature 81: exterior_ring_orient=CW (area_signed=1.335898e-08) Feature 82: exterior_ring_orient=CW (area_signed=5.007120e-09) Feature 83: exterior_ring_orient=CW (area_signed=3.890324e-08) Feature 84: exterior_ring_orient=CW (area_signed=2.952526e-08) Feature 85: exterior_ring_orient=CW (area_signed=2.851800e-08) Feature 86: exterior_ring_orient=CW (area_signed=2.276762e-07) Feature 87: exterior_ring_orient=CW (area_signed=1.859954e-08) Feature 88: exterior_ring_orient=CW (area_signed=1.092956e-07) Feature 89: exterior_ring_orient=CW (area_signed=7.064530e-08) Feature 90: exterior_ring_orient=CW (area_signed=1.069315e-08) Feature 91: exterior_ring_orient=CW (area_signed=4.332092e-08) Feature 92: exterior_ring_orient=CW (area_signed=1.844615e-07) Feature 93: exterior_ring_orient=CW (area_signed=4.075751e-08) Feature 94: exterior_ring_orient=CW (area_signed=3.568666e-08) Feature 95: exterior_ring_orient=CW (area_signed=2.096313e-08) Feature 96: exterior_ring_orient=CW (area_signed=4.817520e-09) Feature 97: exterior_ring_orient=CW (area_signed=1.447988e-07) Feature 98: exterior_ring_orient=CW (area_signed=4.382140e-09) Feature 99: exterior_ring_orient=CW (area_signed=1.659407e-07) Feature 100: exterior_ring_orient=CW (area_signed=6.495338e-08)
0:28
Now I can see the problem! The GeoJSON specification (RFC 7946) requires:
- **Exterior rings**: counterclockwise order (CCW)
- **Interior rings (holes)**: clockwise order (CW)
But this data has:
- **Exterior rings**: clockwise (CW) — WRONG, should be CCW
- **Interior rings (holes)**: counterclockwise (CCW) — WRONG, should be CW
This is the classic "ring orientation" problem that causes rendering engines to display polygons incorrectly (interiors vs exteriors swapped).
The fix is simple: reverse the ring orientation. For each polygon, reverse the coordinate order of every ring (exterior and all holes).
Let me verify my understanding. In the signed area formula I used (the shoelace formula variant), positive = CW for [lon, lat] coordinates. Let me double check...
Actually, my ring_orientation function uses:
```python
area += (x2 - x1) * (y2 + y1)
```
This is a variant of the shoelace formula. For the standard shoelace formula:
area = 0.5 * sum(x_i * y_{i+1} - x_{i+1} * y_i)
Positive area = CCW (counter-clockwise) in standard math coordinates.
But in screen/web coordinates where y increases downward, positive area = CW.
For geographic coordinates (lon, lat), the standard convention is that positive area = CCW (this is the right-hand rule). So my function needs to use the proper shoelace formula.
Let me reconsider. The formula I used: `(x2 - x1) * (y2 + y1)` is actually related to the trapezoid formula. For the shoelace formula: sum(x_i * y_{i+1} - x_{i+1} * y_i). In a standard Cartesian plane, positive = CCW.
In geographic coordinates [lon, lat], the right-hand rule says positive area = CCW. But GeoJSON RFC 7946 says exterior rings must be CCW (positive shoelace area) and holes must be CW (negative shoelace area).
Since I already computed that all exterior rings have "positive signed area" with my formula, and all holes have negative... let me check if my formula gives the same sign as the shoelace formula.
For a simple polygon: [(0,0), (1,0), (1,1), (0,1), (0,0)] — CCW
Shoelace: 0*0 + 1*1 + 1*1 + 0*0 - (0*1 + 0*1 + 1*0 + 1*0) = 0+1+1+0-(0+0+0+0) = 2. Positive = CCW.
My formula: (1-0)*(0+0) + (1-1)*(0+1) + (0-1)*(1+1) + (0-0)*(1+0) = 0 + 0 + (-2) + 0 = -2. Negative for CCW.
So my formula gives negative for CCW. That means:
- Exterior: my formula gives positive → CW → but should be CCW → needs reversing
- Holes: my formula gives negative → CCW → but should be CW → needs reversing
So the fix is: reverse all rings. Simple enough. Let me write the script.
Actually wait, let me re-examine. For the orientation of the ring as stored in the file, let me check a simple case:
Take feature 1: exterior ring coords:
[139.7029081, 35.6592372],
[139.7027971, 35.659202],
[139.7027755, 35.6592471],
[139.7028864, 35.6592823],
[139.7029081, 35.6592372]
Let me verify the shoelace:
sum(x_i * y_{i+1}):
139.7029081 * 35.659202 = 4980.808...
139.7027971 * 35.6592471 = 4980.795...
139.7027755 * 35.6592823 = 4980.786...
139.7028864 * 35.6592372 = 4980.801...
sum(y_i * x_{i+1}):
35.6592372 * 139.7027971 = 4980.807...
35.659202 * 139.7027755 = 4980.790...
35.6592471 * 139.7028864 = 4980.806...
35.6592823 * 139.7029081 = 4980.808...
The difference would be very small. Let me just go with the analysis above - all exterior rings are CW (wrong per GeoJSON spec), all interior rings are CCW (wrong per GeoJSON spec). The fix is to reverse every ring's coordinate order.
Let me write a clean solution.
0:28
0:28
Write
/work/solve.py
content · 31 lines · py
import json
# Read the legacy file
with open('/work/tokyo_buildings_legacy.geojson', 'r') as f:
data = json.load(f)
fixed_count = 0
for feat in data['features']:
geom = feat['geometry']
if geom['type'] != 'Polygon':
continue
rings = geom['coordinates']
if not rings:
continue
fixed_rings = []
for ring in rings:
# Reverse the coordinate order of each ring.
# This flips CW → CCW for exteriors and CCW → CW for holes,
# conforming to RFC 7946.
fixed_rings.append(list(reversed(ring)))
geom['coordinates'] = fixed_rings
fixed_count += 1
# Write the fixed output (preserving all attributes)
with open('/work/tokyo_buildings_fixed.geojson', 'w') as f:
json.dump(data, f, ensure_ascii=False)
print(f"Fixed {fixed_count} features. Output written to tokyo_buildings_fixed.geojson")
Wrote /work/solve.py (873 chars).
0:32
Wait - I should think about this more carefully. The current situation is: - Exterior rings: all CW (positive signed area) - Interior rings (holes): all CCW (negative signed area) The GeoJSON RFC 7946 says: - Exterior rings must be **counterclockwise** (CCW) - Interior rings (holes) must be **clockwise** (CW) So if I simply reverse every ring, then: - Exteriors: CW → CCW ✓ (correct) - Holes: CCW → CW ✓ (correct) That's exactly what we need! Let me verify the fix by running the script and checking.
0:32
Bash
cd /work && python solve.py
Fixed 100 features. Output written to tokyo_buildings_fixed.geojson
0:38
Let me verify the fix by checking the ring orientations in the output file.
0:38
Bash
python3 -c "
python3 -c "
import json
def ring_orientation(ring):
area = 0.0
n = len(ring)
for i in range(n):
x1, y1 = ring[i]
x2, y2 = ring[(i+1) % n]
area += (x2 - x1) * (y2 + y1)
return area # positive = CW, negative = CCW
with open('/work/tokyo_buildings_fixed.geojson') as f:
data = json.load(f)
print(f'Total features: {len(data[\"features\"])}')
holes = 0
orient_ok = 0
for feat in data['features']:
geom = feat['geometry']
if geom['type'] == 'Polygon':
rings = geom['coordinates']
e_orient = ring_orientation(rings[0])
e_cw = e_orient > 0
# Per RFC 7946: exterior = CCW, interior = CW
exterior_good = not e_cw # CCW is good
holes_good = True
for ring in rings[1:]:
h_orient = ring_orientation(ring)
h_cw = h_orient > 0
if not h_cw: # holes should be CW
holes_good = False
holes += 1
if exterior_good and holes_good:
orient_ok += 1
if not exterior_good or not holes_good:
print(f'Feature {feat[\"properties\"][\"feature_id\"]}: exterior_CCW={exterior_good}, holes_CW={holes_good}')
print(f'Features with holes: {len([f for f in data[\"features\"] if len(f[\"geometry\"][\"coordinates\"]) > 1])}')
print(f'Orientations correct: {orient_ok}/{len(data[\"features\"])}')
"
Total features: 100 Features with holes: 5 Orientations correct: 100/100
0:43
All 100 features now have correct ring orientations per RFC 7946. Let me also verify that attributes are untouched and check a hole-bearing feature to make sure holes are preserved.
0:43
0:43
Bash
python3 -c "
python3 -c "
import json
with open('/work/tokyo_buildings_legacy.geojson') as f:
orig = json.load(f)
with open('/work/tokyo_buildings_fixed.geojson') as f:
fixed = json.load(f)
# Check attributes are untouched
for o, f in zip(orig['features'], fixed['features']):
if o['properties'] != f['properties']:
print(f'Mismatch at feature {o[\"properties\"][\"feature_id\"]}')
break
else:
print('All attributes preserved perfectly.')
# Check hole structures preserved
for o, f in zip(orig['features'], fixed['features']):
orig_rings = o['geometry']['coordinates']
fixed_rings = f['geometry']['coordinates']
if len(orig_rings) != len(fixed_rings):
print(f'Hole count mismatch at feature {o[\"properties\"][\"feature_id\"]}')
break
# Verify same number of vertices per ring (just reversed)
for i, (or_ring, fr_ring) in enumerate(zip(orig_rings, fixed_rings)):
if len(or_ring) != len(fr_ring):
print(f'Vertex count mismatch at feature {o[\"properties\"][\"feature_id\"]} ring {i}')
break
# Check that fixed ring is the reverse of original
if fr_ring != list(reversed(or_ring)):
print(f'Ring {i} of feature {o[\"properties\"][\"feature_id\"]} is not simply reversed')
break
else:
print('All hole structures and vertex counts preserved.')
# Verify CRS and feature count match
print(f'CRS: {fixed.get(\"crs\")}')
print(f'Feature count: {len(fixed[\"features\"])}')
"
All attributes preserved perfectly.
All hole structures and vertex counts preserved.
CRS: {'type': 'name', 'properties': {'name': 'urn:ogc:def:crs:OGC:1.3:CRS84'}}
Feature count: 100
0:47
All checks pass. Let me verify the output file exists and has valid JSON.
0:47
Bash
ls -lh /work/tokyo_buildings_fixed.geojson && python3 -c "import json; json.load...
-rw-r--r-- 1 agent agent 41K Jun 17 23:43 /work/tokyo_buildings_fixed.geojson Valid JSON
0:55
Everything checks out perfectly. Let me summarize what was done.
0:55