crs-l2-fiji-antimeridian
Map (reference ← swipe → agent)
0:00
I'm pulling together per-transect coverage figures for our survey report, but some of the lines in fiji_transects_wgs84.geojson render as arcs spanning the entire globe instead of short local segments. Can you fix the geometries so they render correctly, convert them to Fiji's national metric grid, and give me each transect as one MultiLineString feature with a length_m attribute in metres?
Every feature must come out as a MultiLineString, so do not leave any transects as plain LineString, and the problematic ones must end up as multi-part geometries that faithfully represent the actual survey path. Carry the survey attributes (vessel and survey_date) through unchanged, use transect_id as the key field, and write the result as GeoJSON named fiji_transects_fmg.geojson.
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 16 -rw-r--r-- 1 agent agent 14090 Jun 15 20:49 fiji_transects_wgs84.geojson
0:14
Bash
head -n 20 fiji_transects_wgs84.geojson
head -n 20 fiji_transects_wgs84.geojson
{
"type": "FeatureCollection",
"name": "fiji_transects_wgs84",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "transect_id": "T001", "vessel": "Taveuni II", "survey_date": "2025-08-15", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 178.941879580782086, -17.632301816020771 ], [ 179.456655009654696, -17.623768593111961 ], [ 179.971430438527307, -17.611072392823225 ], [ -179.513794132600054, -17.596198176111951 ], [ -178.999018703727444, -17.579632069513806 ], [ -178.484243274854833, -17.577321872344196 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T002", "vessel": "Bligh", "survey_date": "2025-08-12", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 179.070288824800542, -17.334767984150709 ], [ 179.725942881740252, -17.171343988196945 ], [ -179.618403061320009, -17.007726843860763 ], [ -178.962749004380299, -16.834850408534852 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T003", "vessel": "Lomaiviti", "survey_date": "2025-08-12", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 177.636947428758333, -17.358837040696127 ], [ 178.462815605623859, -17.416074837434927 ], [ 179.288683782489386, -17.471713663377283 ], [ -179.885448040645059, -17.527675804260884 ], [ -179.059579863779533, -17.590577543375694 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T004", "vessel": "Vanua I", "survey_date": "2025-08-15", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 178.873696381462679, -17.803295238757038 ], [ 179.542254331047587, -17.598477321703708 ], [ -179.789187719367533, -17.405549472537043 ], [ -179.120629769782624, -17.19411411231702 ], [ -178.452071820197745, -16.992332922174427 ], [ -177.783513870612836, -16.795171196845544 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T005", "vessel": "Bligh", "survey_date": "2025-08-19", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 177.596924830927293, -17.923255155112717 ], [ 178.160696712947697, -18.010328270600898 ], [ 178.724468594968101, -18.099224972140938 ], [ 179.288240476988506, -18.17532145144995 ], [ 179.85201235900891, -18.270380145331412 ], [ -179.584215758970686, -18.358875431719703 ], [ -179.020443876950281, -18.437390943572723 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T006", "vessel": "Cakaulevu", "survey_date": "2025-08-13", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 178.622166647099931, -18.382052393687179 ], [ 179.205668025282989, -18.184610858038685 ], [ 179.789169403466019, -17.991866703295319 ], [ -179.627329218350923, -17.789532271264445 ], [ -179.043827840167864, -17.595682751178369 ], [ -178.460326461984835, -17.393483000040479 ], [ -177.876825083801776, -17.203490564162109 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T007", "vessel": "Vanua I", "survey_date": "2025-08-19", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 177.920101619592742, -17.14979850360189 ], [ 178.697177550619955, -17.100522188212071 ], [ 179.474253481647168, -17.053867486482115 ], [ -179.74867058732562, -17.004197778897627 ], [ -178.971594656298407, -16.959481244370728 ], [ -178.194518725271195, -16.911697858015476 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T008", "vessel": "Taveuni II", "survey_date": "2025-08-12", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 179.401134548056262, -17.934415957637167 ], [ 179.67454249485786, -17.899654147497863 ], [ 179.947950441659486, -17.858130727687527 ], [ -179.778641611538916, -17.811143619533269 ], [ -179.50523366473729, -17.768768396969666 ], [ -179.231825717935692, -17.724708363613253 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T009", "vessel": "Vanua I", "survey_date": "2025-08-13", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 178.068433112639013, -16.657838811131668 ], [ 178.561564721122721, -16.689731320158675 ], [ 179.054696329606429, -16.72173241552732 ], [ 179.547827938090137, -16.748891897767297 ], [ -179.959040453426155, -16.775241845025921 ], [ -179.465908844942447, -16.799281899607834 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T010", "vessel": "Vanua I", "survey_date": "2025-08-15", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 177.602847396853463, -17.277026295424367 ], [ 179.128053569323441, -17.468285296635063 ], [ -179.346740258206609, -17.65419136085664 ], [ -177.82153408573663, -17.837325774502055 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T011", "vessel": "Taveuni II", "survey_date": "2025-08-20", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ -176.863797870333627, -17.7932433540029 ], [ -176.985724572324699, -17.81273654528005 ], [ -177.107651274315742, -17.824236921378098 ], [ -177.229577976306814, -17.835861250474697 ], [ -177.351504678297886, -17.857559546831705 ], [ -177.473431380288929, -17.871769203842408 ], [ -177.595358082280001, -17.890225470404214 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T012", "vessel": "Taveuni II", "survey_date": "2025-08-20", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ 176.312195951017685, -17.976122209608448 ], [ 176.362236324041447, -17.722210270969462 ], [ 176.412276697065209, -17.470306839752467 ], [ 176.462317070088972, -17.226475769084448 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T013", "vessel": "Bligh", "survey_date": "2025-08-19", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ -176.70823848972006, -17.696306145080879 ], [ -177.205026853648661, -17.960082567744127 ], [ -177.701815217577291, -18.210737758337459 ], [ -178.198603581505893, -18.469124252379427 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T014", "vessel": "Lomaiviti", "survey_date": "2025-08-13", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ 176.538496893308832, -16.594358164012139 ], [ 176.861572490827996, -16.71494251806207 ], [ 177.184648088347188, -16.832251271240672 ], [ 177.507723685866381, -16.976116506165042 ], [ 177.830799283385545, -17.090939089364483 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T015", "vessel": "Vanua I", "survey_date": "2025-08-15", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ -177.278377449219306, -16.724372731501912 ], [ -177.470251917030794, -16.761535677586746 ], [ -177.662126384842253, -16.787182414326686 ], [ -177.854000852653712, -16.818799270133983 ], [ -178.0458753204652, -16.856282470705128 ], [ -178.237749788276659, -16.891308864567598 ], [ -178.429624256088147, -16.914729063096839 ] ] } },
0:35
Write
/work/solve.py
content · 238 lines · py
import geopandas as gpd
import pandas as pd
from shapely.geometry import LineString, MultiLineString
import numpy as np
def fix_antimeridian_line(coords):
"""
Splits a LineString that crosses the antimeridian into multiple LineStrings.
Coords are (lon, lat).
"""
if not coords:
return None
new_lines = []
current_segment = [coords[0]]
for i in range(1, len(coords)):
prev_lon = coords[i-1][0]
curr_lon = coords[i][0]
# Check if there's a jump across the antimeridian
# If the difference is greater than 180, it's likely a wrap-around
if abs(curr_lon - prev_lon) > 180:
# Close the current segment
if len(current_segment) >= 2:
new_lines.append(LineString(current_segment))
# Start new segment.
# We need to adjust the longitude so it's continuous in the new coordinate space.
# However, for the purpose of just 'splitting', we can just start with the new coord.
# But wait, if we want the new segment to be 'local', we should shift all subsequent
# coordinates by 360 or -360.
# Let's determine the shift
shift = 360 if curr_lon < prev_lon else -360
# Instead of complex logic, let's just split and later we'll handle the projection.
# Actually, the simplest way to handle antimeridian in GeoPandas/Shapely is to
# split the line at the jump and adjust the longitude of the new segment.
# But a better way is to shift all longitudes so they are continuous.
# Let's try this:
current_segment = []
# Find the jump
# Re-evaluating: if we shift the coordinates, we can treat it as a continuous line.
pass
# Let's use a more robust approach for antimeridian splitting.
return None
def split_antimeridian(line):
"""
Splits a LineString into multiple LineStrings if it crosses the antimeridian.
"""
coords = list(line.coords)
if not coords:
return [line]
segments = []
current_segment = [coords[0]]
for i in range(1, len(coords)):
prev_lon = coords[i-1][0]
curr_lon = coords[i][0]
# Detect jump
if abs(curr_lon - prev_lon) > 180:
# Split!
if len(current_segment) >= 2:
segments.append(LineString(current_segment))
# Determine the direction of the jump
# If prev was 179 and curr is -179, jump is positive (towards 180)
# If prev was -179 and curr is 179, jump is negative (towards -180)
# We adjust the longitude of the new segment to be continuous.
# Let's calculate the shift required to make curr_lon continuous with prev_lon
if curr_lon < prev_lon:
# Jump from + to - (e.g., 179 -> -179)
# We want to add 360 to the new coordinates to make them 181, 182...
# Or rather, we want to keep the geometry "local" but consistent.
# Actually, if we are going to project to Fiji metric grid (FMG),
# we should probably just handle the wrap-around by shifting.
pass
# Let's simplify:
# 1. Normalize all longitudes to a continuous range relative to the first point.
# 2. Then split the line when the longitude jumps.
pass
def robust_split_antimeridian(line):
coords = list(line.coords)
if len(coords) < 2:
return [line]
# Step 1: Make longitudes continuous
normalized_coords = [coords[0]]
for i in range(1, len(coords)):
prev_lon, prev_lat = normalized_coords[i-1]
curr_lon, curr_lat = coords[i]
diff = curr_lon - prev_lon
if diff > 180:
diff -= 360
elif diff < -180:
diff += 360
normalized_coords.append((prev_lon + diff, curr_lat))
# Step 2: Split into segments where the jump was > 180 (in the original coords)
# Actually, we can just split where the 'diff' was significant or where the original
# jump was detected.
segments = []
current_seg = [normalized_coords[0]]
for i in range(1, len(normalized_coords)):
prev_orig_lon = coords[i-1][0]
curr_orig_lon = coords[i][0]
if abs(curr_orig_lon - prev_orig_lon) > 180:
# Split
if len(current_seg) >= 2:
segments.append(LineString(current_seg))
current_seg = [normalized_coords[i]]
else:
current_seg.append(normalized_coords[i])
if len(current_seg) >= 2:
segments.append(LineString(current_seg))
# If no split happened, just return the original line (in its original lon range)
# Wait, if we return normalized_coords, they might be outside [-180, 180].
# But that's fine for projection.
if not segments:
return [line]
# We need to return segments that are valid (can be projected).
# The normalized_coords approach is good.
return segments
# Let's refine the process
def process_transects(input_file, output_file):
gdf = gpd.read_file(input_file)
# Fiji National Grid (FMG) is often a UTM variant or specific local projection.
# Checking common Fiji projections. Fiji uses Fiji Grid (FMG) or UTM zones 59S/60S.
# The prompt says "Fiji's national metric grid".
# Usually, this refers to Fiji Grid (EPSG:2977 or similar) or just UTM.
# Let's look for EPSG:2977 (Fiji Grid).
# Actually, let's check common ones.
# EPSG:2977 is "Fiji Grid".
target_crs = "EPSG:2977"
# We will create a new list of features
new_features = []
for _, row in gdf.iterrows():
geom = row.geometry
transect_id = row['transect_id']
vessel = row['vessel']
survey_date = row['survey_date']
if geom.geom_type == 'LineString':
segments = robust_split_antimeridian(geom)
elif geom.geom_type == 'MultiLineString':
# If it's already a MultiLineString, we process each part
segments = []
for part in geom.geoms:
segments.extend(robust_split_antimeridian(part))
else:
segments = [geom]
# Now we have a list of LineStrings (some might be "normalized" outside -180/180)
# Create a MultiLineString from these segments
# Wait, the prompt says "give me each transect as one MultiLineString feature".
# So for each input row, we get ONE output row.
# BUT, we must project them to FMG first.
# If we project normalized coords, it should work.
# Create a temporary GeoDataFrame for the segments of this transect
temp_gdf = gpd.GeoDataFrame({'geometry': segments}, crs="EPSG:4326")
# If the normalized segments are outside [-180, 180],
# we might need to wrap them before projection if the CRS doesn't handle it.
# However, standard projection (pyproj) usually handles longitudes > 180.
# Let's try.
# Project to FMG
temp_gdf = temp_gdf.to_crs(target_crs)
# Combine segments into a single MultiLineString
combined_geom = temp_gdf.geometry.unary_union
# Ensure it's a MultiLineString (unary_union might return LineString if only one segment)
if combined_geom.geom_type == 'LineString':
combined_geom = MultiLineString([combined_geom])
elif combined_geom.geom_type != 'MultiLineString':
# This handles cases where it might be a GeometryCollection
# but for our purposes, it should be LineStrings.
# Let's just force it.
from shapely.geometry import MultiLineString
if combined_geom.geom_type == 'GeometryCollection':
lines = [g for g in combined_geom.geoms if g.geom_type in ['LineString', 'MultiLineString']]
# Flatten if they are MultiLineStrings
all_lines = []
for l in lines:
if l.geom_type == 'LineString':
all_lines.append(l)
else:
all_lines.extend(list(l.geoms))
combined_geom = MultiLineString(all_lines)
# Calculate length in metres
length_m = combined_geom.length
# Prepare new feature properties
new_props = row.to_dict()
new_props['length_m'] = length_m
# Remove the old geometry to avoid conflict during creation
if 'geometry' in new_props:
del new_props['geometry']
new_features.append({
'type': 'Feature',
'properties': new_props,
'geometry': combined_geom
})
# Create the final GeoDataFrame
output_gdf = gpd.GeoDataFrame.from_features(new_features, crs=target_crs)
output_gdf.to_file(output_file, driver='GeoJSON')
if __name__ == "__main__":
process_transects('fiji_transects_wgs84.geojson', 'fiji_transects_fmg.geojson')
Wrote /work/solve.py (9399 chars).
0:37
Bash
python /work/solve.py
python /work/solve.py
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
combined_geom = temp_gdf.geometry.unary_union
Traceback (most recent call last):
File "/work/solve.py", line 237, in <module>
process_transects('fiji_transects_wgs84.geojson', 'fiji_transects_fmg.geojson')
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/work/solve.py", line 199, in process_transects
combined_geom = MultiLineString([combined_geom])
^^^^^^^^^^^^^^^
UnboundLocalError: cannot access local variable 'MultiLineString' where it is not associated with a value
0:40
Edit
/work/solve.py
old_string and new_string are identical; no edit to make
0:47
Edit
/work/solve.py
Edited /work/solve.py.
1:08
Edit
/work/solve.py
old_string not found in /work/solve.py
1:09
Read
/work/solve.py
1 import geopandas as gpd
2 import pandas as pd
3 from shapely.geometry import LineString, MultiLineString, GeometryCollection
4 import numpy as np
5
6 def fix_antimeridian_line(coords):
7 """
8 Splits a LineString that crosses the antimeridian into multiple LineStrings.
9 Coords are (lon, lat).
10 """
11 if not coords:
12 return None
13
14 new_lines = []
15 current_segment = [coords[0]]
16
17 for i in range(1, len(coords)):
18 prev_lon = coords[i-1][0]
19 curr_lon = coords[i][0]
20
21 # Check if there's a jump across the antimeridian
22 # If the difference is greater than 180, it's likely a wrap-around
23 if abs(curr_lon - prev_lon) > 180:
24 # Close the current segment
25 if len(current_segment) >= 2:
26 new_lines.append(LineString(current_segment))
27
28 # Start new segment.
29 # We need to adjust the longitude so it's continuous in the new coordinate space.
30 # However, for the purpose of just 'splitting', we can just start with the new coord.
31 # But wait, if we want the new segment to be 'local', we should shift all subsequent
32 # coordinates by 360 or -360.
33
34 # Let's determine the shift
35 shift = 360 if curr_lon < prev_lon else -360
36
37 # Instead of complex logic, let's just split and later we'll handle the projection.
38 # Actually, the simplest way to handle antimeridian in GeoPandas/Shapely is to
39 # split the line at the jump and adjust the longitude of the new segment.
40
41 # But a better way is to shift all longitudes so they are continuous.
42 # Let's try this:
43 current_segment = []
44 # Find the jump
45 # Re-evaluating: if we shift the coordinates, we can treat it as a continuous line.
46 pass
47
48 # Let's use a more robust approach for antimeridian splitting.
49 return None
50
51 def split_antimeridian(line):
52 """
53 Splits a LineString into multiple LineStrings if it crosses the antimeridian.
54 """
55 coords = list(line.coords)
56 if not coords:
57 return [line]
58
59 segments = []
60 current_segment = [coords[0]]
61
62 for i in range(1, len(coords)):
63 prev_lon = coords[i-1][0]
64 curr_lon = coords[i][0]
65
66 # Detect jump
67 if abs(curr_lon - prev_lon) > 180:
68 # Split!
69 if len(current_segment) >= 2:
70 segments.append(LineString(current_segment))
71
72 # Determine the direction of the jump
73 # If prev was 179 and curr is -179, jump is positive (towards 180)
74 # If prev was -179 and curr is 179, jump is negative (towards -180)
75 # We adjust the longitude of the new segment to be continuous.
76
77 # Let's calculate the shift required to make curr_lon continuous with prev_lon
78 if curr_lon < prev_lon:
79 # Jump from + to - (e.g., 179 -> -179)
80 # We want to add 360 to the new coordinates to make them 181, 182...
81 # Or rather, we want to keep the geometry "local" but consistent.
82 # Actually, if we are going to project to Fiji metric grid (FMG),
83 # we should probably just handle the wrap-around by shifting.
84 pass
85
86 # Let's simplify:
87 # 1. Normalize all longitudes to a continuous range relative to the first point.
88 # 2. Then split the line when the longitude jumps.
89 pass
90
91 def robust_split_antimeridian(line):
92 coords = list(line.coords)
93 if len(coords) < 2:
94 return [line]
95
96 # Step 1: Make longitudes continuous
97 normalized_coords = [coords[0]]
98 for i in range(1, len(coords)):
99 prev_lon, prev_lat = normalized_coords[i-1]
100 curr_lon, curr_lat = coords[i]
101
102 diff = curr_lon - prev_lon
103 if diff > 180:
104 diff -= 360
105 elif diff < -180:
106 diff += 360
107
108 normalized_coords.append((prev_lon + diff, curr_lat))
109
110 # Step 2: Split into segments where the jump was > 180 (in the original coords)
111 # Actually, we can just split where the 'diff' was significant or where the original
112 # jump was detected.
113
114 segments = []
115 current_seg = [normalized_coords[0]]
116
117 for i in range(1, len(normalized_coords)):
118 prev_orig_lon = coords[i-1][0]
119 curr_orig_lon = coords[i][0]
120
121 if abs(curr_orig_lon - prev_orig_lon) > 180:
122 # Split
123 if len(current_seg) >= 2:
124 segments.append(LineString(current_seg))
125 current_seg = [normalized_coords[i]]
126 else:
127 current_seg.append(normalized_coords[i])
128
129 if len(current_seg) >= 2:
130 segments.append(LineString(current_seg))
131
132 # If no split happened, just return the original line (in its original lon range)
133 # Wait, if we return normalized_coords, they might be outside [-180, 180].
134 # But that's fine for projection.
135
136 if not segments:
137 return [line]
138
139 # We need to return segments that are valid (can be projected).
140 # The normalized_coords approach is good.
141 return segments
142
143 # Let's refine the process
144 def process_transects(input_file, output_file):
145 gdf = gpd.read_file(input_file)
146
147 # Fiji National Grid (FMG) is often a UTM variant or specific local projection.
148 # Checking common Fiji projections. Fiji uses Fiji Grid (FMG) or UTM zones 59S/60S.
149 # The prompt says "Fiji's national metric grid".
150 # Usually, this refers to Fiji Grid (EPSG:2977 or similar) or just UTM.
151 # Let's look for EPSG:2977 (Fiji Grid).
152 # Actually, let's check common ones.
153 # EPSG:2977 is "Fiji Grid".
154 target_crs = "EPSG:2977"
155
156 # We will create a new list of features
157 new_features = []
158
159 for _, row in gdf.iterrows():
160 geom = row.geometry
161 transect_id = row['transect_id']
162 vessel = row['vessel']
163 survey_date = row['survey_date']
164
165 if geom.geom_type == 'LineString':
166 segments = robust_split_antimeridian(geom)
167 elif geom.geom_type == 'MultiLineString':
168 # If it's already a MultiLineString, we process each part
169 segments = []
170 for part in geom.geoms:
171 segments.extend(robust_split_antimeridian(part))
172 else:
173 segments = [geom]
174
175 # Now we have a list of LineStrings (some might be "normalized" outside -180/180)
176 # Create a MultiLineString from these segments
177 # Wait, the prompt says "give me each transect as one MultiLineString feature".
178 # So for each input row, we get ONE output row.
179
180 # BUT, we must project them to FMG first.
181 # If we project normalized coords, it should work.
182
183 # Create a temporary GeoDataFrame for the segments of this transect
184 temp_gdf = gpd.GeoDataFrame({'geometry': segments}, crs="EPSG:4326")
185
186 # If the normalized segments are outside [-180, 180],
187 # we might need to wrap them before projection if the CRS doesn't handle it.
188 # However, standard projection (pyproj) usually handles longitudes > 180.
189 # Let's try.
190
191 # Project to FMG
192 temp_gdf = temp_gdf.to_crs(target_crs)
193
194 # Combine segments into a single MultiLineString
195 combined_geom = temp_gdf.geometry.unary_union
196
197 # Ensure it's a MultiLineString (unary_union might return LineString if only one segment)
198 if combined_geom.geom_type == 'LineString':
199 combined_geom = MultiLineString([combined_geom])
200 elif combined_geom.geom_type != 'MultiLineString':
201 # This handles cases where it might be a GeometryCollection
202 # but for our purposes, it should be LineStrings.
203 # Let's just force it.
204 from shapely.geometry import MultiLineString
205 if combined_geom.geom_type == 'GeometryCollection':
206 lines = [g for g in combined_geom.geoms if g.geom_type in ['LineString', 'MultiLineString']]
207 # Flatten if they are MultiLineStrings
208 all_lines = []
209 for l in lines:
210 if l.geom_type == 'LineString':
211 all_lines.append(l)
212 else:
213 all_lines.extend(list(l.geoms))
214 combined_geom = MultiLineString(all_lines)
215
216 # Calculate length in metres
217 length_m = combined_geom.length
218
219 # Prepare new feature properties
220 new_props = row.to_dict()
221 new_props['length_m'] = length_m
222 # Remove the old geometry to avoid conflict during creation
223 if 'geometry' in new_props:
224 del new_props['geometry']
225
226 new_features.append({
227 'type': 'Feature',
228 'properties': new_props,
229 'geometry': combined_geom
230 })
231
232 # Create the final GeoDataFrame
233 output_gdf = gpd.GeoDataFrame.from_features(new_features, crs=target_crs)
234 output_gdf.to_file(output_file, driver='GeoJSON')
235
236 if __name__ == "__main__":
237 process_transects('fiji_transects_wgs84.geojson', 'fiji_transects_fmg.geojson')
1:19
Write
/work/solve.py
content · 119 lines · py
import geopandas as gpd
import pandas as pd
from shapely.geometry import LineString, MultiLineString, GeometryCollection
import numpy as np
def robust_split_antimeridian(line):
"""
Splits a LineString into multiple LineStrings if it crosses the antimeridian.
"""
coords = list(line.coords)
if len(coords) < 2:
return [line]
# Step 1: Make longitudes continuous
normalized_coords = [coords[0]]
for i in range(1, len(coords)):
prev_lon, prev_lat = normalized_coords[i-1]
curr_lon, curr_lat = coords[i]
diff = curr_lon - prev_lon
if diff > 180:
diff -= 360
elif diff < -180:
diff += 360
normalized_coords.append((prev_lon + diff, curr_lat))
# Step 2: Split into segments where the jump was detected
segments = []
current_seg = [normalized_coords[0]]
for i in range(1, len(normalized_coords)):
prev_orig_lon = coords[i-1][0]
curr_orig_lon = coords[i][0]
# Detect if the original jump was an antimeridian crossing
if abs(curr_orig_lon - prev_orig_lon) > 180:
# Split
if len(current_seg) >= 2:
segments.append(LineString(current_seg))
current_seg = [normalized_coords[i]]
else:
current_seg.append(normalized_coords[i])
if len(current_seg) >= 2:
segments.append(LineString(current_seg))
return segments if segments else [line]
def process_transects(input_file, output_file):
gdf = gpd.read_file(input_file)
# Fiji Grid (FMG) - EPSG:2977
target_crs = "EPSG:2977"
new_features = []
for _, row in gdf.iterrows():
geom = row.geometry
if geom.geom_type == 'LineString':
segments = robust_split_antimeridian(geom)
elif geom.geom_type == 'MultiLineString':
segments = []
for part in geom.geoms:
segments.extend(robust_split_antimeridian(part))
else:
segments = [geom]
# Create a temporary GeoDataFrame for the segments of this transect
# IMPORTANT: Since the segments might have longitudes > 180 or < -180,
# we still define them as EPSG:4326. Proj should handle it.
temp_gdf = gpd.GeoDataFrame({'geometry': segments}, crs="EPSG:4326")
# Project to FMG
temp_gdf = temp_gdf.to_crs(target_crs)
# Combine segments into a single geometry
# unary_union can return LineString or MultiLineString
combined_geom = temp_gdf.geometry.union_all()
# Ensure it's a MultiLineString
if combined_geom.geom_type == 'LineString':
combined_geom = MultiLineString([combined_geom])
elif combined_geom.geom_type == 'GeometryCollection':
# Extract only lines
lines = []
for g in combined_geom.geoms:
if g.geom_type == 'LineString':
lines.append(g)
elif g.geom_type == 'MultiLineString':
lines.extend(list(g.geoms))
combined_geom = MultiLineString(lines)
elif combined_geom.geom_type != 'MultiLineString':
# Fallback for safety
combined_geom = MultiLineString([combined_geom])
# Calculate length in metres
length_m = combined_geom.length
# Prepare new feature properties
new_props = row.to_dict()
new_props['length_m'] = length_m
if 'geometry' in new_props:
del new_props['geometry']
new_features.append({
'type': 'Feature',
'properties': new_props,
'geometry': combined_geom
})
# Create the final GeoDataFrame
output_gdf = gpd.GeoDataFrame.from_features(new_features, crs=target_crs)
output_gdf.to_file(output_file, driver='GeoJSON')
if __name__ == "__main__":
process_transects('fiji_transects_wgs84.geojson', 'fiji_transects_fmg.geojson')
Wrote /work/solve.py (4117 chars).
1:22
Bash
python /work/solve.py
python /work/solve.py(no output)
1:27
Bash
head -n 20 fiji_transects_fmg.geojson
head -n 20 fiji_transects_fmg.geojson
{
"type": "FeatureCollection",
"name": "fiji_transects_fmg",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::2977" } },
"features": [
{ "type": "Feature", "properties": { "transect_id": "T001", "vessel": "Taveuni II", "survey_date": "2025-08-15T00:00:00", "crosses_antimeridian_flag": true, "length_m": 242148.17462422466 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2577818.856368530075997, 7808045.933654122054577 ], [ -2517744.839244871865958, 7818720.514956817030907 ], [ -2457959.618174184113741, 7829649.209022815339267 ] ], [ [ -2398415.969535391777754, 7840591.110509935766459 ], [ -2339097.414767996408045, 7851489.390455324202776 ], [ -2279720.962942073587328, 7860430.453650631010532 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T002", "vessel": "Bligh", "survey_date": "2025-08-12T00:00:00", "crosses_antimeridian_flag": true, "length_m": 160320.48503040406 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2568571.367820397950709, 7846831.475579218938947 ], [ -2494815.333217027131468, 7878702.454537282697856 ] ], [ [ -2421194.358998955693096, 7910035.447528526186943 ], [ -2347860.872700769919902, 7941939.787312176078558 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T003", "vessel": "Lomaiviti", "survey_date": "2025-08-12T00:00:00", "crosses_antimeridian_flag": true, "length_m": 294940.8152454049 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2737259.021241321694106, 7816272.578033835627139 ], [ -2638431.858519812580198, 7825394.063890357501805 ], [ -2540319.581941521726549, 7834153.894859520718455 ] ], [ [ -2442858.046751122921705, 7842329.020383344963193 ], [ -2345905.603365985210985, 7849135.118691245093942 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T004", "vessel": "Vanua I", "survey_date": "2025-08-15T00:00:00", "crosses_antimeridian_flag": true, "length_m": 329398.56369995116 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2582407.215880405623466, 7785839.181658865883946 ], [ -2508233.587048316374421, 7823377.507483461871743 ] ], [ [ -2433906.98726980574429, 7858842.509372072294354 ], [ -2359990.324205769691616, 7895939.639540609903634 ], [ -2285968.765630466397852, 7931271.657026222907007 ], [ -2211945.76174630690366, 7965472.228640222921968 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T005", "vessel": "Bligh", "survey_date": "2025-08-19T00:00:00", "crosses_antimeridian_flag": true, "length_m": 337273.70270080183 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2730085.261179696302861, 7745776.327364278957248 ], [ -2661819.787075642962009, 7746489.832580281421542 ], [ -2593883.202560970094055, 7746760.809876747429371 ], [ -2526561.439846133813262, 7748375.548134338110685 ], [ -2459207.692624073475599, 7747464.851115341298282 ] ], [ [ -2392326.301384323276579, 7747142.532282743602991 ], [ -2325963.332817469257861, 7747812.099709572270513 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T006", "vessel": "Cakaulevu", "survey_date": "2025-08-13T00:00:00", "crosses_antimeridian_flag": true, "length_m": 362346.84209049615 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2599992.844046104233712, 7710152.494721526280046 ], [ -2535990.336608688347042, 7745663.094245363026857 ], [ -2471907.25352796446532, 7780096.101796898059547 ] ], [ [ -2408021.86162570444867, 7815198.070424247533083 ], [ -2343992.843024155590683, 7848789.023065808229148 ], [ -2280128.756940491497517, 7882913.996305481530726 ], [ -2216075.889947671908885, 7915112.088575566187501 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T007", "vessel": "Vanua I", "survey_date": "2025-08-19T00:00:00", "crosses_antimeridian_flag": true, "length_m": 368700.1434123707 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2707958.766985294874758, 7847662.46371332090348 ], [ -2617025.080956507474184, 7868560.6145184263587 ], [ -2526493.880338669288903, 7888512.674169298261404 ] ], [ [ -2436457.20139694865793, 7908226.102616477757692 ], [ -2346756.589748611208051, 7926750.916424177587032 ], [ -2257520.004844188690186, 7945076.104863223619759 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T008", "vessel": "Taveuni II", "survey_date": "2025-08-12T00:00:00", "crosses_antimeridian_flag": true, "length_m": 130059.39143118286 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2518182.288068630266935, 7779851.174654245376587 ], [ -2487017.445418053306639, 7789178.679573908448219 ], [ -2456022.297383859753609, 7799241.849767371080816 ] ], [ [ -2425166.019995928741992, 7809880.026913054287434 ], [ -2394257.700750786811113, 7819872.499677928164601 ], [ -2363415.248306550085545, 7829983.100506925955415 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T009", "vessel": "Vanua I", "survey_date": "2025-08-13T00:00:00", "crosses_antimeridian_flag": true, "length_m": 235691.09624016113 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2700142.167306652758271, 7911204.870228514075279 ], [ -2640985.695807782001793, 7916482.104912839829922 ], [ -2582072.975177893880755, 7921554.017558207735419 ], [ -2523489.242259116843343, 7927027.587514635175467 ] ], [ [ -2465150.952466951683164, 7932409.003623221069574 ], [ -2407078.076770232990384, 7937881.784215446561575 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T010", "vessel": "Vanua I", "survey_date": "2025-08-15T00:00:00", "crosses_antimeridian_flag": true, "length_m": 362782.57626249874 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2743004.244949294254184, 7825703.487932769581676 ], [ -2559206.512722389306873, 7831590.124320032075047 ] ], [ [ -2377995.683071207255125, 7836498.912986122071743 ], [ -2199142.433008896652609, 7840154.2901915544644 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T011", "vessel": "Taveuni II", "survey_date": "2025-08-20T00:00:00", "crosses_antimeridian_flag": false, "length_m": 85100.654515888978 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2090510.237545256037265, 7860563.718294717371464 ], [ -2104083.355092440266162, 7856360.095066314563155 ], [ -2117792.864098150283098, 7853092.239760281518102 ], [ -2131508.181665613316, 7849795.275641735643148 ], [ -2145064.558770742733032, 7845283.005933348089457 ], [ -2158751.349236745852977, 7841647.831423315219581 ], [ -2172374.122675319202244, 7837490.689390176907182 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T012", "vessel": "Taveuni II", "survey_date": "2025-08-20T00:00:00", "crosses_antimeridian_flag": false, "length_m": 96678.572314157165 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2881358.679590006358922, 7711997.136072006076574 ], [ -2881147.548506676219404, 7744723.712745074182749 ], [ -2880792.575384852476418, 7777199.373172726482153 ], [ -2880163.700538771227002, 7808666.804016925394535 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T013", "vessel": "Bligh", "survey_date": "2025-08-19T00:00:00", "crosses_antimeridian_flag": false, "length_m": 195242.20241007773 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2074347.516229145694524, 7874463.33793031796813 ], [ -2126661.583137985318899, 7835388.280761742964387 ], [ -2179084.771710069850087, 7797497.99072393681854 ], [ -2231266.005162506829947, 7758301.353639220818877 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T014", "vessel": "Lomaiviti", "survey_date": "2025-08-13T00:00:00", "crosses_antimeridian_flag": false, "length_m": 168768.7882640101 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2884554.542054513935, 7889146.833948561921716 ], [ -2843184.396975137759, 7880624.016131930053234 ], [ -2802044.054834145121276, 7872489.953316772356629 ], [ -2760519.013364502228796, 7861046.430769282393157 ], [ -2719749.847674670163542, 7853185.46901720110327 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T015", "vessel": "Vanua I", "survey_date": "2025-08-15T00:00:00", "crosses_antimeridian_flag": false, "length_m": 136075.16315041631 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2154859.157849435694516, 7981607.483935452997684 ], [ -2176367.295431756414473, 7974281.162554035894573 ], [ -2198072.658420251682401, 7968293.111701854504645 ], [ -2219700.689059948083013, 7961552.54660858400166 ], [ -2241249.826771907042712, 7954069.077079316601157 ], [ -2262853.85095702810213, 7946839.612698352895677 ], [ -2284666.763064748141915, 7940965.200932309031487 ] ] ] } },
3:04