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:03
Let me start by reading the input file to understand its structure and identify the problematic geometries.
0:03
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:49
Let me analyze the problem. Lines that cross the antimeridian (180° longitude) have coordinates like:
For T001:
```
[ [ 178.941..., -17.632... ], [ 179.456..., -17.623... ], [ 179.971..., -17.611... ], [ -179.513..., -17.596... ], ... ]
```
When rendered, these coordinates go from 179.97 to -179.51, which wraps around the entire globe instead of crossing the antimeridian locally. The fix is to shift the longitudes for points on the "wrong side" of the antimeridian by adding or subtracting 360.
For points crossing from east to west across the antimeridian (180°):
- Points with longitudes > 180 (or > 180 in the Eastern hemisphere) should stay as is
- Points that have been represented as negative (west of 180°, e.g., -179.5) should be converted to their equivalent positive value (e.g., 180.5) to show the actual continuous path.
Wait, let me think more carefully. The issue is that when you have coordinates like:
- 179.97 (east of 180°)
- -179.51 (west of 180°, i.e., 180.49°E)
If you just plot these, the line will go from 179.97 to -179.51, which wraps the long way around the globe. The fix is to represent points west of the antimeridian as > 180 (e.g., -179.51 → 180.49), which creates a continuous path across the antimeridian.
Actually, let me think about this differently. The geojson uses WGS84 with longitudes in range [-180, 180]. When a line crosses the antimeridian, we need to "unwrap" the longitudes so they form a continuous path.
For T001:
- 178.94, 179.46, 179.97 (east side)
- -179.51, -179.00, -178.48 (west side, but wrapped to negative)
To make it continuous, we need to add 360 to the west-side coordinates:
- 178.94, 179.46, 179.97 (east side)
- 180.49 (= -179.51+360), 181.00 (= -179.00+360), 181.52 (= -178.48+360)
But wait - these are still in WGS84. The problem is that GeoJSON normally uses -180 to 180 for longitudes. But we're going to project to Fiji's national metric grid anyway, so we just need to make the geometry correct in WGS84 first, then project.
Actually, the standard approach is:
1. For each transect that crosses the antimeridian, detect which coordinates need to be unwrapped
2. Split into segments at the antimeridian
3. Each segment should be a continuous line
4. The result should be a MultiLineString
Let me figure out Fiji's national metric grid. Fiji uses the following projected CRS:
- EPSG:3460 - Fiji 1956 / Fiji Central Meridian 178°E
- Actually, more commonly used is EPSG:3460 or EPSG:3139? Let me check.
Actually, for Fiji, the common national grid is "Fiji 1956 / Fiji Map Grid" (EPSG:3139) or similar. But I think the modern one is EPSG:3460 (Fiji 1956 / Fiji Central Meridian 178°E).
Wait, let me think about what "Fiji's national metric grid" refers to. Fiji has used several:
- Fiji 1956 / Fiji Central (EPSG:3139) - uses central meridian of 178°E
- Also there's EPSG:3460 (Fiji 1956 / Fiji Central Meridian 178°E)
Actually EPSG:3139 (Fiji 1956 / Fiji Central Meridian 178°E) and EPSG:3460 are the same thing? Let me check... no, EPSG:3139 is "Fiji 1956 / Fiji Central Meridian 178°E" and EPSG:3460 is "Fiji 1956 / Fiji Central Meridian 180°E".
Wait, looking at EPSG definitions:
- EPSG:3139: Fiji 1956 / Fiji Central Meridian 178°E
- EPSG:3460: Fiji 1956 / Fiji Central Meridian 180°E? No...
Actually, let me just look it up. The common Fiji metric grid projection parameters:
- Projection: Transverse Mercator
- Central meridian: 178°E (for the main Fiji group)
- Scale factor: 0.9999 (or 1.0?)
- False easting: 0
- False northing: 0
- Datum: Fiji 1956
EPSG:3139 is "Fiji 1956 / Fiji Central Meridian 178°E" with these parameters.
Let me also look at EPSG:3460 - this might be "Fiji 1956 / Fiji Central Meridian 180°E" for the eastern islands.
Actually, I think the simplest approach is to use pyproj.CRS to find the right one. Let me write a script.
Let me plan the Python script:
1. Read the GeoJSON
2. For each feature:
a. Check if it crosses the antimeridian (the `crosses_antimeridian_flag` property tells us)
b. If it does, fix the geometry by unwrapping longitudes and splitting into segments
c. Convert to MultiLineString if it's a LineString
3. Project to Fiji's national metric grid (EPSG:3139 or similar)
4. Calculate length_m for each transect
5. Write as GeoJSON
For the antimeridian crossing fix:
- Find where the longitude jumps across the antimeridian (from near +180 to near -180 or vice versa)
- Split into line segments on each side
- Each segment gets its longitudes adjusted (add 360 to the negative side) so they're continuous
- Return as MultiLineString
Let me write a comprehensive script.
Actually wait, let me reconsider. The points that cross the antimeridian need to be split into multiple line segments. For example, T001 has:
- [ 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 segment crossing the antimeridian is between [179.97, -17.61] and [-179.51, -17.60]. On the east side, the longitude goes up to 179.97, then on the west side it continues at -179.51 (= 180.49).
To fix this, I need to:
1. Find where consecutive coordinates cross the 180° meridian
2. At each crossing, split the line and create separate segments
3. For the western segments, add 360° to longitudes to make them continuous
4. Project to Fiji grid
5. Combine into MultiLineString
Actually, a simpler approach: instead of splitting, I can just "unwrap" the longitudes by adding 360 to the negative values when the line crosses from east to west. But the result would have longitudes > 180 which isn't standard WGS84. However, since we're going to project to a metric grid anyway, it should work fine for the projection step.
Let me think again... Actually, if I just add 360 to the negative values for transects that cross the antimeridian, then project to Fiji grid, that should work. The projection function should handle longitudes > 180 correctly.
Wait, but the instruction says "the problematic ones must end up as multi-part geometries that faithfully represent the actual survey path." So they need to be MultiLineString with separate segments on each side of the antimeridian.
Let me think about this more carefully. A transect that crosses the antimeridian - the actual survey path goes continuously across. So maybe the approach is:
1. For transects crossing the antimeridian, "unwrap" longitudes by adding 360 to negative values
2. Project to Fiji grid
3. But... the resulting geometry would still be a LineString, just with unwrapped coordinates
Hmm, but the user says "the problematic ones must end up as multi-part geometries that faithfully represent the actual survey path." So maybe they want each side of the antimeridian as a separate part of a MultiLineString?
Actually, I think the real problem is rendering. When you have a LineString that goes from +179.97 to -179.51, most GIS software will draw the line across the entire globe (passing through longitude 0). The fix is to split into two segments:
1. Segment on the east side: from 178.94 to 179.97 (normal longitudes)
2. Segment on the west side: from -179.51 to -178.48 (or equivalently, from 180.49 to 181.52)
But these two segments, when rendered in WGS84, would still have the gap issue if we keep them as separate features. However, if we project them first to Fiji grid, they'll connect properly.
Actually, rethinking: Let me unwrap all the coordinates first, then project, then check if the projected geometry is a single continuous line or if it needs to be split.
Let me take the approach:
1. For transects that cross the antimeridian, unwrap longitude by adding 360 to negative values so the path is continuous
2. Project to Fiji grid
3. Calculate length
4. Store as MultiLineString (wrapping single LineStrings in MultiLineString if needed)
For transects that don't cross the antimeridian, just project directly.
Actually, I need to be more careful. Let me look at the data again.
For T001 (crosses antimeridian):
- East side: 178.94, 179.46, 179.97
- West side: -179.51, -179.00, -178.48
If I add 360 to west side: 180.49, 181.00, 181.52
So the unwrapped path is: 178.94, 179.46, 179.97, 180.49, 181.00, 181.52
This will project correctly to Fiji grid and form a continuous line.
But wait, the user says "the problematic ones must end up as multi-part geometries that faithfully represent the actual survey path." So maybe they DO want separate parts?
Let me re-read: "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."
So:
- All features should be MultiLineString (even if they have just one part)
- Problematic ones (crossing antimeridian) should have multiple parts
For the problematic ones, I think the expectation is:
- Part 1: The segment(s) on the east side of 180°
- Part 2: The segment(s) on the west side of 180°
This way, when rendered, each part stays on its own side of the antimeridian and doesn't wrap around the globe.
But how to split? Let me find where the actual crossing occurs and split the line there.
For T001:
Points: [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 crossing occurs between point 3 (179.97) and point 4 (-179.51). So:
- Part 1 (east side): [178.94, ...], [179.46, ...], [179.97, ...]
- Part 2 (west side): [-179.51, ...], [-179.00, ...], [-178.48, ...]
For the projection, each part is projected separately from WGS84 to Fiji grid.
Let me now write the script.
Actually, I realize I need to figure out the Fiji grid CRS. Let me look up what's available.
Common Fiji projected CRS options:
- EPSG:3139 - Fiji 1956 / Fiji Central Meridian 178°E (Transverse Mercator, central meridian 178°E)
- EPSG:3460 - This might be something else
Let me just use pyproj to query CRS info.
Actually, I think "Fiji's national metric grid" is likely EPSG:3139 (Fiji 1956 / Fiji Map Grid / Central Meridian 178°E). But let me also check EPSG:3460.
Let me also look at what packages are available and just write the script.
Let me start by exploring what CRS options are available.
For the approach:
1. Read the GeoJSON
2. For each feature:
- If it's a LineString, wrap in MultiLineString
- If it crosses the antimeridian, split into segments
3. For each segment of each feature:
a. If the segment contains negative longitudes and the feature crosses the antimeridian, add 360 to those longitudes
b. Actually no, let me think about this differently.
OK let me think about this more carefully. The issue is reproduction in a projected coordinate system.
When we have coordinates like:
- East of 180: 179.97°E
- West of 180: 179.51°W = -179.51° (in lon/lat), which is 180.49°E
If I just project these as-is to a Fiji grid (which has central meridian at 178°E), both coordinates will project correctly:
- 179.97°E → some positive easting
- -179.51° (which is 180.49°E) → some positive easting
But the issue is the line between them. In WGS84, the software might interpret this as going from 179.97 to -179.51 through 0° longitude, which is the long way around.
Actually, I think the simplest fix that addresses the user's concern is:
For antimeridian-crossing transects, split into two LineStrings:
1. Points with longitude < 0 (west of antimeridian): longitudes are in [-180, 0), but they're actually just west of 180°E
2. Points with longitude > 0 (east of antimeridian): longitudes in (0, 180]
But wait, some points might be on both sides. Let me look at T001:
- East side (> 0): 178.94, 179.46, 179.97
- West side (< 0): -179.51, -179.00, -178.48
So I split at the antimeridian crossing point (between 179.97 and -179.51).
For rendering, each part is within a normal longitude range and won't wrap.
But for projection, the west side points (-179.51, -179.00, -178.48) need to be interpreted correctly. Since we're in a Fiji projection centered near 178°E, these should project to the far east of the grid. Actually, in a Transverse Mercator projection with central meridian at 178°E, longitude -179.51° (= 180.49°E) is only 2.49° from the central meridian, so it should project fine.
But wait, if I pass -179.51 to the projection, will it know that it should be 180.49°E? In pyproj, if I project (-179.51, -17.60) from EPSG:4326 to EPSG:3139, it should handle it correctly because the projection covers the full globe.
Let me just try it.
Let me write the script now.
```python
import geopandas as gpd
import json
import numpy as np
from shapely.geometry import LineString, MultiLineString
from pyproj import CRS, Transformer
from shapely.ops import transform
# Read the GeoJSON
gdf = gpd.read_file('/work/fiji_transects_wgs84.geojson')
# Define the CRS
# Fiji 1956 / Fiji Central Meridian 178°E
fiji_crs = CRS.from_epsg(3139)
# Or check what's available
print(CRS.from_epsg(3139))
```
Actually, let me just write a full script and run it.
For fixing antimeridian crossing:
1. For each feature with crosses_antimeridian_flag = True
2. Get the coordinates
3. Find where the longitude jumps from positive to negative (or vice versa) with abs(delta_lon) > 180
4. Split the coordinates into segments at each crossing
5. For the western segments (negative longitudes), add 360 to make them continuous for projection
6. Project each segment to Fiji grid
7. Calculate lengths
8. Combine segments into MultiLineString
For features without antimeridian crossing:
1. Just wrap LineString in MultiLineString
2. Project to Fiji grid
3. Calculate length
Let me write this out.
Actually, let me first check what EPSG codes are available for Fiji.
Let me write a quick exploration script.
0:49
Bash
cd /work && python3 -c "
cd /work && python3 -c "
from pyproj import CRS
# Check Fiji-related CRS
for code in [3139, 3460, 3159, 3140, 3141, 3142]:
try:
crs = CRS.from_epsg(code)
print(f'EPSG:{code} -> {crs.name}')
except:
print(f'EPSG:{code} -> not available')
"
EPSG:3139 -> Vanua Levu 1915 / Vanua Levu Grid EPSG:3460 -> Fiji 1986 / Fiji Map Grid EPSG:3159 -> NAD83(CSRS) / UTM zone 15N EPSG:3140 -> Viti Levu 1912 / Viti Levu Grid EPSG:3141 -> Fiji 1956 / UTM zone 60S EPSG:3142 -> Fiji 1956 / UTM zone 1S
33:51
Let me check the Fiji Map Grid (EPSG:3460) more carefully. This seems like the national metric grid for Fiji.
Let me also understand the problem: the features that "cross the antimeridian" have coordinates that span from positive longitudes (e.g., 179.xx) to negative longitudes (e.g., -179.xx). When rendered naively, a line from 179.9 to -179.9 will go all the way around the globe instead of crossing the 180° meridian locally.
The fix is to adjust the longitudes of points on one side so that they are continuous. For points crossing the antimeridian, we can convert negative longitudes (west of anti-meridian) to their equivalent positive values by adding 360 (so -179.5 becomes 180.5), then the line will be continuous. After projecting, we can optionally break it into segments that properly represent the actual path.
Wait, but the instruction says "the problematic ones must end up as multi-part geometries that faithfully represent the actual survey path." So the idea is:
1. For antimeridian-crossing lines, adjust coordinates so the line correctly crosses the 180° meridian
2. Then project to Fiji Map Grid
3. Split the projected geometry at the antimeridian (or wherever needed) to create a MultiLineString with parts on each side
Actually, let me think again. The issue is that GeoJSON uses longitude -180 to 180. When a line goes from 179 to -179, the renderer draws a line across the entire globe instead of the short segment that crosses the date line.
The approach:
1. For antimeridian-crossing lines: convert negative longitudes (west of 180) to positive values by adding 360, making coordinates continuous
2. Then intersect/split the line at the antimeridian (x=180°) to create a multipart geometry
3. Then shift the eastern part back to negative longitudes for proper GeoJSON representation
4. Project each part to the Fiji Map Grid
5. Calculate lengths
Actually, a simpler approach:
1. For each feature, determine if it crosses the antimeridian
2. For those that do, adjust longitudes to make them continuous (add 360 to negative longitudes)
3. Now the line goes smoothly across
4. Project to Fiji Map Grid
5. The projected line might span a large x-range, but that's actually correct in the projected CRS
6. Then... we need to make it a MultiLineString
Wait, the problem is about rendering arcs spanning the globe. In WGS84 longitude/latitude, a line from 179 to -179 goes the "long way" around (358 degrees) instead of the short way (2 degrees).
To fix this, we need to adjust coordinates so they "unwrap" the antimeridian. For a line crossing the dateline going westwards (longitudes going from positive to negative), we add 360 to the negative longitudes. For going eastwards (negative to positive), we subtract 360 from positive ones.
Let me look at the coordinates:
T001: 178.94, 179.46, 179.97, -179.51, -178.99, -178.48
This goes eastwards crossing the dateline. So we need to add 360 to the negative values to make them continuous:
178.94, 179.46, 179.97, 180.49, 181.01, 181.52
Actually wait -179.51 + 360 = 180.49, -178.99 + 360 = 181.01, -178.48 + 360 = 181.52. That makes sense, the line goes from 178.94 to 181.52, crossing the 180° line.
Then after projecting, the line will be in Fiji Map Grid coordinates. The issue is that the projected coordinates might wrap around or have a gap.
Actually, let me think about this differently. The approach in many GIS workflows:
1. "Unwrap" the longitudes across the antimeridian by adding/subtracting 360 where needed
2. Project the unwrapped line to the target CRS
3. If needed, split the projected line where it crossed the antimeridian (the 180° meridian) to create a MultiLineString
But for the Fiji Map Grid (EPSG:3460), which is a projected CRS for Fiji, the antimeridian issue might not be relevant since the projection will handle it naturally once we unwrap the coordinates.
Let me check what EPSG:3460 actually is.
Actually, let me just use a practical approach:
For each feature:
1. If it crosses the antimeridian, shift coordinates so they're continuous (add 360 to negative longitudes)
2. Create a Shapely LineString with the adjusted coordinates
3. Project to EPSG:3460 (Fiji Map Grid)
4. For antimeridian-crossing ones, split the line at the 180° meridian (which after unwrapping is at x=180 in the original CRS, but in the projected CRS it'll be some value). Actually, the split should happen in the original CRS before projection.
Wait, I think the simplest approach is:
1. For lines crossing the antimeridian, "unwrap" them by converting to a continuous coordinate system (add 360 to negative longitudes)
2. In this unwrapped space, split the line at the antimeridian (longitude = 180°)
3. This gives us two LineStrings, one on each side
4. Convert the eastern part back to normal longitude (subtract 360 if > 180)
5. Now we have a proper multi-part geometry in WGS84
6. Project each part to EPSG:3460
7. Compute length
Actually, let me think about this more carefully. The goal is:
- Fix geometries so they render correctly (no global-spanning arcs)
- Convert to Fiji's national metric grid (EPSG:3460 - Fiji 1986 / Fiji Map Grid)
- Each transect as one MultiLineString feature
- Add length_m attribute
Let me write a Python script:
```python
import geopandas as gpd
import shapely
import shapely.ops
from shapely.geometry import LineString, MultiLineString, Point
import json
# Read the GeoJSON
gdf = gpd.read_file('/work/fiji_transects_wgs84.geojson')
# Target CRS
target_crs = 'EPSG:3460' # Fiji 1986 / Fiji Map Grid
def fix_antimeridian_line(coords):
"""
Unwrap a line that crosses the antimeridian.
Returns a list of (wrapped) linestrings that, when combined, form a MultiLineString.
"""
# First, adjust coordinates to be continuous across the antimeridian
# by adding 360 to negative longitudes
adjusted_coords = []
for lon, lat in coords:
adjusted_coords.append((lon + 360 if lon < 0 else lon, lat))
# Now find where the line crosses the 180° meridian
# We'll split at x=180
line = LineString(adjusted_coords)
# Split at the meridian 180
meridian = LineString([(180, -90), (180, 90)])
# Use shapely's split
from shapely.ops import split
parts = split(line, meridian)
# Now convert back: any x > 180 should have 360 subtracted
fixed_parts = []
for part in parts.geoms if hasattr(parts, 'geoms') else [parts]:
new_coords = []
for x, y in part.coords:
if x > 180:
new_coords.append((x - 360, y))
else:
new_coords.append((x, y))
fixed_parts.append(LineString(new_coords))
return fixed_parts
# Process each feature
results = []
for idx, row in gdf.iterrows():
geom = row.geometry
props = {k: v for k, v in row.items() if k != 'geometry'}
coords = list(geom.coords)
# Check if this line crosses the antimeridian
# A line crosses if adjacent coordinates have opposite signs near 180
crosses = False
for i in range(len(coords) - 1):
lon1, lat1 = coords[i]
lon2, lat2 = coords[i+1]
if (lon1 > 170 and lon2 < -170) or (lon1 < -170 and lon2 > 170):
crosses = True
break
if crosses:
parts = fix_antimeridian_line(coords)
# Create MultiLineString
multi_line = MultiLineString(parts)
else:
# Still make it a MultiLineString but with one part
multi_line = MultiLineString([geom])
# Now project to target CRS
from shapely.ops import transform
import pyproj
wgs84 = pyproj.CRS('EPSG:4326')
fmg = pyproj.CRS(target_crs)
project = pyproj.Transformer.from_crs(wgs84, fmg, always_xy=True).transform
projected_geom = transform(project, multi_line)
# Calculate length in metres
length_m = projected_geom.length
results.append({
'geometry': projected_geom,
'properties': {**props, 'length_m': round(length_m, 2)}
})
# ... build output GeoJSON
```
Wait, but `shapely.ops.split` might not work perfectly. Let me think about this differently.
Actually, a simpler approach:
For antimeridian-crossing lines:
1. Convert all coordinates to continuous longitudes (add 360 to negative ones)
2. Build the full line
3. Project the full line to Fiji Map Grid
4. The projected line should be continuous and correct
5. Then we don't need to split at all - just use it as a MultiLineString
But the problem says "the problematic ones must end up as multi-part geometries that faithfully represent the actual survey path." So maybe they want explicit multi-part geometries where each part is on one side of the dateline?
Let me re-read: "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."
So non-problematic ones should be MultiLineStrings with a single part. Problematic ones should have multiple parts.
I think the approach is:
1. For antimeridian-crossing lines: split them at the 180° meridian into parts on each side
2. Project each part to Fiji Map Grid
3. Combine into a MultiLineString
Let me write this more carefully.
Actually, I think the simplest way to handle this in Python with Shapely:
```python
def split_line_at_antimeridian(line):
"""
Split a LineString at the antimeridian (180° longitude).
Returns a list of LineStrings.
"""
# Shift negative longitudes to positive (add 360)
# to make the line continuous
coords = list(line.coords)
adjusted = []
for lon, lat in coords:
if lon < 0:
adjusted.append((lon + 360, lat))
else:
adjusted.append((lon, lat))
adjusted_line = LineString(adjusted)
# Create a vertical line at the antimeridian (180°)
meridian = LineString([(180, -90), (180, 90)])
# Split
from shapely.ops import split
parts = split(adjusted_line, meridian)
# Convert back to normal coordinates
result_parts = []
for part in parts.geoms:
new_coords = []
for x, y in part.coords:
if x > 180:
new_coords.append((x - 360, y))
else:
new_coords.append((x, y))
result_parts.append(LineString(new_coords))
return result_parts
```
But `shapely.ops.split` might not always work well for this case. Let me try a different approach: manually find the intersection point and split.
Actually, let me just write the script and test it.
33:51
Bash
cd /work && python3 -c "
cd /work && python3 -c " import pyproj crs = pyproj.CRS.from_epsg(3460) print(crs.name) print(crs.area_of_use) "
Fiji 1986 / Fiji Map Grid - name: Fiji - onshore. Includes Viti Levu, Vanua Levu, Taveuni, the Yasawa Group, the Kadavu Group, the Lau Islands and Rotuma Islands. - bounds: (176.81, -20.81, -178.15, -12.42)
34:16
EPSG:3460 (Fiji 1986 / Fiji Map Grid) is the national metric grid for Fiji. Let's use that. Now, the key problem is that some transects cross the antimeridian (longitude jumps from ~179.9 to -179.9 or similar). In WGS84, coordinates near 180°E/-180°W that cross the antimeridian will render as arcs across the globe on a map. The fix is: 1. For features that cross the antimeridian, split the LineString at the antimeridian into multiple segments, each staying on the same "side" (east or west of 180°). 2. Convert to Fiji Map Grid (EPSG:3460) 3. Calculate length in metres 4. Output as MultiLineString always Let me think about the antimeridian crossing issue more carefully. The coordinates are in WGS84 (EPSG:4326). The antimeridian is at ±180° longitude. For the crossing transects, the coordinates go from say ~179.9 to -179.9. This means the line literally goes from just east of the dateline to just west of it. But a naive plotting library would draw this as a line going across the entire globe (longitude -179.9 to 179.9 going the "long way" around). The solution: For each segment that crosses the antimeridian (i.e., consecutive points where longitude jumps from > 179 to < -179 or vice versa), we need to insert a point at the antimeridian (±180°) and split into two parts. Actually, for proper rendering, we need a different approach. Let me think... The proper way to handle antimeridian crossing in a projected coordinate system like EPSG:3460 (Fiji Map Grid) is: 1. Shift coordinates that are negative (west of the dateline) by adding 360° to make them continuous, OR 2. Split the LineString at the antimeridian into multiple parts, each in a consistent longitude range. Since EPSG:3460 has bounds (176.81, -20.81, -178.15, -12.42), it covers from 176.81°E to -178.15°W (i.e., 176.81°E to 181.85°E). So the grid covers longitudes from 176.81°E to about 181.85°E (which is -178.15°W). For Fiji, the dateline area near 180° is included. Let me think about this differently. When we have a transect crossing the antimeridian, e.g., T001: [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 coordinates go from 178.94E → 179.46E → 179.97E → -179.51W (which is 180.49E) → -179.00W (181.00E) → -178.48W (181.52E). In EPSG:3460, the longitude range is (176.81, -178.15) which is (176.81°E to 181.85°E). So negative longitudes in the range -178.15 to 0 are interpreted as 180 + that value (i.e., 180 + (-178.15) = 1.85°E... wait no. Actually, EPSG:3460 bounds: x=(176.81, -178.15), y=(-20.81, -12.42). The x range goes from 176.81 to -178.15. That's not right in western hemisphere terms. Let me reconsider. If we look at it, the Fiji Map Grid covers all of Fiji. The projection likely uses a central meridian that's different. Let me check: EPSG:3460 - Fiji 1986 / Fiji Map Grid - this is a Transverse Mercator projection with central meridian at 177°E (or maybe 178°E?). Let me look it up.
34:16
Bash
cd /work && python3 -c "
cd /work && python3 -c "
from pyproj import CRS
crs = CRS.from_epsg(3460)
print(crs.to_proj4())
print()
# Check the bounds in native CRS
print('Area of use:', crs.area_of_use)
"
+proj=tmerc +lat_0=-17 +lon_0=178.75 +k=0.99985 +x_0=2000000 +y_0=4000000 +ellps=WGS72 +units=m +no_defs +type=crs Area of use: - name: Fiji - onshore. Includes Viti Levu, Vanua Levu, Taveuni, the Yasawa Group, the Kadavu Group, the Lau Islands and Rotuma Islands. - bounds: (176.81, -20.81, -178.15, -12.42)
/usr/local/lib/python3.14/site-packages/pyproj/crs/crs.py:1295: UserWarning: You will likely lose important projection information when converting to a PROJ string from another format. See: https://proj.org/faq.html#what-is-the-best-format-for-describing-coordinate-reference-systems proj = self._crs.to_proj4(version=version)
35:02
Now I understand the situation. Let me analyze the data:
1. The features have coordinates in WGS84 (EPSG:4326), some crossing the antimeridian (180° longitude).
2. Fiji Map Grid (EPSG:3460) uses a central meridian of 178.75°E, so it handles the Fiji region well.
3. The "arcs spanning the globe" problem: when geometries cross the antimeridian (e.g., from +179.9° to -179.9°), a naive renderer draws a line going across the whole globe instead of the short local segment. The fix is to "un-cross" or split the geometry at the antimeridian.
Let me think about this more carefully:
Coordinates near Fiji range from about 176°E to 180° and then -180° to about -176° (which is equivalent to 180° to 184°E). When a line goes from, say, 179.97° to -179.96°, it actually crosses the 180° meridian but the naive interpolation goes the long way around.
The proper approach:
1. For antimeridian-crossing transects, I need to adjust the longitudes so they're continuous (e.g., add 360 to negative longitudes), then split at the antimeridian, then convert back if needed.
2. Then project to Fiji Map Grid (EPSG:3460).
3. Calculate length.
4. Output as MultiLineString.
Actually, let me think about the approach more carefully.
The Fiji Map Grid uses central meridian 178.75°E. It's a Transverse Mercator projection. The longitudes in the data range from about 176°E to -176°W (which is 184°E).
Given that the data spans ~176°E to ~-176°W (184°E), and the central meridian is 178.75°E, all points are within about 5° of the central meridian. So projecting should work fine IF we handle the antimeridian crossing properly.
For the features that cross the antimeridian, the coordinates in the GeoJSON go from positive longitudes (e.g., 179.97°) to negative longitudes (e.g., -179.96°). When projected, these two points should be close to each other, but the intermediate points interpolated in the projection would be wrong.
The solution:
1. For features flagged as `crosses_antimeridian_flag: true`, I need to:
- Shift the longitude of points with negative longitudes by adding 360 (making them continuous, e.g., -179.96° → 180.04°)
- This creates a continuous coordinate sequence
- Then split at the antimeridian (180° longitude) to create separate line segments
- Each segment's coordinates need to be normalized back to [-180, 180] range for WGS84 before projection
Wait, actually let me think about this differently. The projection system can handle coordinates beyond -180/180. So I could:
1. For all points in a feature, normalize longitudes to be continuous: if longitude < 0 and there are also longitudes > 0, add 360 to negative ones.
2. Then find where the line crosses 180°, and split it into segments.
3. Project each segment.
Actually, a simpler approach:
For each feature that crosses the antimeridian:
1. Convert all longitudes to a continuous range near the central meridian (178.75). So for longitudes < 0, add 360 to them.
2. The coordinates now range from about 176 to 184.
3. Find where the line crosses 180°, split into segments at those crossing points.
4. For each segment, its coordinates are all either < 180 or > 180. For those > 180, subtract 360 to get back to [-180, 180] range for WGS84.
5. Project each segment to EPSG:3460.
6. Combine into a MultiLineString.
Let me implement this.
Actually, I could also handle this more simply using shapely's `normalize` or `split` functionality. Let me think...
A cleaner approach using shapely and pyproj:
1. For each feature, if it crosses the antimeridian:
- Shift all coordinates to [0, 360] range by adding 360 to negative longitudes
- Use shapely to split the line at longitude=180
- For each resulting part, shift the longitudes > 180 back by subtracting 360 (back to [-180, 180])
- Now project each part and combine into a MultiLineString
Let me code this up.
Actually wait - let me also think about what "the problematic ones must end up as multi-part geometries that faithfully represent the actual survey path" means. For transects that DO cross the antimeridian, they should be split into multiple segments (a MultiLineString). For those that DON'T cross, they might still need to be MultiLineString if they have multiple parts, but the instruction says "Every feature must come out as a MultiLineString", so even simple ones should be MultiLineString with one part.
Let me write a Python script:
```python
import json
import geopandas as gpd
import shapely.geometry as geom
import shapely.ops as ops
from pyproj import Transformer
import numpy as np
# Read the GeoJSON
with open('/work/fiji_transects_wgs84.geojson', 'r') as f:
data = json.load(f)
# Define the transformer
transformer = Transformer.from_crs("EPSG:4326", "EPSG:3460", always_xy=True)
def antimeridian_split(linestring):
"""
Split a LineString that crosses the antimeridian into segments.
Returns a list of LineStrings in WGS84 coordinates (lon in [-180, 180]).
"""
coords = list(linestring.coords)
# First, make longitudes continuous by shifting negative ones
shifted = []
for lon, lat in coords:
if lon < 0:
shifted.append((lon + 360, lat))
else:
shifted.append((lon, lat))
# Now split at longitude=180
segments = []
current_seg = [shifted[0]]
for i in range(1, len(shifted)):
p1 = shifted[i-1]
p2 = shifted[i]
# Check if the segment crosses the 180° line
if (p1[0] <= 180 and p2[0] >= 180) or (p1[0] >= 180 and p2[0] <= 180):
# Find the intersection point at longitude=180
# Linear interpolation
lon1, lat1 = p1
lon2, lat2 = p2
if lon2 != lon1:
frac = (180 - lon1) / (lon2 - lon1)
lat_at_180 = lat1 + frac * (lat2 - lat1)
intersection = (180, lat_at_180)
# Finish current segment with the intersection point
current_seg.append(intersection)
segments.append(current_seg)
# Start new segment with the intersection point
current_seg = [intersection, p2]
else:
current_seg.append(p2)
else:
current_seg.append(p2)
if current_seg and len(current_seg) >= 2:
segments.append(current_seg)
# Now convert each segment back to [-180, 180] range
result_lines = []
for seg in segments:
wgs_coords = []
for lon, lat in seg:
if lon > 180:
wgs_coords.append((lon - 360, lat))
else:
wgs_coords.append((lon, lat))
result_lines.append(geom.LineString(wgs_coords))
return result_lines
# Process each feature
features_out = []
for feature in data['features']:
props = feature['properties']
transect_id = props['transect_id']
geometry = feature['geometry']
line = geom.shape(geometry)
if props.get('crosses_antimeridian_flag', False):
# Split at antimeridian
segments = antimeridian_split(line)
else:
segments = [line]
# Project each segment to EPSG:3460
projected_segments = []
for seg in segments:
# Project coordinates
proj_coords = [transformer.transform(lon, lat) for lon, lat in seg.coords]
projected_segments.append(geom.LineString(proj_coords))
# Create MultiLineString
multi_line = geom.MultiLineString(projected_segments)
# Calculate length in meters
length_m = multi_line.length
# Create output feature
out_feature = {
"type": "Feature",
"properties": {
"transect_id": props['transect_id'],
"vessel": props['vessel'],
"survey_date": props['survey_date'],
"length_m": round(length_m, 2)
},
"geometry": geom.mapping(multi_line)
}
features_out.append(out_feature)
# Write output
out_data = {
"type": "FeatureCollection",
"name": "fiji_transects_fmg",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG:3460" } },
"features": features_out
}
with open('/work/fiji_transects_fmg.geojson', 'w') as f:
json.dump(out_data, f, indent=2)
```
Wait, there's a subtlety. The antimeridian split function needs to handle the case where a point lands exactly at 180° (which is fine, it's the same in both systems). Let me also think about edge cases more carefully.
Also, for the split at antimeridian, I should be careful about the direction. When crossing from 179 to -179, in shifted coordinates it goes from 179 to 181, crossing 180. Good.
When crossing from -179 to 179, shifted goes from 181 to 179, also crossing 180.
Let me also think: after I split, do I need to handle the case where a segment has only 1 point? I check `len(current_seg) >= 2` so that should be fine. But actually the intersection point will be added, so there should be at least 2 points per segment.
Wait, there's another edge case: what if a coordinate is exactly at 180°? The GeoJSON shows things like -179.999... which is close but not exactly. Let me handle that robustly.
Actually, let me also be more careful. The antimeridian flag tells us which ones cross. Let me look at T001:
- Points: [178.94, -17.63], [179.46, -17.62], [179.97, -17.61], [-179.51, -17.60], [-178.99, -17.58], [-178.48, -17.58]
Shifted: [178.94], [179.46], [179.97], [180.49], [181.01], [181.52]
So the crossing happens between point 3 (179.97) and point 4 (180.49 = -179.51+360). The intersection at 180 would be between these two points.
Then I get segments:
1. From 178.94 through 179.46, 179.97 to 180 (intersection) → shifted back: 178.94 to 179.97, then 180 (which is -180)
2. From 180 (intersection) through 180.49, 181.01, 181.52 → shifted back: -180 through -179.51, -178.99, -178.48
This gives two valid LineStrings.
Let me refine my code a bit and run it.
Actually, I think I should handle the case where coordinates might be exactly at 180° more carefully. But let me just try running it first.
One more thing: for non-antimeridian-crossing features, I still need to output MultiLineString. So:
```python
if not isinstance(multi_line, geom.MultiLineString):
multi_line = geom.MultiLineString([multi_line])
```
Wait, `geom.MultiLineString(projected_segments)` already returns a MultiLineString. If there's only one segment, it still returns a MultiLineString.
Actually, let me check: `shapely.geometry.MultiLineString([line])` - yes, this creates a MultiLineString with one part.
Let me also check: for T011 and others that are entirely in negative longitudes (west of 180), they don't cross the antimeridian and should project fine.
OK let me write the script.
One more thing about the length calculation: I should use the projected length (which will be in meters since EPSG:3460 uses meters). The `.length` property of a projected geometry will give the length in meters.
Let me write the script now.
35:02
Bash
cd /work && python3 << 'PYEOF'
cd /work && python3 << 'PYEOF'
import json
import shapely.geometry as geom
import shapely.ops as ops
from pyproj import Transformer
# Read the GeoJSON
with open('/work/fiji_transects_wgs84.geojson', 'r') as f:
data = json.load(f)
transformer = Transformer.from_crs("EPSG:4326", "EPSG:3460", always_xy=True)
def split_at_antimeridian(linestring):
"""Split a LineString crossing the antimeridian into multiple WGS84 segments."""
coords = list(linestring.coords)
# Shift negative longitudes by +360 to make continuous near 180°
shifted = [(lon + 360 if lon < 0 else lon, lat) for lon, lat in coords]
segments = [] # list of lists of (lon, lat) pairs, in shifted coords
current = [shifted[0]]
for i in range(1, len(shifted)):
p1 = shifted[i-1]
p2 = shifted[i]
# Check if this edge crosses the 180° line
# (one side <= 180, the other >= 180, and not both on the same side)
lo, hi = sorted([p1[0], p2[0]])
if lo < 180 < hi:
# Linear interpolation to find the intersection at lon=180
lon1, lat1 = p1
lon2, lat2 = p2
frac = (180 - lon1) / (lon2 - lon1)
lat_at_180 = lat1 + frac * (lat2 - lat1)
inter = (180.0, lat_at_180)
current.append(inter)
segments.append(current)
current = [inter, p2]
else:
current.append(p2)
if current and len(current) >= 2:
segments.append(current)
# Convert segments back to [-180, 180] longitude range
result = []
for seg in segments:
wgs = [(lon - 360, lat) if lon > 180 else (lon, lat) for lon, lat in seg]
result.append(geom.LineString(wgs))
return result
features_out = []
for feature in data['features']:
props = feature['properties']
line = geom.shape(feature['geometry'])
if props.get('crosses_antimeridian_flag', False):
wgs_segments = split_at_antimeridian(line)
else:
wgs_segments = [line]
# Project each segment to Fiji Map Grid (EPSG:3460)
proj_segs = []
for seg in wgs_segments:
coords_proj = [transformer.transform(lon, lat) for lon, lat in seg.coords]
proj_segs.append(geom.LineString(coords_proj))
multi = geom.MultiLineString(proj_segs)
length_m = round(multi.length, 2)
out_feat = {
"type": "Feature",
"properties": {
"transect_id": props["transect_id"],
"vessel": props["vessel"],
"survey_date": props["survey_date"],
"length_m": length_m
},
"geometry": geom.mapping(multi)
}
features_out.append(out_feat)
out_collection = {
"type": "FeatureCollection",
"name": "fiji_transects_fmg",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG:3460" } },
"features": features_out
}
with open('/work/fiji_transects_fmg.geojson', 'w') as f:
json.dump(out_collection, f, indent=2)
print("Done. Wrote", len(features_out), "features.")
PYEOF
Done. Wrote 30 features.
35:08
Let me verify the output to make sure it's correct.
35:08
Read
/work/fiji_transects_fmg.geojson
1 {
2 "type": "FeatureCollection",
3 "name": "fiji_transects_fmg",
4 "crs": {
5 "type": "name",
6 "properties": {
7 "name": "urn:ogc:def:crs:EPSG:3460"
8 }
9 },
10 "features": [
11 {
12 "type": "Feature",
13 "properties": {
14 "transect_id": "T001",
15 "vessel": "Taveuni II",
16 "survey_date": "2025-08-15",
17 "length_m": 273335.11
18 },
19 "geometry": {
20 "type": "MultiLineString",
21 "coordinates": [
22 [
23 [
24 2020343.340583844,
25 3930017.4674364463
26 ],
27 [
28 2074969.3287187445,
29 3930832.111936373
30 ],
31 [
32 2129608.4030453707,
33 3931959.043532026
34 ],
35 [
36 2132641.3519591475,
37 3932030.6236885176
38 ]
39 ],
40 [
41 [
42 2132641.3519591475,
43 3932030.6236885176
44 ],
45 [
46 2184265.933617849,
47 3933178.8742693216
48 ],
49 [
50 2238946.8792888806,
51 3934438.0861816974
52 ],
53 [
54 2293630.6493575512,
55 3933970.5213900264
56 ]
57 ]
58 ]
59 }
60 },
61 {
62 "type": "Feature",
63 "properties": {
64 "transect_id": "T002",
65 "vessel": "Bligh",
66 "survey_date": "2025-08-12",
67 "length_m": 216575.29
68 },
69 "geometry": {
70 "type": "MultiLineString",
71 "coordinates": [
72 [
73 [
74 2034023.7576998495,
75 3962923.9288114174
76 ],
77 [
78 2103801.3645465984,
79 3980775.0173757505
80 ],
81 [
82 2133006.7821587846,
83 3988176.984284049
84 ]
85 ],
86 [
87 [
88 2133006.7821587846,
89 3988176.984284049
90 ],
91 [
92 2173711.8899896345,
93 3998417.276510603
94 ],
95 [
96 2243773.2911946434,
97 4016860.265945784
98 ]
99 ]
100 ]
101 }
102 },
103 {
104 "type": "Feature",
105 "properties": {
106 "transect_id": "T003",
107 "vessel": "Lomaiviti",
108 "survey_date": "2025-08-12",
109 "length_m": 351823.46
110 },
111 "geometry": {
112 "type": "MultiLineString",
113 "coordinates": [
114 [
115 [
116 1881698.8337329356,
117 3959945.940913004
118 ],
119 [
120 1969475.352082875,
121 3953932.1918599945
122 ],
123 [
124 2057192.4957280513,
125 3947717.5506673623
126 ],
127 [
128 2132707.1824515397,
129 3942028.7593159503
130 ]
131 ],
132 [
133 [
134 2132707.1824515397,
135 3942028.7593159503
136 ],
137 [
138 2144865.828376211,
139 3941086.0440104534
140 ],
141 [
142 2232501.11407936,
143 3933301.4671774227
144 ]
145 ]
146 ]
147 }
148 },
149 {
150 "type": "Feature",
151 "properties": {
152 "transect_id": "T004",
153 "vessel": "Vanua I",
154 "survey_date": "2025-08-15",
155 "length_m": 372666.09
156 },
157 "geometry": {
158 "type": "MultiLineString",
159 "coordinates": [
160 [
161 [
162 2013096.2318831745,
163 3911101.2374744187
164 ],
165 [
166 2084064.7331161185,
167 3933595.078164197
168 ],
169 [
170 2132746.0370150185,
171 3947953.374021069
172 ]
173 ],
174 [
175 [
176 2132746.0370150185,
177 3947953.374021069
178 ],
179 [
180 2155191.552825498,
181 3954527.977713378
182 ],
183 [
184 2226505.8923469945,
185 3977271.8304685825
186 ],
187 [
188 2297989.454556443,
189 3998716.763596262
190 ],
191 [
192 2369647.398370236,
193 4019426.6353879017
194 ]
195 ]
196 ]
197 }
198 },
199 {
200 "type": "Feature",
201 "properties": {
202 "transect_id": "T005",
203 "vessel": "Bligh",
204 "survey_date": "2025-08-19",
205 "length_m": 362394.57
206 },
207 "geometry": {
208 "type": "MultiLineString",
209 "coordinates": [
210 [
211 [
212 1877826.2337985218,
213 3897452.1115597077
214 ],
215 [
216 1937585.6830181086,
217 3888095.4565095943
218 ],
219 [
220 1997281.7339928765,
221 3878356.8414042895
222 ],
223 [
224 2056921.5799520519,
225 3869852.4157240363
226 ],
227 [
228 2116501.4382828893,
229 3859064.775548195
230 ],
231 [
232 2132132.72367144,
233 3856392.831893989
234 ]
235 ],
236 [
237 [
238 2132132.72367144,
239 3856392.831893989
240 ],
241 [
242 2176031.798402194,
243 3848816.3934150883
244 ],
245 [
246 2235528.2870951835,
247 3839483.53885849
248 ]
249 ]
250 ]
251 }
252 },
253 {
254 "type": "Feature",
255 "properties": {
256 "transect_id": "T006",
257 "vessel": "Cakaulevu",
258 "survey_date": "2025-08-13",
259 "length_m": 393605.56
260 },
261 "geometry": {
262 "type": "MultiLineString",
263 "coordinates": [
264 [
265 [
266 1986477.0291858702,
267 3847052.749570283
268 ],
269 [
270 2048183.9264241843,
271 3868847.9958887855
272 ],
273 [
274 2110030.3122592983,
275 3889929.569491461
276 ],
277 [
278 2132414.0483812992,
279 3897883.660491682
280 ]
281 ],
282 [
283 [
284 2132414.0483812992,
285 3897883.660491682
286 ],
287 [
288 2172028.247827847,
289 3911884.2606283487
290 ],
291 [
292 2234167.3185592326,
293 3932716.7800173583
294 ],
295 [
296 2296469.7202252313,
297 3954296.42636324
298 ],
299 [
300 2358907.291712429,
301 3974351.608875371
302 ]
303 ]
304 ]
305 }
306 },
307 {
308 "type": "Feature",
309 "properties": {
310 "transect_id": "T007",
311 "vessel": "Vanua I",
312 "survey_date": "2025-08-19",
313 "length_m": 414594.38
314 },
315 "geometry": {
316 "type": "MultiLineString",
317 "coordinates": [
318 [
319 [
320 1911692.438271574,
321 3983231.465846738
322 ],
323 [
324 1994362.6345828045,
325 3988871.9466625084
326 ],
327 [
328 2077074.393066105,
329 3993892.378778944
330 ],
331 [
332 2133065.3654978485,
333 3997328.8452844336
334 ]
335 ],
336 [
337 [
338 2133065.3654978485,
339 3997328.8452844336
340 ],
341 [
342 2159841.5664019543,
343 3998918.8506064746
344 ],
345 [
346 2242670.474942513,
347 4003071.442188108
348 ],
349 [
350 2325581.1283138376,
351 4007239.653809817
352 ]
353 ]
354 ]
355 }
356 },
357 {
358 "type": "Feature",
359 "properties": {
360 "transect_id": "T008",
361 "vessel": "Taveuni II",
362 "survey_date": "2025-08-12",
363 "length_m": 146792.55
364 },
365 "geometry": {
366 "type": "MultiLineString",
367 "coordinates": [
368 [
369 [
370 2068958.3651469543,
371 3896474.8093812363
372 ],
373 [
374 2097941.3062179913,
375 3900199.435533137
376 ],
377 [
378 2126942.1228124853,
379 3904630.4103711965
380 ],
381 [
382 2132465.642506932,
383 3905584.342467365
384 ]
385 ],
386 [
387 [
388 2132465.642506932,
389 3905584.342467365
390 ],
391 [
392 2155963.4499872266,
393 3909624.458919142
394 ],
395 [
396 2184998.036693292,
397 3914066.6121028704
398 ],
399 [
400 2214051.662654211,
401 3918654.097943046
402 ]
403 ]
404 ]
405 }
406 },
407 {
408 "type": "Feature",
409 "properties": {
410 "transect_id": "T009",
411 "vessel": "Vanua I",
412 "survey_date": "2025-08-13",
413 "length_m": 263388.29
414 },
415 "geometry": {
416 "type": "MultiLineString",
417 "coordinates": [
418 [
419 [
420 1927285.4610478787,
421 4037732.1943981266
422 ],
423 [
424 1979888.171983514,
425 4034317.8200324415
426 ],
427 [
428 2032472.1715915422,
429 4030761.5922226436
430 ],
431 [
432 2085042.7925657453,
433 4027610.627861323
434 ],
435 [
436 2133238.856866916,
437 4024688.3579825987
438 ]
439 ],
440 [
441 [
442 2133238.856866916,
443 4024688.3579825987
444 ],
445 [
446 2137604.3518881057,
447 4024418.1879896913
448 ],
449 [
450 2190162.2360857013,
451 4021349.754676055
452 ]
453 ]
454 ]
455 }
456 },
457 {
458 "type": "Feature",
459 "properties": {
460 "transect_id": "T010",
461 "vessel": "Vanua I",
462 "survey_date": "2025-08-15",
463 "length_m": 489842.48
464 },
465 "geometry": {
466 "type": "MultiLineString",
467 "coordinates": [
468 [
469 [
470 1878020.6287486383,
471 3968978.967213469
472 ],
473 [
474 2040133.8855144326,
475 3948137.8948760154
476 ],
477 [
478 2132667.393619207,
479 3935979.8021080676
480 ]
481 ],
482 [
483 [
484 2132667.393619207,
485 3935979.8021080676
486 ],
487 [
488 2201937.698953716,
489 3926588.128825637
490 ],
491 [
492 2363529.5082588177,
493 3904005.3603546056
494 ]
495 ]
496 ]
497 }
498 },
499 {
500 "type": "Feature",
501 "properties": {
502 "transect_id": "T011",
503 "vessel": "Taveuni II",
504 "survey_date": "2025-08-20",
505 "length_m": 78492.17
506 },
507 "geometry": {
508 "type": "MultiLineString",
509 "coordinates": [
510 [
511 [
512 2465344.860581143,
513 3906767.4726070557
514 ],
515 [
516 2452339.7066555577,
517 3904904.8687637383
518 ],
519 [
520 2439358.6785597224,
521 3903920.698017729
522 ],
523 [
524 2426380.6456198604,
525 3902914.5859909956
526 ],
527 [
528 2413382.539078825,
529 3900783.353759157
530 ],
531 [
532 2400405.893514905,
533 3899474.351449523
534 ],
535 [
536 2387423.5467098975,
537 3897686.467113443
538 ]
539 ]
540 ]
541 }
542 },
543 {
544 "type": "Feature",
545 "properties": {
546 "transect_id": "T012",
547 "vessel": "Taveuni II",
548 "survey_date": "2025-08-20",
549 "length_m": 84537.56
550 },
551 "geometry": {
552 "type": "MultiLineString",
553 "coordinates": [
554 [
555 [
556 1741748.7332561535,
557 3890283.693144509
558 ],
559 [
560 1746692.5945271784,
561 3918471.030985262
562 ],
563 [
564 1751658.9788316165,
565 3946432.2638834873
566 ],
567 [
568 1756655.23549125,
569 3973496.030364219
570 ]
571 ]
572 ]
573 }
574 },
575 {
576 "type": "Feature",
577 "properties": {
578 "transect_id": "T013",
579 "vessel": "Bligh",
580 "survey_date": "2025-08-19",
581 "length_m": 179800.48
582 },
583 "geometry": {
584 "type": "MultiLineString",
585 "coordinates": [
586 [
587 [
588 2482137.408760446,
589 3917127.8950326387
590 ],
591 [
592 2428688.657510872,
593 3889083.6235283166
594 ],
595 [
596 2375443.2052126382,
597 3862380.253960575
598 ],
599 [
600 2322350.650514273,
601 3834700.4346561665
602 ]
603 ]
604 ]
605 }
606 },
607 {
608 "type": "Feature",
609 "properties": {
610 "transect_id": "T014",
611 "vessel": "Lomaiviti",
612 "survey_date": "2025-08-13",
613 "length_m": 148392.88
614 },
615 "geometry": {
616 "type": "MultiLineString",
617 "coordinates": [
618 [
619 [
620 1763975.2418630726,
621 4043578.795124353
622 ],
623 [
624 1798590.965242443,
625 4030582.772307513
626 ],
627 [
628 1833155.9390679915,
629 4017897.3350352263
630 ],
631 [
632 1867693.298104288,
633 4002219.6929108296
634 ],
635 [
636 1902160.329611775,
637 3989702.4129796983
638 ]
639 ]
640 ]
641 }
642 },
643 {
644 "type": "Feature",
645 "properties": {
646 "transect_id": "T015",
647 "vessel": "Vanua I",
648 "survey_date": "2025-08-15",
649 "length_m": 124738.16
650 },
651 "geometry": {
652 "type": "MultiLineString",
653 "coordinates": [
654 [
655 [
656 2423739.783700946,
657 4026263.28896241
658 ],
659 [
660 2403163.542824007,
661 4022543.302237784
662 ],
663 [
664 2382622.078231965,
665 4020081.2554188394
666 ],
667 [
668 2362078.3244465566,
669 4016938.6757560875
670 ],
671 [
672 2341534.154948448,
673 4013127.314926124
674 ],
675 [
676 2321005.3259064667,
677 4009569.4162317035
678 ],
679 [
680 2300505.4150438267,
681 4007278.1846613027
682 ]
683 ]
684 ]
685 }
686 },
687 {
688 "type": "Feature",
689 "properties": {
690 "transect_id": "T016",
691 "vessel": "Taveuni II",
692 "survey_date": "2025-08-15",
693 "length_m": 53310.24
694 },
695 "geometry": {
696 "type": "MultiLineString",
697 "coordinates": [
698 [
699 [
700 1967763.1376268503,
701 3864348.9772176393
702 ],
703 [
704 1981044.9340631447,
705 3864774.370362437
706 ],
707 [
708 1994327.5364892795,
709 3864095.7190000876
710 ],
711 [
712 2007608.7888293017,
713 3862426.337582078
714 ],
715 [
716 2020890.884403137,
717 3863623.624927437
718 ]
719 ]
720 ]
721 }
722 },
723 {
724 "type": "Feature",
725 "properties": {
726 "transect_id": "T017",
727 "vessel": "Taveuni II",
728 "survey_date": "2025-08-12",
729 "length_m": 152432.38
730 },
731 "geometry": {
732 "type": "MultiLineString",
733 "coordinates": [
734 [
735 [
736 2459196.783273682,
737 4040076.8577193925
738 ],
739 [
740 2445503.0422038976,
741 4017855.351608406
742 ],
743 [
744 2431865.897774811,
745 3997180.8985599047
746 ],
747 [
748 2418220.5069328533,
749 3974931.1076368974
750 ],
751 [
752 2404624.705595245,
753 3953897.7538502435
754 ],
755 [
756 2391055.945635675,
757 3932981.1069712103
758 ],
759 [
760 2377499.6209094804,
761 3911400.2397073773
762 ]
763 ]
764 ]
765 }
766 },
767 {
768 "type": "Feature",
769 "properties": {
770 "transect_id": "T018",
771 "vessel": "Bligh",
772 "survey_date": "2025-08-19",
773 "length_m": 112421.62
774 },
775 "geometry": {
776 "type": "MultiLineString",
777 "coordinates": [
778 [
779 [
780 1718720.2972681404,
781 4014591.143533892
782 ],
783 [
784 1738197.5781078157,
785 3994553.0214466406
786 ],
787 [
788 1757642.797153981,
789 3974080.649730659
790 ],
791 [
792 1777042.657001174,
793 3954256.1167427967
794 ],
795 [
796 1796415.9225681531,
797 3933347.3340493958
798 ]
799 ]
800 ]
801 }
802 },
803 {
804 "type": "Feature",
805 "properties": {
806 "transect_id": "T019",
807 "vessel": "Lomaiviti",
808 "survey_date": "2025-08-12",
809 "length_m": 179435.73
810 },
811 "geometry": {
812 "type": "MultiLineString",
813 "coordinates": [
814 [
815 [
816 1921232.177273387,
817 3921611.4164991337
818 ],
819 [
820 1877615.3144519601,
821 3932437.2333025597
822 ],
823 [
824 1833949.291065143,
825 3942738.01477121
826 ],
827 [
828 1790234.490579545,
829 3952596.93857858
830 ],
831 [
832 1746468.6202754881,
833 3962251.7484134217
834 ]
835 ]
836 ]
837 }
838 },
839 {
840 "type": "Feature",
841 "properties": {
842 "transect_id": "T020",
843 "vessel": "Taveuni II",
844 "survey_date": "2025-08-13",
845 "length_m": 215352.23
846 },
847 "geometry": {
848 "type": "MultiLineString",
849 "coordinates": [
850 [
851 [
852 1799842.78527887,
853 3989614.6337389858
854 ],
855 [
856 1850089.5464570043,
857 3969622.1670543184
858 ],
859 [
860 1900223.5116513204,
861 3950791.606715858
862 ],
863 [
864 1950261.0912236797,
865 3930816.037580312
866 ],
867 [
868 2000196.12611439,
869 3910677.6427223445
870 ]
871 ]
872 ]
873 }
874 },
875 {
876 "type": "Feature",
877 "properties": {
878 "transect_id": "T021",
879 "vessel": "Vanua I",
880 "survey_date": "2025-08-16",
881 "length_m": 230219.24
882 },
883 "geometry": {
884 "type": "MultiLineString",
885 "coordinates": [
886 [
887 [
888 1734792.6614569672,
889 3988023.0114604924
890 ],
891 [
892 1796140.996215118,
893 3941226.4356850684
894 ],
895 [
896 1857192.2097571124,
897 3895079.338822874
898 ],
899 [
900 1917953.2043430656,
901 3848549.896766379
902 ]
903 ]
904 ]
905 }
906 },
907 {
908 "type": "Feature",
909 "properties": {
910 "transect_id": "T022",
911 "vessel": "Vanua I",
912 "survey_date": "2025-08-15",
913 "length_m": 50981.78
914 },
915 "geometry": {
916 "type": "MultiLineString",
917 "coordinates": [
918 [
919 [
920 1826735.497725808,
921 3908468.0307612177
922 ],
923 [
924 1833530.8357058733,
925 3915690.085356508
926 ],
927 [
928 1840325.6909893686,
929 3923592.9662667327
930 ],
931 [
932 1847131.1924793813,
933 3930812.0080819936
934 ],
935 [
936 1853932.773435727,
937 3939251.123958868
938 ],
939 [
940 1860748.965022611,
941 3946407.5329954834
942 ]
943 ]
944 ]
945 }
946 },
947 {
948 "type": "Feature",
949 "properties": {
950 "transect_id": "T023",
951 "vessel": "Lomaiviti",
952 "survey_date": "2025-08-12",
953 "length_m": 81980.09
954 },
955 "geometry": {
956 "type": "MultiLineString",
957 "coordinates": [
958 [
959 [
960 2430814.5176551496,
961 3920116.8471598867
962 ],
963 [
964 2415479.5967341997,
965 3913735.5872768546
966 ],
967 [
968 2400171.1034651063,
969 3908063.181956481
970 ],
971 [
972 2384901.9803659474,
973 3903846.206101202
974 ],
975 [
976 2369582.6457509208,
977 3896469.9735108716
978 ],
979 [
980 2354312.0603208095,
981 3891057.5105217276
982 ]
983 ]
984 ]
985 }
986 },
987 {
988 "type": "Feature",
989 "properties": {
990 "transect_id": "T024",
991 "vessel": "Vanua I",
992 "survey_date": "2025-08-12",
993 "length_m": 251074.91
994 },
995 "geometry": {
996 "type": "MultiLineString",
997 "coordinates": [
998 [
999 [
1000 2228238.4566725227,
1001 4015411.736894843
1002 ],
1003 [
1004 2276757.963919763,
1005 4002318.5037398003
1006 ],
1007 [
1008 2325229.232446313,
1009 3988882.404731997
1010 ],
1011 [
1012 2373665.340709087,
1013 3975810.063902585
1014 ],
1015 [
1016 2422085.660914305,
1017 3963774.4034233717
1018 ],
1019 [
1020 2470420.227856475,
1021 3949290.9864591043
1022 ]
1023 ]
1024 ]
1025 }
1026 },
1027 {
1028 "type": "Feature",
1029 "properties": {
1030 "transect_id": "T025",
1031 "vessel": "Taveuni II",
1032 "survey_date": "2025-08-19",
1033 "length_m": 108459.05
1034 },
1035 "geometry": {
1036 "type": "MultiLineString",
1037 "coordinates": [
1038 [
1039 [
1040 1769898.113943761,
1041 3863034.971961179
1042 ],
1043 [
1044 1769580.71284751,
1045 3898636.037103303
1046 ],
1047 [
1048 1769258.2869353632,
1049 3935337.3281767596
1050 ],
1051 [
1052 1768950.0859736353,
1053 3971489.878227382
1054 ]
1055 ]
1056 ]
1057 }
1058 },
1059 {
1060 "type": "Feature",
1061 "properties": {
1062 "transect_id": "T026",
1063 "vessel": "Cakaulevu",
1064 "survey_date": "2025-08-19",
1065 "length_m": 100661.48
1066 },
1067 "geometry": {
1068 "type": "MultiLineString",
1069 "coordinates": [
1070 [
1071 [
1072 1987271.4052228667,
1073 3939376.2990370244
1074 ],
1075 [
1076 1967307.9907347183,
1077 3936889.1958410414
1078 ],
1079 [
1080 1947350.861923434,
1081 3933738.235256531
1082 ],
1083 [
1084 1927393.682270505,
1085 3932163.1557427393
1086 ],
1087 [
1088 1907448.9126200052,
1089 3928390.8376744
1090 ],
1091 [
1092 1887498.9116150867,
1093 3926697.0667341426
1094 ]
1095 ]
1096 ]
1097 }
1098 },
1099 {
1100 "type": "Feature",
1101 "properties": {
1102 "transect_id": "T027",
1103 "vessel": "Vanua I",
1104 "survey_date": "2025-08-12",
1105 "length_m": 130089.67
1106 },
1107 "geometry": {
1108 "type": "MultiLineString",
1109 "coordinates": [
1110 [
1111 [
1112 2369948.7263461673,
1113 3976319.9325690367
1114 ],
1115 [
1116 2346287.470686171,
1117 3964968.188987548
1118 ],
1119 [
1120 2322678.80831227,
1121 3955012.8928886973
1122 ],
1123 [
1124 2299080.230015055,
1125 3943891.124994715
1126 ],
1127 [
1128 2275520.6251862263,
1129 3933450.761366028
1130 ],
1131 [
1132 2251971.126515633,
1133 3921591.3665094175
1134 ]
1135 ]
1136 ]
1137 }
1138 },
1139 {
1140 "type": "Feature",
1141 "properties": {
1142 "transect_id": "T028",
1143 "vessel": "Vanua I",
1144 "survey_date": "2025-08-12",
1145 "length_m": 173047.04
1146 },
1147 "geometry": {
1148 "type": "MultiLineString",
1149 "coordinates": [
1150 [
1151 [
1152 2330479.7686202456,
1153 4050623.346325348
1154 ],
1155 [
1156 2372415.289708903,
1157 4039380.3649832667
1158 ],
1159 [
1160 2414329.386504559,
1161 4028381.670683045
1162 ],
1163 [
1164 2456265.7878334904,
1165 4019496.345153804
1166 ],
1167 [
1168 2498125.9280114328,
1169 4007925.0495241787
1170 ]
1171 ]
1172 ]
1173 }
1174 },
1175 {
1176 "type": "Feature",
1177 "properties": {
1178 "transect_id": "T029",
1179 "vessel": "Lomaiviti",
1180 "survey_date": "2025-08-19",
1181 "length_m": 205262.64
1182 },
1183 "geometry": {
1184 "type": "MultiLineString",
1185 "coordinates": [
1186 [
1187 [
1188 2496390.5070293318,
1189 3872926.5081186867
1190 ],
1191 [
1192 2456887.5880763554,
1193 3906639.2715288056
1194 ],
1195 [
1196 2417234.1074046907,
1197 3939224.290901031
1198 ],
1199 [
1200 2377443.041219402,
1201 3970892.0161980125
1202 ],
1203 [
1204 2337541.907860386,
1205 4002897.035326764
1206 ]
1207 ]
1208 ]
1209 }
1210 },
1211 {
1212 "type": "Feature",
1213 "properties": {
1214 "transect_id": "T030",
1215 "vessel": "Cakaulevu",
1216 "survey_date": "2025-08-19",
1217 "length_m": 163829.39
1218 },
1219 "geometry": {
1220 "type": "MultiLineString",
1221 "coordinates": [
1222 [
1223 [
1224 2370599.7528943215,
1225 4007936.284838661
1226 ],
1227 [
1228 2348033.804542877,
1229 3992395.776298311
1230 ],
1231 [
1232 2325521.730368626,
1233 3977972.568507736
1234 ],
1235 [
1236 2303009.840542533,
1237 3961256.978484933
1238 ],
1239 [
1240 2280550.736391237,
1241 3945560.2422843603
1242 ],
1243 [
1244 2258135.8435976296,
1245 3930440.8739923015
1246 ],
1247 [
1248 2235752.355614778,
1249 3914958.146484169
1250 ]
1251 ]
1252 ]
1253 }
1254 }
1255 ]
1256 }
35:54
Let me verify a few things about the output: 1. All features are MultiLineString - ✓ 2. The antimeridian-crossing ones (T001-T010 with crosses_antimeridian_flag=true) have been split into multi-part geometries - ✓ 3. Survey attributes (vessel, survey_date) are preserved - ✓ 4. transect_id is present - ✓ 5. length_m in metres - ✓ 6. CRS is EPSG:3460 (Fiji Map Grid) - ✓ Let me also double-check the antimeridian splitting logic more carefully. The issue is that coordinates like `[179.971..., -17.611...]` followed by `[-179.513..., -17.596...]` would render as an arc across the globe. My approach shifts negative longitudes by +360 to make them continuous. Let me verify with T001: - Original: [178.94, -17.63], [179.46, -17.62], [179.97, -17.61], [-179.51, -17.60], [-179.00, -17.58], [-178.48, -17.58] - Shifted: [178.94, -17.63], [179.46, -17.62], [179.97, -17.61], [180.49, -17.60], [181.00, -17.58], [181.52, -17.58] The crossings: - Segment 3->4: 179.97 < 180 < 180.49 → crosses. Split at 180. - Segment 4->5: 180.49 to 181.00, both > 180, no crossing. - Segment 5->6: 181.00 to 181.52, both > 180, no crossing. So first seg: [178.94, 179.46, 179.97, 180.0] Second seg: [180.0, 180.49, 181.00, 181.52] Convert back: first seg stays as is (all < 180 already), second becomes [180-360=-180, ...] wait... Actually looking more carefully: the conversion back says `(lon - 360, lat) if lon > 180 else (lon, lat)`. So: - First seg: [178.94, ...], [179.46,...], [179.97,...], [180.0,...] → all ≤ 180, so stays as is. But we need to check - is 180.0 > 180? No, 180.0 is not > 180. So it stays as (180, lat). But (180, lat) is the antimeridian. Actually wait, the first seg after split: [178.94, -17.63], [179.46, -17.62], [179.97, -17.61], [180.0, -17.60] - these are all <= 180, so they stay as-is. Good. Second seg: [180.0, -17.60], [180.49, -17.58], [181.00, -17.58], [181.52, -17.58] → 180.0 stays, 180.49 - 360 = -179.51, 181.00 - 360 = -179.00, 181.52 - 360 = -178.48. So the second seg becomes [180.0, -17.60], [-179.51, -17.58], [-179.00, -17.58], [-178.48, -17.58]. Hmm, 180.0 is still a valid longitude. That should be fine as a split point. Actually wait, I want to check something. The coordinates in the output for T001 are: ``` [2020343.34, 3930017.47], [2074969.33, 3930832.11], [2129608.40, 3931959.04], [2132641.35, 3932030.62] ``` and ``` [2132641.35, 3932030.62], [2184265.93, 3933178.87], [2238946.88, 3934438.09], [2293630.65, 3933970.52] ``` These look reasonable for Fiji Map Grid coordinates (easting ~2,000,000 to ~2,300,000m, northing ~3,930,000 to ~3,934,000m which is near -17°S in the southern hemisphere). Let me also verify T011 which doesn't cross the antimeridian - it should be MultiLineString with a single part. Looking at T011 output: ``` "type": "MultiLineString", "coordinates": [[[2465344.86, 3906767.47], ..., [2387423.55, 3897686.47]]] ``` Yes, it's a single-part MultiLineString. Good. Now, I notice something about the output - the antimeridian-crossing transects have the split point at 180° which is fine. But let me verify that the projection of 180° lon to EPSG:3460 makes sense. The Fiji Map Grid is a Transverse Mercator with central meridian at 178.75°E. So 180° is close to the central meridian. The coordinates at the split point for T001 are [2132641.35, 3932030.62] which is about 132,641m east of the central meridian, and about 3,932,031m north of the equator minus the false northing of 4,000,000... Actually the false northing is 4,000,000 and we're south of the equator (lat -17°), so northing = 4,000,000 - something ≈ 3,930,000 which checks out. The output looks good! All features are MultiLineString, the antimeridian-crossing ones have been properly split, survey attributes are preserved, and lengths are calculated. Let me also quickly verify the GeoJSON is valid.
35:54
35:54
Bash
cd /work && python3 << 'PYEOF'
cd /work && python3 << 'PYEOF'
import json
with open('/work/fiji_transects_fmg.geojson') as f:
data = json.load(f)
print(f"Type: {data['type']}")
print(f"CRS: {data['crs']}")
print(f"Number of features: {len(data['features'])}")
print()
for feat in data['features']:
props = feat['properties']
geom = feat['geometry']
geom_type = geom['type']
n_parts = len(geom['coordinates'])
print(f" {props['transect_id']:5s} | {props['vessel']:15s} | {props['survey_date']:12s} | {geom_type:15s} | {n_parts:2d} part(s) | length_m={props['length_m']:>8.2f}")
assert geom_type == 'MultiLineString', f"NOT MultiLineString: {geom_type}"
print()
# Check that antimeridian crossers have multiple parts
for feat in data['features']:
pid = feat['properties']['transect_id']
n_parts = len(feat['geometry']['coordinates'])
if pid in ['T001','T002','T003','T004','T005','T006','T007','T008','T009','T010']:
assert n_parts == 2, f"{pid} should be 2 parts but got {n_parts}"
else:
assert n_parts == 1, f"{pid} should be 1 part but got {n_parts}"
print("All assertions passed!")
PYEOF
Type: FeatureCollection
CRS: {'type': 'name', 'properties': {'name': 'urn:ogc:def:crs:EPSG:3460'}}
Number of features: 30
T001 | Taveuni II | 2025-08-15 | MultiLineString | 2 part(s) | length_m=273335.11
T002 | Bligh | 2025-08-12 | MultiLineString | 2 part(s) | length_m=216575.29
T003 | Lomaiviti | 2025-08-12 | MultiLineString | 2 part(s) | length_m=351823.46
T004 | Vanua I | 2025-08-15 | MultiLineString | 2 part(s) | length_m=372666.09
T005 | Bligh | 2025-08-19 | MultiLineString | 2 part(s) | length_m=362394.57
T006 | Cakaulevu | 2025-08-13 | MultiLineString | 2 part(s) | length_m=393605.56
T007 | Vanua I | 2025-08-19 | MultiLineString | 2 part(s) | length_m=414594.38
T008 | Taveuni II | 2025-08-12 | MultiLineString | 2 part(s) | length_m=146792.55
T009 | Vanua I | 2025-08-13 | MultiLineString | 2 part(s) | length_m=263388.29
T010 | Vanua I | 2025-08-15 | MultiLineString | 2 part(s) | length_m=489842.48
T011 | Taveuni II | 2025-08-20 | MultiLineString | 1 part(s) | length_m=78492.17
T012 | Taveuni II | 2025-08-20 | MultiLineString | 1 part(s) | length_m=84537.56
T013 | Bligh | 2025-08-19 | MultiLineString | 1 part(s) | length_m=179800.48
T014 | Lomaiviti | 2025-08-13 | MultiLineString | 1 part(s) | length_m=148392.88
T015 | Vanua I | 2025-08-15 | MultiLineString | 1 part(s) | length_m=124738.16
T016 | Taveuni II | 2025-08-15 | MultiLineString | 1 part(s) | length_m=53310.24
T017 | Taveuni II | 2025-08-12 | MultiLineString | 1 part(s) | length_m=152432.38
T018 | Bligh | 2025-08-19 | MultiLineString | 1 part(s) | length_m=112421.62
T019 | Lomaiviti | 2025-08-12 | MultiLineString | 1 part(s) | length_m=179435.73
T020 | Taveuni II | 2025-08-13 | MultiLineString | 1 part(s) | length_m=215352.23
T021 | Vanua I | 2025-08-16 | MultiLineString | 1 part(s) | length_m=230219.24
T022 | Vanua I | 2025-08-15 | MultiLineString | 1 part(s) | length_m=50981.78
T023 | Lomaiviti | 2025-08-12 | MultiLineString | 1 part(s) | length_m=81980.09
T024 | Vanua I | 2025-08-12 | MultiLineString | 1 part(s) | length_m=251074.91
T025 | Taveuni II | 2025-08-19 | MultiLineString | 1 part(s) | length_m=108459.05
T026 | Cakaulevu | 2025-08-19 | MultiLineString | 1 part(s) | length_m=100661.48
T027 | Vanua I | 2025-08-12 | MultiLineString | 1 part(s) | length_m=130089.67
T028 | Vanua I | 2025-08-12 | MultiLineString | 1 part(s) | length_m=173047.04
T029 | Lomaiviti | 2025-08-19 | MultiLineString | 1 part(s) | length_m=205262.64
T030 | Cakaulevu | 2025-08-19 | MultiLineString | 1 part(s) | length_m=163829.39
All assertions passed!
37:17
Wait, I need to check that the antimeridian-crossing features are properly split. Let me look at T001 more carefully.
T001 coordinates in the original:
[ 178.94, -17.63 ], [ 179.46, -17.62 ], [ 179.97, -17.61 ], [ -179.51, -17.60 ], [ -179.00, -17.58 ], [ -178.48, -17.58 ]
So it goes from 178.94 → 179.46 → 179.97 → -179.51 (crosses 180°!) → -179.00 → -178.48
My antimeridian splitting logic:
- Shifted coords: (178.94, -17.63), (179.46, -17.62), (179.97, -17.61), (180.49, -17.60), (181.00, -17.58), (181.52, -17.58)
Wait, -179.51 + 360 = 180.49. -179.00 + 360 = 181.00. -178.48 + 360 = 181.52.
So the shifted coords are: (178.94, -17.63), (179.46, -17.62), (179.97, -17.61), (180.49, -17.60), (181.00, -17.58), (181.52, -17.58)
The crossing happens between (179.97, -17.61) and (180.49, -17.60). Since 179.97 < 180 < 180.49, we interpolate at lon=180.
So seg1 = [(178.94, -17.63), (179.46, -17.62), (179.97, -17.61), (180.0, lat_at_180)]
seg2 = [(180.0, lat_at_180), (180.49, -17.60), (181.00, -17.58), (181.52, -17.58)]
Then convert back:
seg1: (178.94, -17.63), (179.46, -17.62), (179.97, -17.61), (180, lat) → (180-360=-180, lat) = (-180, lat)
seg2: (180.49-360=-179.51, -17.60), (181.00-360=-179.00, -17.58), (181.52-360=-178.48, -17.58)
Wait, the interpolation point at lon=180 maps to -180 in WGS84. So both segments share (-180, lat_at_180). That's fine.
Now the projected result for T001 shows:
Part 1: 4 points (2020343, 3930017) → (2074969, 3930832) → (2129608, 3931959) → (2132641, 3932030)
Part 2: 4 points (2132641, 3932030) → (2184265, 3933178) → (2238946, 3934438) → (2293630, 3933970)
Wait, the first part has 4 points and the second part has 4 points (including the shared split point). Let me check...
Original has 6 points. After splitting:
- First segment: 3 original points (178.94, 179.46, 179.97) + interpolated point at 180 = 4 points
- Second segment: interpolated point + 3 remaining original points = 4 points
So 4+4=8 points total. That's correct.
But wait, looking at the output for T001 part 1 coordinates:
(2020343, 3930017) - first point
(2074969, 3930832) - second point
(2129608, 3931959) - third point
(2132641, 3932030) - fourth point (the split point at 180°)
Part 2:
(2132641, 3932030) - the split point
(2184265, 3933178) - next
(2238946, 3934438) - next
(2293630, 3933970) - last
Looks correct!
Now let me verify the non-antimeridian features are also MultiLineString (with 1 part). Looking at the output, all features are MultiLineString. T011-T030 have 1 part each, which is correct since they don't cross the antimeridian.
Wait, but there's a subtle issue. For non-antimeridian features, I just wrap them in a MultiLineString with 1 part. But the user said "Every feature must come out as a MultiLineString, so do not leave any transects as plain LineString" - which is satisfied. And "the problematic ones must end up as multi-part geometries that faithfully represent the actual survey path" - which is also satisfied.
Let me also verify the length calculation. The length is computed in the projected CRS (EPSG:3460, Fiji Map Grid), which uses metres. So `multi.length` should give the length in metres. Good.
Let me also double-check that the `crosses_antimeridian_flag` is used correctly for identifying which ones need splitting. Looking at the input:
- T001-T010: crosses_antimeridian_flag = true
- T011-T030: crosses_antimeridian_flag = false
And in my code, I use `props.get('crosses_antimeridian_flag', False)` to decide whether to split. That's correct.
The output looks good. Let me just verify the GeoJSON is valid by trying to load it with geopandas.
37:17
37:17
Bash
cd /work && python3 << 'PYEOF'
cd /work && python3 << 'PYEOF'
import geopandas as gpd
# Read back the output
gdf = gpd.read_file('/work/fiji_transects_fmg.geojson')
print(gdf.head(3))
print(f"\nCRS: {gdf.crs}")
print(f"All geom types: {gdf.geometry.type.unique()}")
print(f"All MultiLineString: {(gdf.geometry.type == 'MultiLineString').all()}")
print(f"\nTotal features: {len(gdf)}")
PYEOF
transect_id ... geometry 0 T001 ... MULTILINESTRING ((2020343.341 3930017.467, 207... 1 T002 ... MULTILINESTRING ((2034023.758 3962923.929, 210... 2 T003 ... MULTILINESTRING ((1881698.834 3959945.941, 196... [3 rows x 5 columns] CRS: EPSG:3460 All geom types: <ArrowStringArray> ['MultiLineString'] Length: 1, dtype: str All MultiLineString: True Total features: 30
37:33
Everything looks good. The output file is valid, all features are MultiLineString, the antimeridian-crossing transects (T001-T010) are split into 2-part MultiLineStrings, and all attributes are carried through with the added length_m field. Let me provide a summary.
37:33