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:02
Let me start by examining the input file to understand the structure and identify the issues.
0:02
Read
/work/tokyo_buildings_legacy.geojson
1 {
2 "type": "FeatureCollection",
3 "name": "tokyo_buildings_legacy",
4 "crs": {
5 "type": "name",
6 "properties": {
7 "name": "urn:ogc:def:crs:OGC:1.3:CRS84"
8 }
9 },
10 "features": [
11 {
12 "type": "Feature",
13 "geometry": {
14 "type": "Polygon",
15 "coordinates": [
16 [
17 [
18 139.7029081,
19 35.6592372
20 ],
21 [
22 139.7027971,
23 35.659202
24 ],
25 [
26 139.7027755,
27 35.6592471
28 ],
29 [
30 139.7028864,
31 35.6592823
32 ],
33 [
34 139.7029081,
35 35.6592372
36 ]
37 ]
38 ]
39 },
40 "properties": {
41 "feature_id": 1,
42 "overture_id": "01302ced-3141-4b64-b630-98019352a9fd",
43 "name_primary": "渋谷協和ビル",
44 "building_class": "",
45 "height": null
46 }
47 },
48 {
49 "type": "Feature",
50 "geometry": {
51 "type": "Polygon",
52 "coordinates": [
53 [
54 [
55 139.7010896,
56 35.6601981
57 ],
58 [
59 139.7010851,
60 35.660084
61 ],
62 [
63 139.7010559,
64 35.660088
65 ],
66 [
67 139.7010511,
68 35.6600707
69 ],
70 [
71 139.7009769,
72 35.6600834
73 ],
74 [
75 139.7009923,
76 35.6601243
77 ],
78 [
79 139.7009007,
80 35.6601417
81 ],
82 [
83 139.7009195,
84 35.6602237
85 ],
86 [
87 139.7010896,
88 35.6601981
89 ]
90 ]
91 ]
92 },
93 "properties": {
94 "feature_id": 2,
95 "overture_id": "046ab130-a9a2-4a0e-9142-b7ea36374fdb",
96 "name_primary": "丸大ビル",
97 "building_class": "",
98 "height": null
99 }
100 },
<system-reminder>Showed lines 1-100 of 4430. Re-call Read with offset=101 to continue, or raise limit.</system-reminder>
0:25
0:25
Bash
cd /work && python -c "
cd /work && python -c "
import json
with open('tokyo_buildings_legacy.geojson') as f:
data = json.load(f)
print('Total features:', len(data['features']))
# Check for MultiPolygon
multi = [f for f in data['features'] if f['geometry']['type'] != 'Polygon']
print('Non-Polygon:', len(multi), [f['geometry']['type'] for f in multi[:5]])
# Check for features with holes (interior rings)
with_holes = []
for f in data['features']:
rings = f['geometry']['coordinates']
if len(rings) > 1:
with_holes.append(f['properties'].get('feature_id'))
print('Features with holes:', len(with_holes), with_holes[:10])
# Print a sample feature with holes
for f in data['features']:
if len(f['geometry']['coordinates']) > 1:
print(json.dumps(f, indent=2)[:2000])
break
"
Total features: 100
Non-Polygon: 0 []
Features with holes: 5 [6, 18, 32, 50, 74]
{
"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
],
[
139.7024481,
35.6611726
],
[
139.7024485,
35.6612117
],
[
139.7025726,
35.661211
]
],
[
[
139.70261625,
35.6612158
],
[
139.70263306500001,
35.6612158
],
[
139.70263306500001,
35.6612207
],
[
139.70261625,
35.6612207
],
[
139.70261625,
35.6612158
]
]
]
},
"properties": {
"feature_id": 6,
"overture_id": "0ff95784-5c37-4c0e-8ee3-674f96075b8c",
"name_primary": "",
"building_class": "",
"height": null
}
}
0:30
Let me look at the issue more carefully. The problem is likely with the winding order (orientation) of the rings. In GeoJSON, the exterior ring of a polygon must be counter-clockwise (right-hand rule / WindingRing) and interior rings (holes) must be clockwise. Many validators and renderers check this. Let me check the orientation of the rings.
0:30
Bash
cd /work && python -c "
cd /work && python -c "
import json
from shapely.geometry import shape
from shapely.validation import explain_validity
with open('tokyo_buildings_legacy.geojson') as f:
data = json.load(f)
# Check orientation of exterior rings
issues = []
for i, feat in enumerate(data['features']):
try:
s = shape(feat['geometry'])
except Exception as e:
issues.append((feat['properties']['feature_id'], str(e)))
continue
valid = s.is_valid
if not valid:
issues.append((feat['properties']['feature_id'], explain_validity(s)))
print(f'Invalid features: {len(issues)}')
for fid, msg in issues[:20]:
print(f' feature_id={fid}: {msg}')
"
Invalid features: 0
0:37
Interesting - shapely says they're all valid but the user says web-based GeoJSON viewers render them incorrectly. The issue is likely ring orientation. GeoJSON standard (RFC 7946) requires that exterior rings follow the **Right-Hand Rule** (counterclockwise winding), and interior rings (holes) must be clockwise. Some viewers handle winding direction strictly. Let me check the ring orientations.
0:37
Bash
cd /work && python -c "
cd /work && python -c "
import json
from shapely.geometry import shape, Polygon
from shapely import wkt
with open('tokyo_buildings_legacy.geojson') as f:
data = json.load(f)
def ring_orientation(ring):
"""Returns 'cw' or 'ccw' for a ring (list of coords)."""
# Shoelace formula - positive area for CCW in standard lon/lat plane
area = 0.0
n = len(ring)
for i in range(n - 1):
x1, y1 = ring[i]
x2, y2 = ring[i+1]
area += (x2 - x1) * (y2 + y1)
# For geographic coordinates (lon/lat), positive area means CW?
# Actually in math, positive signed area = CCW on Cartesian plane.
# But for lon/lat it's slightly different but consistent.
if area > 0:
return 'ccw'
elif area < 0:
return 'cw'
else:
return 'degenerate'
for feat in data['features']:
fid = feat['properties']['feature_id']
coords = feat['geometry']['coordinates']
for ring_idx, ring in enumerate(coords):
orient = ring_orientation(ring)
label = 'exterior' if ring_idx == 0 else f'hole #{ring_idx}'
expected = 'ccw' if ring_idx == 0 else 'cw'
if orient != expected:
print(f'feature_id={fid}: {label} is {orient} (expected {expected})')
"
bash: -c: line 10: syntax error near unexpected token `('
bash: -c: line 10: ` """Returns 'cw' or 'ccw' for a ring (list of coords)."""'
0:46
The heredoc approach with Python multi-line strings has issues. Let me write a script file instead.
0:46
Write
/work/check_orient.py
content · 44 lines · py
import json
from shapely.geometry import shape, Polygon
with open('tokyo_buildings_legacy.geojson') as f:
data = json.load(f)
def ring_orientation(ring):
"""Returns 'cw' or 'ccw' for a ring (list of coords)."""
area = 0.0
n = len(ring)
for i in range(n - 1):
x1, y1 = ring[i]
x2, y2 = ring[i+1]
area += (x2 - x1) * (y2 + y1)
if area > 0:
return 'ccw'
elif area < 0:
return 'cw'
else:
return 'degenerate'
issues = []
all_feats = []
for feat in data['features']:
fid = feat['properties']['feature_id']
coords = feat['geometry']['coordinates']
for ring_idx, ring in enumerate(coords):
orient = ring_orientation(ring)
label = 'exterior' if ring_idx == 0 else f'interior(hole) #{ring_idx}'
expected = 'ccw' if ring_idx == 0 else 'cw'
if orient != expected:
issues.append((fid, label, orient, expected))
print(f"Features with wrong ring orientation: {len(set(i[0] for i in issues))}")
for fid, label, orient, expected in issues:
print(f" feature_id={fid}: {label} is {orient} (expected {expected})")
# Also check for self-intersections
print("\nChecking validity...")
for feat in data['features']:
s = shape(feat['geometry'])
if not s.is_valid:
print(f" feature_id={feat['properties']['feature_id']}: INVALID - {s.explain_validity()}")
print("Done checking.")
Wrote /work/check_orient.py (1416 chars).
0:48
Let me run the check script, but I need to be careful about the syntax.
0:48
Bash
cd /work && python check_orient.py
Features with wrong ring orientation: 0 Checking validity... Done checking.
1:19
Hmm, all features are valid and all rings have correct orientation. But the user says the GeoJSON renders incorrectly with holes filled and exteriors rendered as empty space. And the tile-server import script rejects every feature with geometry warnings.
Let me think about common GeoJSON issues:
1. The CRS definition - GeoJSON officially should be EPSG:4326 (WGS84) without any CRS specification. The file has a `crs` property which says `urn:ogc:def:crs:OGC:1.3:CRS84`. However, this is actually the correct CRS for GeoJSON (WGS84). Actually, strict GeoJSON specification says CRS should not be specified because it MUST be WGS84. But this shouldn't cause rendering issues.
2. Maybe the coordinates are in reverse order? Some systems use (lat, lon) instead of (lon, lat). Let me check the coordinate range.
3. Maybe there's a precision issue or the ring closure is not exact.
4. Let me look more carefully at the data. Wait, shapely says everything is valid, but the user's tile-server import script rejects features with geometry warnings. This might be about the winding order. Let me double-check.
Actually, let me look at the GeoJSON specification for Polygon more carefully. Per RFC 7946 (the GeoJSON standard), for Polygons:
- The exterior ring MUST follow the right-hand rule (counterclockwise when viewed from above)
- Interior rings (holes) MUST follow the left-hand rule (clockwise when viewed from above)
- This is the opposite of what I checked above!
Wait, RFC 7946 says: "A linear ring MUST follow the right-hand rule with respect to the area it bounds, i.e., exterior rings are counterclockwise, and holes are clockwise."
Actually checking: "right-hand rule" means if you curl your fingers in the direction of the ring, your thumb points up. For exterior rings, this means counterclockwise. For holes, this means clockwise.
So my check was: exterior should be CCW, interior should be CW. That's what I checked and they're correct. Let me verify one more time.
Actually wait - let me re-read my orientation function. The shoelace formula: area = sum of (x2 - x1) * (y2 + y1). In a standard Cartesian plane:
- Positive area = CCW
- Negative area = CW
But for lon/lat coordinates (where x=longitude, y=latitude), the coordinates span a small area so the sign should be the same as Cartesian.
Let me verify with the first feature's exterior ring.
Feature 1 coords:
[139.7029081, 35.6592372]
[139.7027971, 35.659202]
[139.7027755, 35.6592471]
[139.7028864, 35.6592823]
[139.7029081, 35.6592372]
Let me compute manually:
(139.7027971 - 139.7029081) * (35.659202 + 35.6592372) = -0.000111 * 71.3184392 = negative
(139.7027755 - 139.7027971) * (35.6592471 + 35.659202) = -0.0000216 * 71.3184491 = negative
(139.7028864 - 139.7027755) * (35.6592823 + 35.6592471) = 0.0001109 * 71.3185294 = positive
(139.7029081 - 139.7028864) * (35.6592372 + 35.6592823) = 0.0000217 * 71.3185195 = positive
Sum: negative total = CW in Cartesian. But expected exterior = CCW. So the exterior ring is CW!
Wait, but my code said 0 issues. Let me re-examine the code logic.
Hmm, area += (x2 - x1) * (y2 + y1)
For a closed ring (last point = first point), the standard shoelace formula is:
Area = 0.5 * sum(x_i * y_{i+1} - x_{i+1} * y_i)
But my formula is (x2 - x1) * (y2 + y1). This is NOT the standard shoelace.
Actually, let me think about this:
(x2 - x1) * (y2 + y1) = x2*y2 + x2*y1 - x1*y2 - x1*y1
The standard shoelace term is: x_i * y_{i+1} - x_{i+1} * y_i
Let me see if there's a relationship:
Sum of (x_{i+1} - x_i) * (y_{i+1} + y_i)
= Sum of (x_{i+1}*y_{i+1} + x_{i+1}*y_i - x_i*y_{i+1} - x_i*y_i)
= Sum of x_{i+1}*y_{i+1} - x_i*y_i + Sum of (x_{i+1}*y_i - x_i*y_{i+1})
= (x_n*y_n - x_1*y_1) + Sum of (x_{i+1}*y_i - x_i*y_{i+1})
For a closed ring, x_n*y_n = x_1*y_1, so the first part is 0.
So it's: Sum of (x_{i+1}*y_i - x_i*y_{i+1}) = - Sum of (x_i*y_{i+1} - x_{i+1}*y_i)
So my formula gives the negative of twice the area. So positive area means CW, negative means CCW in Cartesian.
So in my code, I said if area > 0 -> 'ccw' but actually area > 0 would mean CW!
Let me fix my check.
Actually wait, let me re-examine. The standard shoelace formula for area:
A = 0.5 * |sum(x_i*y_{i+1} - x_{i+1}*y_i)|
The signed area (without absolute value) is:
SA = 0.5 * sum(x_i*y_{i+1} - x_{i+1}*y_i)
SA > 0 means CCW in Cartesian coordinates.
My formula: area = sum((x2 - x1) * (y2 + y1))
= -2 * SA (since I showed sum(x_{i+1}*y_i - x_i*y_{i+1}) = - sum(x_i*y_{i+1} - x_{i+1}*y_i) = -2*SA)
So if SA > 0 (CCW), then my area < 0.
If SA < 0 (CW), then my area > 0.
So my mapping is wrong! area > 0 means CW, not CCW.
Let me re-run with corrected logic.
1:19
1:19
Write
/work/check_orient2.py
content · 64 lines · py
import json
from shapely.geometry import shape, Polygon
with open('tokyo_buildings_legacy.geojson') as f:
data = json.load(f)
def ring_orientation(ring):
"""Returns 'cw' or 'ccw' for a ring (list of coords).
Uses signed area: positive = CCW in Cartesian coordinates."""
area = 0.0
n = len(ring)
for i in range(n - 1):
x1, y1 = ring[i]
x2, y2 = ring[i+1]
# Standard shoelace term: x1*y2 - x2*y1
area += x1 * y2 - x2 * y1
# area is twice the signed area
if area > 0:
return 'ccw'
elif area < 0:
return 'cw'
else:
return 'degenerate'
issues = []
for feat in data['features']:
fid = feat['properties']['feature_id']
coords = feat['geometry']['coordinates']
for ring_idx, ring in enumerate(coords):
orient = ring_orientation(ring)
label = 'exterior' if ring_idx == 0 else f'interior(hole) #{ring_idx}'
# RFC 7946: exterior=CCW, holes=CW
expected = 'ccw' if ring_idx == 0 else 'cw'
if orient != expected:
issues.append((fid, label, orient, expected))
print(f"Features with wrong ring orientation: {len(set(i[0] for i in issues))}")
for fid, label, orient, expected in issues:
print(f" feature_id={fid}: {label} is {orient} (expected {expected})")
print("\nChecking feature validity via shapely...")
valid_count = 0
invalid_count = 0
for feat in data['features']:
s = shape(feat['geometry'])
if s.is_valid:
valid_count += 1
else:
invalid_count += 1
print(f" feature_id={feat['properties']['feature_id']}: INVALID - {s.explain_validity()}")
print(f"Valid: {valid_count}, Invalid: {invalid_count}")
# Also check coordinate order (lon/lat vs lat/lon)
print("\nChecking coordinate ranges...")
lons = []
lats = []
for feat in data['features']:
for ring in feat['geometry']['coordinates']:
for coord in ring:
lons.append(coord[0])
lats.append(coord[1])
print(f"Longitude range: {min(lons):.4f} to {max(lons):.4f}")
print(f"Latitude range: {min(lats):.4f} to {max(lats):.4f}")
print("Tokyo is roughly at 139.7E, 35.7N — coordinates appear correct (lon, lat order)")
Wrote /work/check_orient2.py (2195 chars).
1:24
Let me think about this more carefully. The user said: 1. Building interiors are shaded incorrectly: holes appear filled and exteriors render as empty space 2. Tile-server import script rejects every feature with geometry warnings But my checks show: - All features are valid polygons (according to shapely) - Ring orientations seem correct (according to my calculation) Wait, let me re-check the ring orientation function. The shoelace formula should be `x1*y2 - x2*y1` summed for each edge. Actually, let me look more carefully at the GeoJSON standard (RFC 7946). It says: - Exterior rings should be counterclockwise - Interior rings (holes) should be clockwise But my earlier check showed no issues. Let me re-examine. Actually wait - the issue might be with how the GeoJSON is structured. Let me look at the file more carefully. Maybe the issue is that interior rings are actually exterior rings or something weird like that. Let me print out a few features with holes to check.
1:24
Bash
cd /work && python check_orient2.py
Features with wrong ring orientation: 100 feature_id=1: exterior is cw (expected ccw) feature_id=2: exterior is cw (expected ccw) feature_id=3: exterior is cw (expected ccw) feature_id=4: exterior is cw (expected ccw) feature_id=5: exterior is cw (expected ccw) feature_id=6: exterior is cw (expected ccw) feature_id=6: interior(hole) #1 is ccw (expected cw) feature_id=7: exterior is cw (expected ccw) feature_id=8: exterior is cw (expected ccw) feature_id=9: exterior is cw (expected ccw) feature_id=10: exterior is cw (expected ccw) feature_id=11: exterior is cw (expected ccw) feature_id=12: exterior is cw (expected ccw) feature_id=13: exterior is cw (expected ccw) feature_id=14: exterior is cw (expected ccw) feature_id=15: exterior is cw (expected ccw) feature_id=16: exterior is cw (expected ccw) feature_id=17: exterior is cw (expected ccw) feature_id=18: exterior is cw (expected ccw) feature_id=18: interior(hole) #1 is ccw (expected cw) feature_id=19: exterior is cw (expected ccw) feature_id=20: exterior is cw (expected ccw) feature_id=21: exterior is cw (expected ccw) feature_id=22: exterior is cw (expected ccw) feature_id=23: exterior is cw (expected ccw) feature_id=24: exterior is cw (expected ccw) feature_id=25: exterior is cw (expected ccw) feature_id=26: exterior is cw (expected ccw) feature_id=27: exterior is cw (expected ccw) feature_id=28: exterior is cw (expected ccw) feature_id=29: exterior is cw (expected ccw) feature_id=30: exterior is cw (expected ccw) feature_id=31: exterior is cw (expected ccw) feature_id=32: exterior is cw (expected ccw) feature_id=32: interior(hole) #1 is ccw (expected cw) feature_id=33: exterior is cw (expected ccw) feature_id=34: exterior is cw (expected ccw) feature_id=35: exterior is cw (expected ccw) feature_id=36: exterior is cw (expected ccw) feature_id=37: exterior is cw (expected ccw) feature_id=38: exterior is cw (expected ccw) feature_id=39: exterior is cw (expected ccw) feature_id=40: exterior is cw (expected ccw) feature_id=41: exterior is cw (expected ccw) feature_id=42: exterior is cw (expected ccw) feature_id=43: exterior is cw (expected ccw) feature_id=44: exterior is cw (expected ccw) feature_id=45: exterior is cw (expected ccw) feature_id=46: exterior is cw (expected ccw) feature_id=47: exterior is cw (expected ccw) feature_id=48: exterior is cw (expected ccw) feature_id=49: exterior is cw (expected ccw) feature_id=50: exterior is cw (expected ccw) feature_id=50: interior(hole) #1 is ccw (expected cw) feature_id=51: exterior is cw (expected ccw) feature_id=52: exterior is cw (expected ccw) feature_id=53: exterior is cw (expected ccw) feature_id=54: exterior is cw (expected ccw) feature_id=55: exterior is cw (expected ccw) feature_id=56: exterior is cw (expected ccw) feature_id=57: exterior is cw (expected ccw) feature_id=58: exterior is cw (expected ccw) feature_id=59: exterior is cw (expected ccw) feature_id=60: exterior is cw (expected ccw) feature_id=61: exterior is cw (expected ccw) feature_id=62: exterior is cw (expected ccw) feature_id=63: exterior is cw (expected ccw) feature_id=64: exterior is cw (expected ccw) feature_id=65: exterior is cw (expected ccw) feature_id=66: exterior is cw (expected ccw) feature_id=67: exterior is cw (expected ccw) feature_id=68: exterior is cw (expected ccw) feature_id=69: exterior is cw (expected ccw) feature_id=70: exterior is cw (expected ccw) feature_id=71: exterior is cw (expected ccw) feature_id=72: exterior is cw (expected ccw) feature_id=73: exterior is cw (expected ccw) feature_id=74: exterior is cw (expected ccw) feature_id=74: interior(hole) #1 is ccw (expected cw) feature_id=75: exterior is cw (expected ccw) feature_id=76: exterior is cw (expected ccw) feature_id=77: exterior is cw (expected ccw) feature_id=78: exterior is cw (expected ccw) feature_id=79: exterior is cw (expected ccw) feature_id=80: exterior is cw (expected ccw) feature_id=81: exterior is cw (expected ccw) feature_id=82: exterior is cw (expected ccw) feature_id=83: exterior is cw (expected ccw) feature_id=84: exterior is cw (expected ccw) feature_id=85: exterior is cw (expected ccw) feature_id=86: exterior is cw (expected ccw) feature_id=87: exterior is cw (expected ccw) feature_id=88: exterior is cw (expected ccw) feature_id=89: exterior is cw (expected ccw) feature_id=90: exterior is cw (expected ccw) feature_id=91: exterior is cw (expected ccw) feature_id=92: exterior is cw (expected ccw) feature_id=93: exterior is cw (expected ccw) feature_id=94: exterior is cw (expected ccw) feature_id=95: exterior is cw (expected ccw) feature_id=96: exterior is cw (expected ccw) feature_id=97: exterior is cw (expected ccw) feature_id=98: exterior is cw (expected ccw) feature_id=99: exterior is cw (expected ccw) feature_id=100: exterior is cw (expected ccw) Checking feature validity via shapely... Valid: 100, Invalid: 0 Checking coordinate ranges... Longitude range: 139.7000 to 139.7040 Latitude range: 35.6582 to 35.6618 Tokyo is roughly at 139.7E, 35.7N — coordinates appear correct (lon, lat order)
1:35
Now I see the issue clearly. All rings have the wrong orientation: - **Every** exterior ring is **clockwise (CW)**, but GeoJSON RFC 7946 requires exterior rings to be **counter-clockwise (CCW)**. - **Every** interior ring (hole) is **counter-clockwise (CCW)**, but RFC 7946 requires holes to be **clockwise (CW)**. This is what causes the rendering issue: web viewers follow RFC 7946 and treat CW exterior as a hole and CCW interior as filled areas, making holes appear filled and exteriors appear empty. The fix is simple: reverse the coordinate order of every ring in every polygon. This will flip all orientations to the correct standard. Shapely's `shape()` function already handles this correctly (it normalizes orientation), so the data is valid by shapely's standards (it doesn't care about orientation). But when we emit GeoJSON for web viewers, we need to follow RFC 7946. Let me write the fix.
1:35
1:35
Write
/work/fix_geojson.py
content · 59 lines · py
import json
# Load the legacy GeoJSON
with open('/work/tokyo_buildings_legacy.geojson') as f:
data = json.load(f)
# Fix every feature's ring orientation to comply with RFC 7946:
# - Exterior ring: counter-clockwise (CCW)
# - Interior rings (holes): clockwise (CW)
for feat in data['features']:
rings = feat['geometry']['coordinates']
for ring_idx, ring in enumerate(rings):
# Reverse the ring to flip orientation
# RFC 7946: exterior=CCW, holes=CW.
# All current rings have the opposite orientation, so
# simply reversing fixes everything.
feat['geometry']['coordinates'][ring_idx] = ring[::-1]
# Remove the non-standard "crs" property (RFC 7946 uses WGS 84 by default)
if 'crs' in data:
del data['crs']
# Write the fixed GeoJSON
with open('/work/tokyo_buildings_fixed.geojson', 'w') as f:
json.dump(data, f, ensure_ascii=False)
print("Done. Fixed ring orientation for all features.")
print(f"Total features: {len(data['features'])}")
# Verify the fix
def ring_orientation(ring):
area = 0.0
n = len(ring)
for i in range(n - 1):
x1, y1 = ring[i]
x2, y2 = ring[i+1]
area += x1 * y2 - x2 * y1
if area > 0:
return 'ccw'
elif area < 0:
return 'cw'
else:
return 'degenerate'
errors = 0
for feat in data['features']:
fid = feat['properties']['feature_id']
coords = feat['geometry']['coordinates']
for ring_idx, ring in enumerate(coords):
orient = ring_orientation(ring)
expected = 'ccw' if ring_idx == 0 else 'cw'
if orient != expected:
print(f" ERROR: feature_id={fid} ring {ring_idx} is {orient} (expected {expected})")
errors += 1
if errors == 0:
print("All ring orientations correctly follow RFC 7946.")
else:
print(f"{errors} orientation issues remain.")
Wrote /work/fix_geojson.py (1865 chars).
1:37
Wait, I should also check that the GeoJSON file follows proper format. Let me also verify that the output works properly. Actually, one concern: the feature_id is listed as the join key, so I need to make sure the features maintain their order or at least the feature_id values remain the same. Since I'm just reversing rings in-place, the feature_ids are untouched. Let me run the script and also verify with shapely that the geometries are valid and their polygon areas are preserved (not flipped).
1:37
Bash
cd /work && python fix_geojson.py
Done. Fixed ring orientation for all features. Total features: 100 All ring orientations correctly follow RFC 7946.
1:46
Let me also verify the output file is valid GeoJSON and check a few features to make sure attributes and structure are preserved.
1:46
1:46
Bash
cd /work && python -c "
cd /work && python -c "
import json
# Verify the output file
with open('tokyo_buildings_fixed.geojson') as f:
data = json.load(f)
print('Type:', data['type'])
print('Feature count:', len(data['features']))
print('Has CRS:', 'crs' in data)
# Check a random sample including one with holes
for feat in data['features']:
fid = feat['properties']['feature_id']
if fid in [1, 2, 6, 50]:
print(f'\nFeature {fid}:')
print(f' Geometry type: {feat[\"geometry\"][\"type\"]}')
print(f' Num rings: {len(feat[\"geometry\"][\"coordinates\"])}')
print(f' Properties: {feat[\"properties\"]}')
print(f' Exterior coords: {feat[\"geometry\"][\"coordinates\"][0][:3]}...')
if len(feat['geometry']['coordinates']) > 1:
print(f' Interior ring coords: {feat[\"geometry\"][\"coordinates\"][1][:3]}...')
# Verify attribute integrity
print('\nVerifying attribute integrity...')
for feat_orig, feat_fixed in zip(
json.load(open('tokyo_buildings_legacy.geojson'))['features'],
data['features']
):
if feat_orig['properties'] != feat_fixed['properties']:
print(f' MISMATCH feature_id={feat_orig[\"properties\"][\"feature_id\"]}')
break
else:
print(' All properties match original!')
# Verify geometry structure preserved (same number of rings)
print('\nVerifying geometry structure...')
mismatch = 0
for feat_orig, feat_fixed in zip(
json.load(open('tokyo_buildings_legacy.geojson'))['features'],
data['features']
):
orig_rings = len(feat_orig['geometry']['coordinates'])
fixed_rings = len(feat_fixed['geometry']['coordinates'])
if orig_rings != fixed_rings:
print(f' Ring count mismatch for feature_id={feat_orig[\"properties\"][\"feature_id\"]}')
mismatch += 1
# Also check same number of vertices per ring
for i in range(orig_rings):
if len(feat_orig['geometry']['coordinates'][i]) != len(feat_fixed['geometry']['coordinates'][i]):
print(f' Vertex count mismatch for feature_id={feat_orig[\"properties\"][\"feature_id\"]}, ring {i}')
mismatch += 1
if mismatch == 0:
print(' All geometry structures preserved!')
"
Type: FeatureCollection
Feature count: 100
Has CRS: False
Feature 1:
Geometry type: Polygon
Num rings: 1
Properties: {'feature_id': 1, 'overture_id': '01302ced-3141-4b64-b630-98019352a9fd', 'name_primary': '渋谷協和ビル', 'building_class': '', 'height': None}
Exterior coords: [[139.7029081, 35.6592372], [139.7028864, 35.6592823], [139.7027755, 35.6592471]]...
Feature 2:
Geometry type: Polygon
Num rings: 1
Properties: {'feature_id': 2, 'overture_id': '046ab130-a9a2-4a0e-9142-b7ea36374fdb', 'name_primary': '丸大ビル', 'building_class': '', 'height': None}
Exterior coords: [[139.7010896, 35.6601981], [139.7009195, 35.6602237], [139.7009007, 35.6601417]]...
Feature 6:
Geometry type: Polygon
Num rings: 2
Properties: {'feature_id': 6, 'overture_id': '0ff95784-5c37-4c0e-8ee3-674f96075b8c', 'name_primary': '', 'building_class': '', 'height': None}
Exterior coords: [[139.7025726, 35.661211], [139.7024485, 35.6612117], [139.7024481, 35.6611726]]...
Interior ring coords: [[139.70261625, 35.6612158], [139.70261625, 35.6612207], [139.70263306500001, 35.6612207]]...
Feature 50:
Geometry type: Polygon
Num rings: 2
Properties: {'feature_id': 50, 'overture_id': '61580917-74ae-4b83-9246-cef727d5593a', 'name_primary': '御嶽神社', 'building_class': '', 'height': None}
Exterior coords: [[139.7036066, 35.6604974], [139.7034969, 35.6604744], [139.7035278, 35.6603771]]...
Interior ring coords: [[139.70356017, 35.660436047], [139.70356017, 35.660442061999994], [139.7035672, 35.660442061999994]]...
Verifying attribute integrity...
All properties match original!
Verifying geometry structure...
All geometry structures preserved!
1:53
The fix is complete. Let me summarize what was found and fixed: 1. **Problem**: All polygon rings had reversed orientation compared to RFC 7946 standard. - Exterior rings were clockwise (CW) instead of counter-clockwise (CCW) - Interior rings (holes) were CCW instead of CW 2. **Why this caused issues**: Most GeoJSON web renderers follow RFC 7946, which specifies that the exterior ring should be CCW and interior rings (holes) should be CW. When rings have the opposite orientation, renderers treat them as the opposite — so the exterior looks like a hole (transparent) and holes look like filled areas. 3. **Fix**: Simply reversed each ring's coordinate array to flip the orientation. 4. **Additional fix**: Removed the non-standard "crs" property since RFC 7946 uses WGS 84 by default. 5. **Preserved**: All attributes, geometry structure (same number of rings and vertices per ring), and all features remain as Polygons (no flattening of holes).
1:53