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": "deepseek/deepseek-v4-flash",
"base_url": "https://openrouter.ai/api/v1"
}
0:04
Let me start by reading the input file to understand the data structure and identify the problematic geometries.
0:04
Read
/work/fiji_transects_wgs84.geojson
1 {
2 "type": "FeatureCollection",
3 "name": "fiji_transects_wgs84",
4 "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
5 "features": [
6 { "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 ] ] } },
7 { "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 ] ] } },
8 { "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 ] ] } },
9 { "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 ] ] } },
10 { "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 ] ] } },
11 { "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 ] ] } },
12 { "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 ] ] } },
13 { "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 ] ] } },
14 { "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 ] ] } },
15 { "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 ] ] } },
16 { "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 ] ] } },
17 { "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 ] ] } },
18 { "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 ] ] } },
19 { "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 ] ] } },
20 { "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 ] ] } },
21 { "type": "Feature", "properties": { "transect_id": "T016", "vessel": "Taveuni II", "survey_date": "2025-08-15", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ 178.445325115686984, -18.225562645051426 ], [ 178.570919872630384, -18.22187730118792 ], [ 178.696514629573755, -18.228085980840198 ], [ 178.822109386517127, -18.243164774607269 ], [ 178.947704143460527, -18.232257588046291 ] ] } },
22 { "type": "Feature", "properties": { "transect_id": "T017", "vessel": "Taveuni II", "survey_date": "2025-08-12", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ -176.949497668404376, -16.593233226112279 ], [ -177.073130399044231, -16.796136633276152 ], [ -177.196763129684086, -16.985064764093782 ], [ -177.320395860323913, -17.188172387851619 ], [ -177.444028590963768, -17.380278567766066 ], [ -177.567661321603623, -17.571298263401527 ], [ -177.691294052243478, -17.76827441812642 ] ] } },
23 { "type": "Feature", "properties": { "transect_id": "T018", "vessel": "Bligh", "survey_date": "2025-08-19", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ 176.111138114901024, -16.851117503263293 ], [ 176.291399614433374, -17.034309766495799 ], [ 176.471661113965695, -17.221302006070296 ], [ 176.651922613498016, -17.402317112686557 ], [ 176.832184113030365, -17.592994204367653 ] ] } },
24 { "type": "Feature", "properties": { "transect_id": "T019", "vessel": "Lomaiviti", "survey_date": "2025-08-12", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ 178.007516628261584, -17.706954766350957 ], [ 177.596962177324428, -17.607161226082525 ], [ 177.186407726387301, -17.511279680346377 ], [ 176.775853275450146, -17.418568907300433 ], [ 176.36529882451299, -17.326891370330944 ] ] } },
25 { "type": "Feature", "properties": { "transect_id": "T020", "vessel": "Taveuni II", "survey_date": "2025-08-13", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ 176.869640838143511, -17.085093978662378 ], [ 177.34023164697706, -17.269542693344249 ], [ 177.810822455810609, -17.442447410976666 ], [ 178.281413264644158, -17.624621823987695 ], [ 178.752004073477707, -17.807162082048386 ] ] } },
26 { "type": "Feature", "properties": { "transect_id": "T021", "vessel": "Vanua I", "survey_date": "2025-08-16", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ 176.258661225850091, -17.092877710432774 ], [ 176.830345357965598, -17.521807453385343 ], [ 177.402029490081105, -17.943438299004075 ], [ 177.973713622196613, -18.366984215963161 ] ] } },
27 { "type": "Feature", "properties": { "transect_id": "T022", "vessel": "Vanua I", "survey_date": "2025-08-15", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ 177.115699442929383, -17.820293640195572 ], [ 177.180354072206086, -17.755581112632651 ], [ 177.245008701482789, -17.684692899482695 ], [ 177.30966333075952, -17.61995755685173 ], [ 177.374317960036223, -17.544174647257226 ], [ 177.438972589312925, -17.479955835030527 ] ] } },
28 { "type": "Feature", "properties": { "transect_id": "T023", "vessel": "Lomaiviti", "survey_date": "2025-08-12", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ -177.191351883954098, -17.679938619650844 ], [ -177.334313691261116, -17.740394590021616 ], [ -177.477275498568133, -17.794367957957562 ], [ -177.62023730587515, -17.835119945356809 ], [ -177.763199113182168, -17.904282380225705 ], [ -177.906160920489185, -17.955635131096855 ] ] } },
29 { "type": "Feature", "properties": { "transect_id": "T024", "vessel": "Vanua I", "survey_date": "2025-08-12", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ -179.108274205314615, -16.84950325482971 ], [ -178.651703763804733, -16.962456436762313 ], [ -178.195133322294822, -17.077410225872036 ], [ -177.73856288078494, -17.187966865781235 ], [ -177.281992439275029, -17.288042922581088 ], [ -176.825421997765147, -17.40904519070677 ] ] } },
30 { "type": "Feature", "properties": { "transect_id": "T025", "vessel": "Taveuni II", "survey_date": "2025-08-19", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ 176.574749815324452, -18.225329195672629 ], [ 176.575711406506713, -17.903825702399828 ], [ 176.576672997688945, -17.572376190438085 ], [ 176.577634588871206, -17.245872893430239 ] ] } },
31 { "type": "Feature", "properties": { "transect_id": "T026", "vessel": "Cakaulevu", "survey_date": "2025-08-19", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ 178.630248956242411, -17.547785158041279 ], [ 178.44215451378426, -17.570057172060459 ], [ 178.254060071326109, -17.598148620651486 ], [ 178.065965628867986, -17.611819625668215 ], [ 177.877871186409834, -17.645164465599024 ], [ 177.689776743951683, -17.659546383089758 ] ] } },
32 { "type": "Feature", "properties": { "transect_id": "T027", "vessel": "Vanua I", "survey_date": "2025-08-12", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ -177.773526218774975, -17.183972526301677 ], [ -177.993798674497214, -17.290101550005915 ], [ -178.214071130219452, -17.383420029016879 ], [ -178.434343585941718, -17.487055860389813 ], [ -178.654616041663957, -17.584325384194859 ], [ -178.874888497386195, -17.694189986558115 ] ] } },
33 { "type": "Feature", "properties": { "transect_id": "T028", "vessel": "Vanua I", "survey_date": "2025-08-12", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ -178.154921880223981, -16.519500569812283 ], [ -177.760867938660567, -16.61473611015824 ], [ -177.366813997097154, -16.706948361526067 ], [ -176.972760055533769, -16.779276042892469 ], [ -176.578706113970355, -16.874986722588798 ] ] } },
34 { "type": "Feature", "properties": { "transect_id": "T029", "vessel": "Lomaiviti", "survey_date": "2025-08-19", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ -176.563841265411412, -18.091246216590591 ], [ -176.943316553982299, -17.796168551154441 ], [ -177.322791842553215, -17.510273970538908 ], [ -177.702267131124103, -17.231705179840109 ], [ -178.08174241969499, -16.949176694851047 ] ] } },
35 { "type": "Feature", "properties": { "transect_id": "T030", "vessel": "Cakaulevu", "survey_date": "2025-08-19", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ -177.772700226865993, -16.898671712097919 ], [ -177.981739053528088, -17.042373280579991 ], [ -178.190777880190183, -17.175821795383495 ], [ -178.399816706852278, -17.32978967298839 ], [ -178.608855533514344, -17.474383681681747 ], [ -178.817894360176439, -17.613581860280195 ], [ -179.026933186838534, -17.755873350663471 ] ] } }
36 ]
37 }
0:56
Let me analyze the data. The problem is that some transects cross the antimeridian (180° longitude). When a LineString has coordinates that straddle the antimeridian, GIS software can incorrectly render the line as wrapping the entire globe instead of taking the short path.
For example, in T001, coordinates go from 179.97 to -179.51 — that's crossing the antimeridian. The correct path should go from 179.97 eastward past 180 to -179.51 (a short hop), but software might think it goes the long way around.
The fix: I need to identify transects that cross the antimeridian and split them into multiple segments where the longitude jumps across the antimeridian. Then offset the longitudes on one side by adding/subtracting 360 to make them continuous.
Let me plan:
1. For each transect that crosses the antimeridian, I need to find the split points where longitudes jump from near +180 to near -180 (or vice versa)
2. At those split points, I need to create separate line parts, then normalize coordinates back to -180..180 range
3. For the antimeridian-crossing ones, I'll need to break them into multi-part geometries
Let me think about the algorithm:
For each coordinate pair, track longitude. If two consecutive points have a longitude difference > 180 (or < -180), that indicates crossing the antimeridian.
Actually, a better approach: the transects are all in the Fiji region. If I look at coordinates that cross from positive to negative across 180, I need to handle them by:
1. Detecting when consecutive longitudes differ by more than ~180 degrees (indicating crossing antimeridian)
2. Splitting the line at that point
3. For coordinates on one side, adding/subtracting 360 to make them continuous within their segment
Then each segment gets its own LineString in a MultiLineString geometry.
Let me also think about the non-antimeridian-crossing ones: they should still be MultiLineString as per the requirement.
Let me write a Python script to do this.
Fiji's national metric grid — that would be Fiji 1986 / Fiji Map Grid (EPSG:3460) or something similar. Let me check. The Fiji Map Grid (FMG) is EPSG:3460 for the southern hemisphere.
Actually, let me verify. Fiji 1986 / Fiji Map Grid (EPSG:3460) - "Fiji 1986 / Fiji Map Grid" is a projected CRS with a central meridian of 178°E (actually 178°45'E for the south and 179°20'E for the north... let me check).
Actually, EPSG:3460 is "Fiji 1986 / Fiji Map Grid" with params:
- projection: Transverse Mercator
- Central meridian: 178°45'E (for South) and 179°20'E (for North)
- Scale factor: 0.9998
- False easting: 2000000
- False northing: 4000000
Wait, I need to check more carefully. Let me look it up.
Fiji 1986 / Fiji Map Grid - EPSG:3460
- Method: Transverse Mercator
- Latitude of natural origin: 0°
- Central meridian: 178°45' E (178.75°) — for the South chain...
Actually, I think there are two zones: North and South.
Let me just check in code. Actually EPSG:3460 is the standard one for Fiji. Let me use that.
Let me write the script now.
Key steps:
1. Read the GeoJSON
2. For each feature:
a. Extract coordinates from LineString
b. Check if it crosses the antimeridian
c. If it does, split into segments at the antimeridian crossings
d. For each segment, normalize longitudes (add 360 to negative ones or subtract 360 from positive ones to make them contiguous)
e. Convert each segment to a LineString
f. Combine into MultiLineString
g. Project to EPSG:3460
h. Calculate length
i. Keep attributes
3. Write output GeoJSON
For normalization: when crossing the antimeridian, I'll shift coordinates to the "other side" of the antimeridian. A common approach:
Split points into sequences where consecutive longitudes don't differ by more than 180. For each segment, if all longitudes are negative (west of antimeridian), leave them; if all longitudes are positive (east of antimeridian), leave them. The issue is that a point at e.g. -179.5 and 179.5 are close but have different signs.
Better approach:
1. Find split indices where abs(lon2 - lon1) > 180
2. For each group of consecutive points, decide whether to add 360 to negative values (to make them continuous with positive values) or subtract 360 from positive values (to make them continuous with negative values)
Actually, let me think more carefully. The survey path crosses the antimeridian going from east to west (or west to east). The actual path should be the short route.
Let me take T001 as an example:
[ 178.94, -17.63 ]
[ 179.46, -17.62 ]
[ 179.97, -17.61 ]
[ -179.51, -17.60 ]
[ -179.00, -17.58 ]
[ -178.48, -17.58 ]
The longitude goes: 178.94 → 179.46 → 179.97 → -179.51 → -179.00 → -178.48
The jump from 179.97 to -179.51 is a change of about -359.48 degrees. This means the survey actually crossed from just east of 180 to just west of 180.
So the actual path is: ...179.97 (just east of 180) -> 180.49 (just west of 180 in continuous terms) ... no wait, -179.51 = 180.49 if we add 360.
So the approach should be: when a longitude decreases by more than ~180 (e.g. from 179.97 to -179.51), the ship crossed the antimeridian east-to-west. We should add 360 to the subsequent longitudes to make them continuous.
Actually, I think a cleaner approach is:
For each feature, process its coordinates:
1. Iterate through coordinates
2. If a longitude jumps by more than 180 in absolute value, start a new segment
3. Within each segment, normalize longitudes so they're continuous (add/subtract 360 as needed)
4. Then each segment in its normalized form can be converted to Fiji grid and measured
Wait, but when I project the split segments, each segment should be in the correct geographic position. If I have one segment east of the antimeridian and another west, they'll be on opposite sides when projected. So I should project them separately.
Let me think about this differently. The simple approach:
For each feature:
1. Iterate through coordinates
2. Track "segment" starting at index 0
3. When abs(lon[i] - lon[i-1]) > 180, split. The point at index i-1 ends one segment, point at i starts another
4. For each segment, normalize coordinates so they're in a continuous range
For normalization within a segment:
- If all longitudes are > 0 (east of antimeridian), leave them
- If all longitudes are < 0 (west of antimeridian), leave them
- If mixed (which shouldn't happen within a segment if we split correctly), add 360 to negative ones
Wait, but the split point itself... Let me trace through T001:
Points:
0: [178.94, -17.63]
1: [179.46, -17.62]
2: [179.97, -17.61]
3: [-179.51, -17.60]
4: [-179.00, -17.58]
5: [-178.48, -17.58]
Split at index 2-3: |179.97 - (-179.51)| = 359.48 > 180
So segment 1: points 0,1,2 — all longitudes positive — leave as-is
Segment 2: points 3,4,5 — all longitudes negative — leave as-is
But when I project segment 1 (east of antimeridian) to Fiji grid, it'll be at far eastern Fiji
And segment 2 (west of antimeridian) will be at western Fiji
And the gap between them is very small (just across 180), which is correct geographically!
So the approach works: just split at crossings, no normalization needed for individual segments, because each segment will be fully on one side of 180.
But wait, there's a subtlety. For T003:
[177.64, -17.36]
[178.46, -17.42]
[179.29, -17.47]
[-179.89, -17.53]
[-179.06, -17.59]
Split between index 2 and 3: 179.29 to -179.89, diff = -359.18
Segment 1: 177.64, 178.46, 179.29 (all positive)
Segment 2: -179.89, -179.06 (both negative)
Good, that works.
Now for T004:
[178.87, -17.80]
[179.54, -17.60]
[-179.79, -17.41]
[-179.12, -17.19]
[-178.45, -16.99]
[-177.78, -16.80]
Split between index 1 and 2: 179.54 to -179.79, diff = -359.33
Segment 1: 178.87, 179.54 (positive)
Segment 2: -179.79, -179.12, -178.45, -177.78 (all negative)
Good.
For T005:
[177.60, -17.92]
[178.16, -18.01]
[178.72, -18.10]
[179.29, -18.18]
[179.85, -18.27]
[-179.58, -18.36]
[-179.02, -18.44]
Split between index 4 and 5: 179.85 to -179.58, diff = -359.43
Segment 1: 177.60, 178.16, 178.72, 179.29, 179.85 (positive)
Segment 2: -179.58, -179.02 (negative)
Good.
T006:
[178.62, -18.38]
[179.21, -18.18]
[179.79, -17.99]
[-179.63, -17.79]
[-179.04, -17.60]
[-178.46, -17.39]
[-177.88, -17.20]
Split between index 2 and 3: 179.79 to -179.63, diff = -359.42
Segment 1: 178.62, 179.21, 179.79 (positive)
Segment 2: -179.63, -179.04, -178.46, -177.88 (negative)
T007:
[177.92, -17.15]
[178.70, -17.10]
[179.47, -17.05]
[-179.75, -17.00]
[-178.97, -16.96]
[-178.19, -16.91]
Split between index 2 and 3: 179.47 to -179.75, diff = -359.22
Segment 1: 177.92, 178.70, 179.47 (positive)
Segment 2: -179.75, -178.97, -178.19 (negative)
T008:
[179.40, -17.93]
[179.67, -17.90]
[179.95, -17.86]
[-179.78, -17.81]
[-179.51, -17.77]
[-179.23, -17.72]
Split between index 2 and 3: 179.95 to -179.78, diff = -359.73
Segment 1: 179.40, 179.67, 179.95 (positive)
Segment 2: -179.78, -179.51, -179.23 (negative)
T009:
[178.07, -16.66]
[178.56, -16.69]
[179.05, -16.72]
[179.55, -16.75]
[-179.96, -16.78]
[-179.47, -16.80]
Split between index 3 and 4: 179.55 to -179.96, diff = -359.51
Segment 1: 178.07, 178.56, 179.05, 179.55 (positive)
Segment 2: -179.96, -179.47 (negative)
T010:
[177.60, -17.28]
[179.13, -17.47]
[-179.35, -17.65]
[-177.82, -17.84]
Split between index 1 and 2: 179.13 to -179.35, diff = -358.48
Segment 1: 177.60, 179.13 (positive)
Segment 2: -179.35, -177.82 (negative)
Good, all the antimeridian-crossing ones have a simple pattern: they go from positive (east of 180) to negative (west of 180) with a single crossing.
Now for the Fiji Map Grid CRS. Let me check what's available. EPSG:3460 is Fiji 1986 / Fiji Map Grid. Let me verify by looking at pyproj's CRS database.
Actually, let me also check if there's a more appropriate CRS. Fiji 1986 / Fiji Map Grid (EPSG:3460) should be the standard.
Let me write the script now.
Actually, for the non-antimeridian transects, I still need to output them as MultiLineString features (with a single LineString in the MultiLineString).
Let me code this up.
```python
import geopandas as gpd
import pandas as pd
from shapely.geometry import LineString, MultiLineString, shape
from shapely.ops import transform
import pyproj
import json
# Read the GeoJSON
with open('/work/fiji_transects_wgs84.geojson', 'r') as f:
data = json.load(f)
features = data['features']
# Define CRS
wgs84 = pyproj.CRS('EPSG:4326')
fmg = pyproj.CRS('EPSG:3460') # Fiji 1986 / Fiji Map Grid
transformer = pyproj.Transformer.from_crs(wgs84, fmg, always_xy=True)
def split_at_antimeridian(coords):
"""Split a list of coordinate tuples at antimeridian crossings.
Returns a list of lists, each inner list being a continuous segment."""
if not coords:
return []
segments = []
current_segment = [coords[0]]
for i in range(1, len(coords)):
prev_lon, prev_lat = coords[i-1]
curr_lon, curr_lat = coords[i]
# Check if crossing antimeridian (longitude jump > 180)
if abs(curr_lon - prev_lon) > 180:
# Start a new segment
segments.append(current_segment)
current_segment = [coords[i]]
else:
current_segment.append(coords[i])
segments.append(current_segment)
return segments
def project_coords(coords, transformer_obj):
"""Project a list of (lon, lat) tuples to the target CRS."""
projected = []
for lon, lat in coords:
x, y = transformer_obj.transform(lon, lat)
projected.append((x, y))
return projected
def calculate_length(projected_coords):
"""Calculate 2D length from projected coordinates."""
total = 0.0
for i in range(1, len(projected_coords)):
x1, y1 = projected_coords[i-1]
x2, y2 = projected_coords[i]
dx = x2 - x1
dy = y2 - y1
total += (dx*dx + dy*dy) ** 0.5
return total
output_features = []
for feature in features:
props = feature['properties'].copy()
geom = feature['geometry']
coords = geom['coordinates']
# Split at antimeridian
segments = split_at_antimeridian(coords)
# Project each segment and create LineStrings
projected_lines = []
for seg in segments:
projected_seg = project_coords(seg, transformer)
if len(projected_seg) >= 2:
line = LineString(projected_seg)
projected_lines.append(line)
# Create MultiLineString
multiline = MultiLineString(projected_lines)
# Calculate total length
total_length = 0.0
for seg in segments:
projected_seg = project_coords(seg, transformer)
total_length += calculate_length(projected_seg)
props['length_m'] = round(total_length, 2)
# Remove the flag if present (not requested to carry through... actually keep all attributes)
# Actually the instruction says "Carry the survey attributes (vessel and survey_date) through unchanged"
# Let me keep transect_id, vessel, survey_date and length_m
# But also remove crosses_antimeridian_flag if present as it wasn't mentioned
# Actually, let me just keep all the original properties plus length_m
# But crosses_antimeridian_flag might not be needed. The instructions say carry "vessel and survey_date" through.
# Let me keep transect_id as key and all original properties plus length_m
out_feature = {
'type': 'Feature',
'properties': props,
'geometry': {
'type': 'MultiLineString',
'coordinates': [list(seg) for seg in multiline.geoms]
}
}
# But wait, the coordinates need to be in the projected CRS for the output
# Actually no, we want the output in FMG (projected) format
# So the geometry coordinates should be projected coordinates
output_features.append(out_feature)
# Create output GeoJSON
output = {
'type': 'FeatureCollection',
'name': 'fiji_transects_fmg',
'crs': {
'type': 'name',
'properties': {'name': 'urn:ogc:def:crs:EPSG::3460'}
},
'features': output_features
}
with open('/work/fiji_transects_fmg.geojson', 'w') as f:
json.dump(output, f, indent=2)
print(f"Wrote {len(output_features)} features to fiji_transects_fmg.geojson")
```
Wait, I realize there's an issue. The coordinates stored in the MultiLineString need to be projected coordinates (in FMG), not WGS84. Let me fix that.
Also, I should be more careful about how I construct the output geometry.
Let me also double-check: the problem description says the transects "render as arcs spanning the entire globe instead of short local segments." This confirms the antimeridian issue.
Let me refine the script and also check the EPSG:3460 CRS exists.
Actually, let me first verify EPSG:3460.
Let me also think about whether to use always_xy=True. For pyproj.Transformer.from_crs, when going from EPSG:4326 to EPSG:3460, with always_xy=True, the input should be (lon, lat) and output will be (x, y). That's what we want.
Let me write a cleaner script.
Also, for non-antimeridian features, they'll have just one segment, so the MultiLineString will have one LineString. That's fine.
Let me also handle the case where the output GeoJSON's coordinates need to be in the projected CRS. Let me structure the output properly.
0:56
0:56
Bash
python3 -c "import pyproj; c=pyproj.CRS('EPSG:3460'); print(c.name); print(c.to_...
Fiji 1986 / Fiji Map Grid
PROJCRS["Fiji 1986 / Fiji Map Grid",
BASEGEOGCRS["Fiji 1986",
DATUM["Fiji Geodetic Datum 1986",
ELLIPSOID["WGS 72",6378135,298.26,
LENGTHUNIT["metre",1]]],
PRIMEM["Greenwich",0,
ANGLEUNIT["degree",0.0174532925199433]],
ID["EPSG",4720]],
CONVERSION["Fiji Map Grid",
METHOD["Transverse Mercator",
ID["EPSG",9807]],
PARAMETER["Latitude of natural origin",-17,
ANGLEUNIT["degree",0.0174532925
1:40
Good, EPSG:3460 is Fiji 1986 / Fiji Map Grid. Now let me write the solution script.
The key issue: transects that cross the antimeridian have coordinates like [178, 179, -179, -178] — the longitude jumps across 180. I need to split them into segments where each segment stays on one side of 180.
Actually, the approach should be: for each coordinate pair, if the longitude crosses 180 (i.e., one point has positive longitude and the next has negative, or vice versa), I need to insert an intermediate point at the antimeridian (180° longitude) using interpolation, then split the line into separate parts at those crossing points.
Let me think more carefully. The antimeridian for Fiji is at 180°. The coordinates range from about -180 to +180. So the crossing happens when longitude goes from positive to negative or vice versa.
For a simple split approach:
1. Iterate through pairs of consecutive points
2. If both are on the same side of 180, add the point to the current segment
3. If they cross 180, interpolate the crossing point, end the current segment, start a new segment with the crossing point + the next point
For interpolation: we have (lon1, lat1) and (lon2, lat2) where lon1 > 0 and lon2 < 0 (or vice versa).
The antimeridian crossing is at lon = 180 (or -180, same thing).
We need to find the latitude at the crossing using linear interpolation.
Actually let me think about this differently. The standard approach for splitting at the antimeridian:
For each pair (p1, p2):
- If abs(lon1 - lon2) < 180, they're on the same side - no crossing
- If abs(lon1 - lon2) >= 180, there's a crossing
When crossing from positive to negative:
- lon1 > 0 and lon2 < 0: crossing at lon=180
- lon1 < 0 and lon2 > 0: crossing at lon=-180
But really, the antimeridian is at ±180°, so both are the same line. Let me use a consistent approach.
For the interpolation:
- Normalize longitudes so we're working in a continuous space
- lon1 normalized to itself (say 179)
- lon2 normalized to lon2 + 360 if it's negative and lon1 is positive (say -179 + 360 = 181)
- Then find t = (180 - lon1_norm) / (lon2_norm - lon1_norm)
- lat_at_180 = lat1 + t * (lat2 - lat1)
Then the segments are:
- Segment 1: from p1 to (180, lat_at_180)
- Segment 2: from (-180, lat_at_180) to p2 (or from (180, lat_at_180) to p2 if we shift p2)
Actually, let me keep the coordinates as (-180, lat) for simplicity since both 180 and -180 are the same line.
Wait, but for the projection, the Fiji Map Grid handles coordinates around Fiji. Let me just work with the original longitudes and adjust coordinates near the antimeridian properly.
Let me write a clean solution:
```python
import geopandas as gpd
import shapely.geometry as geom
import shapely.ops as ops
import numpy as np
def split_at_antimeridian(line):
"""Split a LineString that crosses the antimeridian into segments."""
coords = list(line.coords)
segments = []
current_segment = [coords[0]]
for i in range(1, len(coords)):
lon1, lat1 = coords[i-1]
lon2, lat2 = coords[i]
# Check if crossing the antimeridian
# Crosses if going from + to - or - to + (with a big jump)
if (lon1 > 0 and lon2 < 0) or (lon1 < 0 and lon2 > 0):
# Interpolate the crossing point at lon = 180 (or -180)
# Normalize: make the crossing continuous
if lon1 > 0 and lon2 < 0:
lon2_norm = lon2 + 360
else: # lon1 < 0 and lon2 > 0
lon1_norm = lon1 + 360
lon2_norm = lon2
if lon1 > 0 and lon2 < 0:
# crossing from 179 to -179 -> normalized 179 to 181
lon1_norm = lon1
lon2_norm = lon2 + 360
t = (180 - lon1_norm) / (lon2_norm - lon1_norm)
lat_cross = lat1 + t * (lat2 - lat1)
current_segment.append((180.0, lat_cross))
segments.append(geom.LineString(current_segment))
current_segment = [(-180.0, lat_cross), (lon2, lat2)]
else: # crossing from -179 to 179
lon1_norm = lon1 + 360
lon2_norm = lon2
t = (180 - lon1_norm) / (lon2_norm - lon1_norm)
lat_cross = lat1 + t * (lat2 - lat1)
current_segment.append((-180.0, lat_cross))
segments.append(geom.LineString(current_segment))
current_segment = [(180.0, lat_cross), (lon2, lat2)]
else:
current_segment.append((lon2, lat2))
segments.append(geom.LineString(current_segment))
if len(segments) == 1:
return segments[0]
else:
return geom.MultiLineString(segments)
```
Wait, let me re-think. For the case where we go from -179 to 179 (crossing the other way):
lon1 = -179, lon2 = 179
lon1_norm = -179 + 360 = 181
lon2_norm = 179
t = (180 - 181) / (179 - 181) = (-1) / (-2) = 0.5
lat_cross = lat1 + 0.5 * (lat2 - lat1)
So the crossing point at lon=180 has lat_cross. Then:
- First segment: from (-179, lat1) to (180, lat_cross) ... wait that doesn't make sense since -179 to 180 goes through 179, not through the antimeridian.
Hmm, let me think again. The issue is about direction. Going from lon=-179 to lon=179: you don't cross the antimeridian going eastward (through longitude 0), you cross it going westward through 180.
Actually, the jump from -179 to 179 is a jump of 358 degrees in one direction. The short path would go through 180. So lon1=-179, lon2=179, the short path goes from -179 → -180 (which is 180) → 179.
Wait actually, -179 to 179 going westward: -179 → -180(180) → 179. So yes this is a crossing.
But in my code above, for -179 to 179:
lon1_norm = -179 + 360 = 181
lon2_norm = 179
t = (180 - 181) / (179 - 181) = -1 / -2 = 0.5
lat at crossing: lat1 + 0.5*(lat2-lat1) — correct.
Segments: first from (-179, lat1) to (-180, lat_cross)... wait, I put (-180.0, lat_cross) as the split point.
Second from (180.0, lat_cross)...
Hmm, this is getting confusing. Let me simplify.
Actually, the crossing at 180° is the same line regardless of whether we call it 180 or -180. So let me just always use 180 for the representation after splitting.
Let me re-think the whole approach more carefully.
For case 1: lon1 > 0, lon2 < 0 (e.g., 179 → -179)
- The short path goes eastward: 179 → 180 → -179
- So we interpolate at lon=180
- Segment 1: 179 → 180 (eastward)
- Segment 2: -179 (which is 180 in the other direction) → -179... wait, segment 2 starts at 180 (or -180) and goes to -179
For case 2: lon1 < 0, lon2 > 0 (e.g., -179 → 179)
- The short path goes westward: -179 → -180(180) → 179
- Interpolate at lon=-180 (same as 180)
- Segment 1: -179 → -180(180)
- Segment 2: 180 → 179
So both cases interpolate at the antimeridian (lon=±180), and in both cases we add a point at the antimeridian and split.
Let me just simplify:
```python
def fix_antimeridian(line):
coords = list(line.coords)
# Check if there's a crossing
has_crossing = False
for i in range(1, len(coords)):
lon1, lat1 = coords[i-1]
lon2, lat2 = coords[i]
if abs(lon1 - lon2) > 180:
has_crossing = True
break
if not has_crossing:
return line
parts = []
current_part = [coords[0]]
for i in range(1, len(coords)):
lon1, lat1 = coords[i-1]
lon2, lat2 = coords[i]
if abs(lon1 - lon2) > 180: # Crossing
# Normalize so the path goes through 180
if lon1 > 0:
# Going east: lon1 > 0, lon2 < 0
lon2_norm = lon2 + 360
t = float(180 - lon1) / float(lon2_norm - lon1)
lat_cross = lat1 + t * float(lat2 - lat1)
current_part.append((180.0, lat_cross))
parts.append(geom.LineString(current_part))
current_part = [(-180.0, lat_cross), (lon2, lat2)]
else:
# Going west: lon1 < 0, lon2 > 0
lon1_norm = lon1 + 360
t = float(180 - lon1_norm) / float(lon2 - lon1_norm)
lat_cross = lat1 + t * float(lat2 - lat1)
current_part.append((-180.0, lat_cross))
parts.append(geom.LineString(current_part))
current_part = [(180.0, lat_cross), (lon2, lat2)]
else:
current_part.append((lon2, lat2))
parts.append(geom.LineString(current_part))
if len(parts) == 1:
return parts[0]
return geom.MultiLineString(parts)
```
Hmm wait, for case 1 (lon1 > 0, lon2 < 0):
lon1 = 179, lon2 = -179
lon2_norm = -179 + 360 = 181
t = (180 - 179) / (181 - 179) = 1/2 = 0.5
lat_cross = lat1 + 0.5 * (lat2 - lat1)
Segment 1: (179, lat1) → (180, lat_cross)
Segment 2: (-180, lat_cross) → (-179, lat2)
This looks correct! The short path goes 179 → 180 → (through antimeridian) → -179.
For case 2 (lon1 < 0, lon2 > 0):
lon1 = -179, lon2 = 179
lon1_norm = -179 + 360 = 181
t = (180 - 181) / (179 - 181) = (-1)/(-2) = 0.5
lat_cross = lat1 + 0.5*(lat2 - lat1)
Segment 1: (-179, lat1) → (-180, lat_cross)
Segment 2: (180, lat_cross) → (179, lat2)
This also looks correct! Short path goes -179 → -180(180) → 179.
Now the longitudes -180 and 180 both represent the same antimeridian line, so this should work fine.
Let me also think about what happens when a point is exactly at ±180. For the data I see, coordinates are like 179.xxx and -179.xxx so no exact 180.
Let me also consider edge cases:
- What if a segment has only one coordinate? Unlikely with Linestrings.
- What about precision? Using 180.0 as the crossing point should be fine.
Now for the Fiji Map Grid projection and length calculation. EPSG:3460 is the correct Fiji Map Grid. Then I need to compute length in metres.
Let me write the complete script:
```python
import geopandas as gpd
from shapely.geometry import LineString, MultiLineString
import numpy as np
# Read input
gdf = gpd.read_file('/work/fiji_transects_wgs84.geojson')
def fix_antimeridian_line(line):
"""Split a LineString that crosses the antimeridian into proper segments."""
coords = list(line.coords)
# Check if this line crosses the antimeridian
has_crossing = False
for i in range(1, len(coords)):
if abs(coords[i][0] - coords[i-1][0]) > 180:
has_crossing = True
break
if not has_crossing:
return line
parts = []
current_part = [coords[0]]
for i in range(1, len(coords)):
lon1, lat1 = coords[i-1]
lon2, lat2 = coords[i]
if abs(lon1 - lon2) > 180: # Crossing the antimeridian
if lon1 > 0: # Going eastward across the antimeridian
lon2_norm = lon2 + 360
t = float(180 - lon1) / float(lon2_norm - lon1)
lat_cross = lat1 + t * float(lat2 - lat1)
current_part.append((180.0, lat_cross))
parts.append(LineString(current_part))
current_part = [(-180.0, lat_cross), (lon2, lat2)]
else: # Going westward across the antimeridian
lon1_norm = lon1 + 360
t = float(180 - lon1_norm) / float(lon2 - lon1_norm)
lat_cross = lat1 + t * float(lat2 - lat1)
current_part.append((-180.0, lat_cross))
parts.append(LineString(current_part))
current_part = [(180.0, lat_cross), (lon2, lat2)]
else:
current_part.append((lon2, lat2))
parts.append(LineString(current_part))
if len(parts) == 1:
return parts[0]
return MultiLineString(parts)
# Fix geometries
gdf['geometry'] = gdf['geometry'].apply(fix_antimeridian_line)
# Project to Fiji Map Grid (EPSG:3460)
gdf = gdf.set_crs(epsg=4326)
gdf = gdf.to_crs(epsg=3460)
# Calculate length in metres
gdf['length_m'] = gdf.geometry.length
# Ensure all features are MultiLineString
from shapely.geometry import MultiLineString as MLS
def ensure_multilinestring(geom):
if geom.geom_type == 'LineString':
return MLS([geom])
return geom
gdf['geometry'] = gdf['geometry'].apply(ensure_multilinestring)
# Keep only desired columns
gdf = gdf[['transect_id', 'vessel', 'survey_date', 'length_m', 'geometry']]
# Write output
gdf.to_file('/work/fiji_transects_fmg.geojson', driver='GeoJSON')
```
Let me check - do I need to worry about the `crosses_antimeridian_flag` field? The user says "Carry the survey attributes (vessel and survey_date) through unchanged, use transect_id as the key field". So I should carry transect_id, vessel, survey_date through. I won't carry crosses_antimeridian_flag since it wasn't mentioned, but actually it's a survey attribute too... Let me re-read: "Carry the survey attributes (vessel and survey_date) through unchanged". So only vessel and survey_date are mentioned.
Wait, the user also says "use transect_id as the key field". So transect_id is carried through too. Let me include:
- transect_id
- vessel
- survey_date
- length_m (computed)
I'll drop crosses_antimeridian_flag since it's not mentioned and was about the issue we're fixing.
Let me write the script and run it.
1:40
1:40
Write
/work/solve.py
content · 99 lines · py
import geopandas as gpd
from shapely.geometry import LineString, MultiLineString
import numpy as np
# ── Read input ──────────────────────────────────────────────────────
gdf = gpd.read_file("/work/fiji_transects_wgs84.geojson")
print(f"Read {len(gdf)} features")
# ── Fix antimeridian crossings ──────────────────────────────────────
def fix_antimeridian_line(line):
"""
Split a LineString that crosses the antimeridian (lon jump > 180°)
into proper segments, each staying on one side of 180°.
Returns LineString (no crossing) or MultiLineString (crossing fixed).
"""
coords = list(line.coords)
# Detect crossing
has_crossing = False
for i in range(1, len(coords)):
if abs(coords[i][0] - coords[i - 1][0]) > 180:
has_crossing = True
break
if not has_crossing:
return line
parts = []
current_part = [coords[0]]
for i in range(1, len(coords)):
lon1, lat1 = coords[i - 1]
lon2, lat2 = coords[i]
if abs(lon1 - lon2) > 180: # crossing the antimeridian
if lon1 > 0:
# Eastward crossing: lon1 (+179) → 180 → lon2+360 (+181)
lon2_norm = lon2 + 360
t = float(180 - lon1) / float(lon2_norm - lon1)
lat_cross = lat1 + t * float(lat2 - lat1)
current_part.append((180.0, lat_cross))
parts.append(LineString(current_part))
current_part = [(-180.0, lat_cross), (lon2, lat2)]
else:
# Westward crossing: lon1_norm (+181) → 180 → lon2 (+179)
lon1_norm = lon1 + 360
t = float(180 - lon1_norm) / float(lon2 - lon1_norm)
lat_cross = lat1 + t * float(lat2 - lat1)
current_part.append((-180.0, lat_cross))
parts.append(LineString(current_part))
current_part = [(180.0, lat_cross), (lon2, lat2)]
else:
current_part.append((lon2, lat2))
parts.append(LineString(current_part))
if len(parts) == 1:
return parts[0]
return MultiLineString(parts)
# Apply the fix
gdf["geometry"] = gdf["geometry"].apply(fix_antimeridian_line)
# Sanity check: count MultiLineStrings vs LineStrings
type_counts = gdf.geometry.geom_type.value_counts()
print(f"Geometry types after fix: {dict(type_counts)}")
# ── Project to Fiji Map Grid (EPSG:3460) ────────────────────────────
gdf = gdf.set_crs(epsg=4326)
gdf = gdf.to_crs(epsg=3460)
print(f"Projected to Fiji Map Grid")
# ── Calculate length in metres ──────────────────────────────────────
gdf["length_m"] = gdf.geometry.length.round(1)
# ── Ensure every feature is a MultiLineString ───────────────────────
def to_multilinestring(geom):
if geom.geom_type == "LineString":
return MultiLineString([geom])
return geom
gdf["geometry"] = gdf["geometry"].apply(to_multilinestring)
# Confirm all are now MultiLineString
type_counts2 = gdf.geometry.geom_type.value_counts()
print(f"Geometry types final: {dict(type_counts2)}")
# ── Keep only the requested columns ─────────────────────────────────
gdf = gdf[["transect_id", "vessel", "survey_date", "length_m", "geometry"]]
# ── Write output ────────────────────────────────────────────────────
gdf.to_file("/work/fiji_transects_fmg.geojson", driver="GeoJSON")
print("Written fiji_transects_fmg.geojson")
# ── Quick validation ────────────────────────────────────────────────
print(f"\nFeature count: {len(gdf)}")
print(f"\nLengths (m):")
print(gdf[["transect_id", "length_m"]].to_string(index=False))
Wrote /work/solve.py (3590 chars).
1:52
Let me run the script to see the results.
1:52
Bash
cd /work && python solve.py
Read 30 features
Geometry types after fix: {'LineString': np.int64(20), 'MultiLineString': np.int64(10)}
Projected to Fiji Map Grid
Geometry types final: {'MultiLineString': np.int64(30)}
Written fiji_transects_fmg.geojson
Feature count: 30
Lengths (m):
transect_id length_m
T001 273335.1
T002 216575.3
T003 351823.5
T004 372666.1
T005 362394.6
T006 393605.6
T007 414594.4
T008 146792.5
T009 263388.3
T010 489842.5
T011 78492.2
T012 84537.6
T013 179800.5
T014 148392.9
T015 124738.2
T016 53310.2
T017 152432.4
T018 112421.6
T019 179435.7
T020 215352.2
T021 230219.2
T022 50981.8
T023 81980.1
T024 251074.9
T025 108459.1
T026 100661.5
T027 130089.7
T028 173047.0
T029 205262.6
T030 163829.4
2:04
Good, the output looks correct. Let me verify the output file: 1. Read it back and check that all features are MultiLineString 2. Check the CRS 3. Check the attributes Let me also do a quick manual sanity check on one of the antimeridian-crossing transects to make sure the fix is correct.
2:04
2:04
Bash
cd /work && python3 -c "
cd /work && python3 -c "
import json
with open('fiji_transects_fmg.geojson') as f:
data = json.load(f)
fc = data['features']
print(f'Total features: {len(fc)}')
types = set()
ids = set()
for feat in fc:
types.add(feat['geometry']['type'])
ids.add(feat['properties']['transect_id'])
print(f'Geometry types: {types}')
print(f'Transect IDs: {sorted(ids)}')
print(f'CRS: {data.get(\"crs\", \"not set\")}')
# Check T001 (antimeridian-crossing) has multi-part
for feat in fc:
if feat['properties']['transect_id'] == 'T001':
geom = feat['geometry']
print(f'\\nT001 geometry type: {geom[\"type\"]}')
print(f'Number of parts: {len(geom[\"coordinates\"])}')
props = feat['properties']
print(f'Properties: {props}')
break
# Check T011 (non-crossing) is MultiLineString with 1 part
for feat in fc:
if feat['properties']['transect_id'] == 'T011':
geom = feat['geometry']
print(f'\\nT011 geometry type: {geom[\"type\"]}')
print(f'Number of parts: {len(geom[\"coordinates\"])}')
print(f'Properties: {feat[\"properties\"]}')
break
"
Total features: 30
Geometry types: {'MultiLineString'}
Transect IDs: ['T001', 'T002', 'T003', 'T004', 'T005', 'T006', 'T007', 'T008', 'T009', 'T010', 'T011', 'T012', 'T013', 'T014', 'T015', 'T016', 'T017', 'T018', 'T019', 'T020', 'T021', 'T022', 'T023', 'T024', 'T025', 'T026', 'T027', 'T028', 'T029', 'T030']
CRS: {'type': 'name', 'properties': {'name': 'urn:ogc:def:crs:EPSG::3460'}}
T001 geometry type: MultiLineString
Number of parts: 2
Properties: {'transect_id': 'T001', 'vessel': 'Taveuni II', 'survey_date': '2025-08-15T00:00:00', 'length_m': 273335.1}
T011 geometry type: MultiLineString
Number of parts: 1
Properties: {'transect_id': 'T011', 'vessel': 'Taveuni II', 'survey_date': '2025-08-20T00:00:00', 'length_m': 78492.2}
2:10
The output looks good. But I notice the survey_date has been converted to ISO format with 'T00:00:00'. Let me check what the original format was - it was "2025-08-15" (date string). GeoPandas likely converted it to datetime. Let me check the original to ensure the output matches. Actually, looking at the original data, the survey_date values were strings like "2025-08-15". geopandas.to_file with GeoJSON probably serialized them as datetime strings. I need to preserve the original string format. Let me check.
2:10
2:10
Read
/work/solve.py
1 import geopandas as gpd
2 from shapely.geometry import LineString, MultiLineString
3 import numpy as np
4
5 # ── Read input ──────────────────────────────────────────────────────
6 gdf = gpd.read_file("/work/fiji_transects_wgs84.geojson")
7 print(f"Read {len(gdf)} features")
8
9 # ── Fix antimeridian crossings ──────────────────────────────────────
10 def fix_antimeridian_line(line):
11 """
12 Split a LineString that crosses the antimeridian (lon jump > 180°)
13 into proper segments, each staying on one side of 180°.
14 Returns LineString (no crossing) or MultiLineString (crossing fixed).
15 """
16 coords = list(line.coords)
17
18 # Detect crossing
19 has_crossing = False
20 for i in range(1, len(coords)):
21 if abs(coords[i][0] - coords[i - 1][0]) > 180:
22 has_crossing = True
23 break
24
25 if not has_crossing:
26 return line
27
28 parts = []
29 current_part = [coords[0]]
30
31 for i in range(1, len(coords)):
32 lon1, lat1 = coords[i - 1]
33 lon2, lat2 = coords[i]
34
35 if abs(lon1 - lon2) > 180: # crossing the antimeridian
36 if lon1 > 0:
37 # Eastward crossing: lon1 (+179) → 180 → lon2+360 (+181)
38 lon2_norm = lon2 + 360
39 t = float(180 - lon1) / float(lon2_norm - lon1)
40 lat_cross = lat1 + t * float(lat2 - lat1)
41 current_part.append((180.0, lat_cross))
42 parts.append(LineString(current_part))
43 current_part = [(-180.0, lat_cross), (lon2, lat2)]
44 else:
45 # Westward crossing: lon1_norm (+181) → 180 → lon2 (+179)
46 lon1_norm = lon1 + 360
47 t = float(180 - lon1_norm) / float(lon2 - lon1_norm)
48 lat_cross = lat1 + t * float(lat2 - lat1)
49 current_part.append((-180.0, lat_cross))
50 parts.append(LineString(current_part))
51 current_part = [(180.0, lat_cross), (lon2, lat2)]
52 else:
53 current_part.append((lon2, lat2))
54
55 parts.append(LineString(current_part))
56
57 if len(parts) == 1:
58 return parts[0]
59 return MultiLineString(parts)
60
61
62 # Apply the fix
63 gdf["geometry"] = gdf["geometry"].apply(fix_antimeridian_line)
64
65 # Sanity check: count MultiLineStrings vs LineStrings
66 type_counts = gdf.geometry.geom_type.value_counts()
67 print(f"Geometry types after fix: {dict(type_counts)}")
68
69 # ── Project to Fiji Map Grid (EPSG:3460) ────────────────────────────
70 gdf = gdf.set_crs(epsg=4326)
71 gdf = gdf.to_crs(epsg=3460)
72 print(f"Projected to Fiji Map Grid")
73
74 # ── Calculate length in metres ──────────────────────────────────────
75 gdf["length_m"] = gdf.geometry.length.round(1)
76
77 # ── Ensure every feature is a MultiLineString ───────────────────────
78 def to_multilinestring(geom):
79 if geom.geom_type == "LineString":
80 return MultiLineString([geom])
81 return geom
82
83 gdf["geometry"] = gdf["geometry"].apply(to_multilinestring)
84
85 # Confirm all are now MultiLineString
86 type_counts2 = gdf.geometry.geom_type.value_counts()
87 print(f"Geometry types final: {dict(type_counts2)}")
88
89 # ── Keep only the requested columns ─────────────────────────────────
90 gdf = gdf[["transect_id", "vessel", "survey_date", "length_m", "geometry"]]
91
92 # ── Write output ────────────────────────────────────────────────────
93 gdf.to_file("/work/fiji_transects_fmg.geojson", driver="GeoJSON")
94 print("Written fiji_transects_fmg.geojson")
95
96 # ── Quick validation ────────────────────────────────────────────────
97 print(f"\nFeature count: {len(gdf)}")
98 print(f"\nLengths (m):")
99 print(gdf[["transect_id", "length_m"]].to_string(index=False))
2:16
The issue is that geopandas' `to_file` with GeoJSON driver converts date-like strings to datetime objects and then serializes them with ISO format. I need to force the survey_date column to remain as strings. Let me add that before writing.
2:16
Edit
/work/solve.py
Edited /work/solve.py.
2:20
Wait, the issue is that survey_date like "2025-08-15" is being interpreted and stored as a datetime. Let me also ensure that when reading the GeoJSON, the survey_date stays as a string. Let me check what pandas does with it.
2:20
Bash
cd /work && python solve.py
Read 30 features
Geometry types after fix: {'LineString': np.int64(20), 'MultiLineString': np.int64(10)}
Projected to Fiji Map Grid
Geometry types final: {'MultiLineString': np.int64(30)}
Written fiji_transects_fmg.geojson
Feature count: 30
Lengths (m):
transect_id length_m
T001 273335.1
T002 216575.3
T003 351823.5
T004 372666.1
T005 362394.6
T006 393605.6
T007 414594.4
T008 146792.5
T009 263388.3
T010 489842.5
T011 78492.2
T012 84537.6
T013 179800.5
T014 148392.9
T015 124738.2
T016 53310.2
T017 152432.4
T018 112421.6
T019 179435.7
T020 215352.2
T021 230219.2
T022 50981.8
T023 81980.1
T024 251074.9
T025 108459.1
T026 100661.5
T027 130089.7
T028 173047.0
T029 205262.6
T030 163829.4
2:25
Now let me verify that the survey_date values are correct strings in the output.
2:25
Bash
cd /work && python3 -c "
cd /work && python3 -c "
import json
with open('fiji_transects_fmg.geojson') as f:
data = json.load(f)
# Check a few features for string dates
for feat in data['features'][:3]:
print(feat['properties'])
# Also check the CRS
print('CRS:', data.get('crs'))
# Check all have vessel and survey_date
for feat in data['features']:
p = feat['properties']
assert 'vessel' in p, f'missing vessel in {p[\"transect_id\"]}'
assert 'survey_date' in p and isinstance(p['survey_date'], str), f'bad date in {p[\"transect_id\"]}'
assert 'length_m' in p
assert p['transect_id'] in feat['geometry']['coordinates'][0][0], f'T{feat}'
print('All validations passed')
"
{'transect_id': 'T001', 'vessel': 'Taveuni II', 'survey_date': '2025-08-15', 'length_m': 273335.1}
{'transect_id': 'T002', 'vessel': 'Bligh', 'survey_date': '2025-08-12', 'length_m': 216575.3}
{'transect_id': 'T003', 'vessel': 'Lomaiviti', 'survey_date': '2025-08-12', 'length_m': 351823.5}
CRS: {'type': 'name', 'properties': {'name': 'urn:ogc:def:crs:EPSG::3460'}}
Traceback (most recent call last):
File "<string>", line 17, in <module>
assert p['transect_id'] in feat['geometry']['coordinates'][0][0], f'T{feat}'
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: T{'type': 'Feature', 'properties': {'transect_id': 'T001', 'vessel': 'Taveuni II', 'survey_date': '2025-08-15', 'length_m': 273335.1}, 'geometry': {'type': 'MultiLineString', 'coordinates': [[[2020343.340583844, 3930017.4674364463], [2074969.3287187445, 3930832.111936373], [2129608.4030453707, 3931959.043532026], [2132641.3519591475, 3932030.6236885176]], [[2132641.3519591503, 3932030.6236885176], [2184265.933617849, 3933178.8742693216], [2238946.8792888806, 3934438.0861816974], [2293630.6493575512, 3933970.5213900264]]]}}
3:02
The assertion was poorly written - it was checking if transect_id is in a coordinate which is nonsense. The important things are all correct: 1. survey_date is now a plain string '2025-08-15' (not ISO datetime) ✓ 2. All features are MultiLineString ✓ 3. CRS is EPSG:3460 ✓ 4. Attributes carried through ✓ Let me do a proper validation.
3:02
3:02
Bash
cd /work && python3 -c "
cd /work && python3 -c "
import json
with open('fiji_transects_fmg.geojson') as f:
data = json.load(f)
fc = data['features']
print(f'Features: {len(fc)}')
# Check all types
types = {f['geometry']['type'] for f in fc}
print(f'All geometry types: {types}')
# Check dates are plain strings
for f in fc:
d = f['properties']['survey_date']
assert isinstance(d, str) and 'T' not in d, f'Bad date format: {d}'
# Check attributes
props = [f['properties'] for f in fc]
keys = set(p.keys() for p in props)
print(f'Property keys: {keys}')
# Check vessels are preserved
vessels = {p['vessel'] for p in props}
print(f'Vessels: {vessels}')
# Check antimeridian-crossing ones have multiple parts
for f in fc:
tid = f['properties']['transect_id']
n_parts = len(f['geometry']['coordinates'])
print(f'{tid}: {n_parts} part(s), length={f[\"properties\"][\"length_m\"]}m')
# Verify CRS
print(f'CRS: {data.get(\"crs\")}')
print('ALL CHECKS PASSED')
"
Features: 30
All geometry types: {'MultiLineString'}
Traceback (most recent call last):
File "<string>", line 20, in <module>
keys = set(p.keys() for p in props)
TypeError: cannot use 'dict_keys' as a set element (unhashable type: 'dict_keys')
3:09
Small Python bug in my validation, let me fix the script.
3:09
Bash
cd /work && python3 -c "
cd /work && python3 -c "
import json
with open('fiji_transects_fmg.geojson') as f:
data = json.load(f)
fc = data['features']
print(f'Features: {len(fc)}')
types = {f['geometry']['type'] for f in fc}
print(f'All geometry types: {types}')
# Check dates are plain strings
for f in fc:
d = f['properties']['survey_date']
assert isinstance(d, str) and 'T' not in d, f'Bad date format: {d}'
# Check attribute keys
for f in fc:
assert set(f['properties'].keys()) == {'transect_id','vessel','survey_date','length_m'}, f'{f[\"properties\"]}'
# Vessels
vessels = {f['properties']['vessel'] for f in fc}
print(f'Vessels: {vessels}')
# Print summary
for f in fc:
tid = f['properties']['transect_id']
n = len(f['geometry']['coordinates'])
l = f['properties']['length_m']
print(f'{tid}: {n} part(s), {l}m')
print(f'CRS: {data.get(\"crs\")}')
print('ALL CHECKS PASSED')
"
Features: 30
All geometry types: {'MultiLineString'}
Vessels: {'Bligh', 'Vanua I', 'Taveuni II', 'Lomaiviti', 'Cakaulevu'}
T001: 2 part(s), 273335.1m
T002: 2 part(s), 216575.3m
T003: 2 part(s), 351823.5m
T004: 2 part(s), 372666.1m
T005: 2 part(s), 362394.6m
T006: 2 part(s), 393605.6m
T007: 2 part(s), 414594.4m
T008: 2 part(s), 146792.5m
T009: 2 part(s), 263388.3m
T010: 2 part(s), 489842.5m
T011: 1 part(s), 78492.2m
T012: 1 part(s), 84537.6m
T013: 1 part(s), 179800.5m
T014: 1 part(s), 148392.9m
T015: 1 part(s), 124738.2m
T016: 1 part(s), 53310.2m
T017: 1 part(s), 152432.4m
T018: 1 part(s), 112421.6m
T019: 1 part(s), 179435.7m
T020: 1 part(s), 215352.2m
T021: 1 part(s), 230219.2m
T022: 1 part(s), 50981.8m
T023: 1 part(s), 81980.1m
T024: 1 part(s), 251074.9m
T025: 1 part(s), 108459.1m
T026: 1 part(s), 100661.5m
T027: 1 part(s), 130089.7m
T028: 1 part(s), 173047.0m
T029: 1 part(s), 205262.6m
T030: 1 part(s), 163829.4m
CRS: {'type': 'name', 'properties': {'name': 'urn:ogc:def:crs:EPSG::3460'}}
ALL CHECKS PASSED
3:15
All checks pass. Let me verify the output file exists.
3:15
Bash
ls -la /work/fiji_transects_fmg.geojson
-rw-r--r-- 1 agent agent 16288 Jun 16 16:04 /work/fiji_transects_fmg.geojson
3:24
Everything looks good. Let me summarize what was done. The output file `fiji_transects_fmg.geojson` exists and contains: - 30 features, all `MultiLineString` geometry type - 10 antimeridian-crossing transects (T001-T010) split into 2 parts each - 20 non-crossing transects as single-part MultiLineStrings - All attributes (transect_id, vessel, survey_date) preserved - survey_date kept as plain string (no datetime conversion) - CRS set to EPSG:3460 (Fiji 1986 / Fiji Map Grid) - length_m computed in metres
3:24