crs-l2-fiji-antimeridian

Status: done
Score: 100
Duration: 4:09
Cost: 1.02¢
Model: deepseek/deepseek-v4-flash

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:09
Let me start by reading the input file to understand its structure and identify the problematic geometries.
0:09
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:35
Let me analyze the data:

1. There are 30 transects, some crossing the antimeridian (180° longitude). 
2. Transects flagged `crosses_antimeridian_flag: true` have coordinates that cross the 180° line. In GeoJSON, longitudes are typically -180 to 180. When a line crosses the antimeridian, rendering engines often draw a line wrapping the entire globe instead of the short local path.

For example, T001 has coordinates:
- [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 actual path goes from 178.94 → 179.46 → 179.97 (approaching 180 from east), then jumps to -179.51 (just west of 180), then continues westward. The renderer draws a line from 179.97 all the way around the globe the long way to -179.51.

The fix: For each segment that crosses the antimeridian, we need to split the LineString into multiple parts at the antimeridian (+/- 180°), so that each part stays on one side. This way each part renders as a short local line.

The standard approach:
1. For each coordinate, check if it's on the "east" side (lon > 0) or "west" side (lon < 0)
2. When the sign changes between consecutive coordinates, interpolate the intersection at lon=180 (or -180 depending on direction)
3. Split the line into separate segments at these intersection points
4. Each segment stays entirely on one side of the antimeridian

Actually, a cleaner approach:
- Shift coordinates that are on the "wrong" side by +360 or -360 to make the line continuous
- Then split at 180° longitude

Let me think about this more carefully.

For T001: [178.94, 179.46, 179.97, -179.51, -179.00, -178.48]
- These are all around Fiji, near 180° longitude
- The actual path: 178.94 → 179.46 → 179.97 (east side) → then crosses to -179.51 (west side, but this is just 180.49 in other notation) → -179.00 → -178.48
- So the line crosses from just east of 180 to just west of 180

If we convert all to a continuous coordinate system (e.g. add 360 to the negative ones when they follow positive ones), we get:
- 178.94, 179.46, 179.97, 180.49 (= -179.51+360), 181.00 (= -179.00+360), 181.52 (= -178.48+360)

Now we split at lon=180. The segment from 179.97 to 180.49 crosses 180. The intersection point is at lon=180, lat interpolated.

Then we convert back to -180..180 for the west-side part.

For each transect that crosses the antimeridian, I'll create a MultiLineString with:
- Part 1: points from start up to the intersection at 180°, using lon values as-is
- Part 2: points from the intersection at -180° (or 180°) to the end, using -180..180 convention

Let me write a Python script to do all of this.

The target CRS for Fiji: I need to find Fiji's national metric grid. Fiji uses "Fiji 1986 / Fiji Map Grid" which is EPSG:3460. Let me verify... Actually, there are a few options:
- EPSG:3460 - Fiji 1986 / Fiji Map Grid (this is the national one)

Let me write the script.

Actually, let me think about the proper approach for splitting at the antimeridian:

1. For each coordinate pair (x, y) where x is longitude:
   - If the line crosses from positive to negative (or vice versa) between two consecutive points, we need to find where it crosses 180° (or -180°)
   - Actually, we check if the difference between consecutive longitudes is more than 180° in absolute value — that indicates an antimeridian crossing

2. The interpolation: given two points (lon1, lat1) and (lon2, lat2) where lon1 and lon2 are on opposite sides:
   - If lon1 > 0 and lon2 < 0, the crossing is at lon=180 (same as -180)
   - Actually we want to find the point where longitude = 180 (or -180)
   - Using linear interpolation: t = (180 - lon1) / (lon2 - lon1) where lon2 might be negative
   - But wait, if lon2 < 0, we should adjust it: lon2_adj = lon2 + 360
   - Then t = (180 - lon1) / ((lon2 + 360) - lon1)
   - lat = lat1 + t * (lat2 - lat1)

Let me write the script.

```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
import math

# Read the GeoJSON
gdf = gpd.read_file('/work/fiji_transects_wgs84.geojson')

def split_antimeridian(line):
    """Split a LineString that crosses the antimeridian into multiple segments."""
    coords = list(line.coords)
    
    # Check if the line crosses the antimeridian
    # We check by looking at the longitude difference between consecutive points
    crosses = False
    for i in range(len(coords) - 1):
        lon1, lat1 = coords[i]
        lon2, lat2 = coords[i+1]
        if abs(lon2 - lon1) > 180:
            crosses = True
            break
    
    if not crosses:
        return [line]
    
    # The line crosses the antimeridian
    # We'll split it into segments, each staying on one side
    segments = []
    current_segment = [coords[0]]
    
    for i in range(len(coords) - 1):
        lon1, lat1 = coords[i]
        lon2, lat2 = coords[i+1]
        
        if abs(lon2 - lon1) > 180:
            # Crossing detected - interpolate the intersection point
            if lon1 > 0 and lon2 < 0:
                # Crossing from east to west at 180
                # Adjust lon2 to be continuous
                lon2_adj = lon2 + 360
                t = (180 - lon1) / (lon2_adj - lon1)
                lat_interp = lat1 + t * (lat2 - lat1)
                interp_point = (180, lat_interp)
                
                current_segment.append(interp_point)
                segments.append(LineString(current_segment))
                
                # Start new segment on west side
                interp_point_west = (-180, lat_interp)
                current_segment = [interp_point_west, coords[i+1]]
            elif lon1 < 0 and lon2 > 0:
                # Crossing from west to east at -180/180
                lon1_adj = lon1 + 360
                t = (180 - lon1_adj) / (lon2 - lon1_adj)
                lat_interp = lat1 + t * (lat2 - lat1)
                interp_point = (180, lat_interp)
                
                current_segment.append(interp_point)
                segments.append(LineString(current_segment))
                
                # Start new segment on east side
                interp_point_east = (-180, lat_interp)
                current_segment = [interp_point_east, coords[i+1]]
        else:
            current_segment.append(coords[i+1])
    
    # Don't forget the last segment
    if len(current_segment) > 1:
        segments.append(LineString(current_segment))
    
    return segments

# Process each feature
new_features = []
for idx, row in gdf.iterrows():
    geom = row.geometry
    props = row.drop('geometry').to_dict()
    
    if geom.geom_type == 'LineString':
        segments = split_antimeridian(geom)
        if len(segments) == 1:
            # No crossing, still a LineString - need to convert to MultiLineString
            new_geom = MultiLineString([segments[0]])
        else:
            new_geom = MultiLineString(segments)
    else:
        new_geom = geom
    
    new_features.append({'type': 'Feature', 'properties': props, 'geometry': new_geom})

# Create new GeoDataFrame
new_gdf = gpd.GeoDataFrame.from_features(new_features, crs='EPSG:4326')

# Now reproject to Fiji Map Grid (EPSG:3460 - Fiji 1986 / Fiji Map Grid)
# Actually let me check the right EPSG code
# Fiji 1986 / Fiji Map Grid is EPSG:3460
new_gdf = new_gdf.to_crs('EPSG:3460')

# Calculate lengths in metres
new_gdf['length_m'] = new_gdf.geometry.length

# Ensure all geometries are MultiLineString
for idx, row in new_gdf.iterrows():
    if row.geometry.geom_type != 'MultiLineString':
        new_gdf.at[idx, 'geometry'] = MultiLineString([row.geometry])

# Write output
new_gdf.to_file('/work/fiji_transects_fmg.geojson', driver='GeoJSON')
```

Wait, I need to think more carefully. The issue is that when a coordinate goes from, say, 179.97 to -179.51, a renderer draws the line going the "long way" around the globe (across 0°), rather than the short way across the antimeridian.

The fix: split the line at the antimeridian so each part stays localized.

Let me think about the interpolation more carefully.

For 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]

Between [179.97, -17.61] and [-179.51, -17.60]:
- lon1 = 179.97, lon2 = -179.51
- Difference = |-179.51 - 179.97| = 359.48 > 180 → crossing
- lon1 > 0, lon2 < 0 → crossing from east to west
- lon2_adj = -179.51 + 360 = 180.49
- t = (180 - 179.97) / (180.49 - 179.97) = 0.03 / 0.52 = 0.05769
- lat_interp = -17.61 + 0.05769 * (-17.60 - (-17.61)) = -17.61 + 0.05769 * 0.01 = -17.61 + 0.000577 = -17.6094

So the intersection point is at (180, -17.6094) on the east side, and (-180, -17.6094) on the west side.

Segment 1 (east side): [178.94, -17.63] → [179.46, -17.62] → [179.97, -17.61] → [180, -17.6094]
Segment 2 (west side): [-180, -17.6094] → [-179.51, -17.60] → [-179.00, -17.58] → [-178.48, -17.58]

Wait, but after reprojection to a metric CRS, the two segments should meet properly.

Actually, I realize there may be a subtlety. When we have the point (180, -17.6094) which is right on the antimeridian, and the other side (-180, -17.6094) - these are the same point in geographic space. After reprojection to a local metric CRS, they should map to the same point.

Let me also consider: some transects might cross the antimeridian multiple times? Looking at the data, each line seems to cross at most once. But to be safe, let me handle the general case.

Actually, looking more carefully at 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]

This crosses once (179.54 → -179.79).

T010: [177.60, -17.28], [179.13, -17.47], [-179.35, -17.65], [-177.82, -17.84]
Crosses once (179.13 → -179.35).

OK so each crossing is at most once. Good.

Now, some of the non-antimeridian-crossing transects might also be simple LineStrings, and the user wants them all as MultiLineString. So I'll wrap them.

Let me also think about the CRS. Fiji's national grid:
- "Fiji 1986 / Fiji Map Grid" - EPSG:3460
- This uses the Fiji 1986 datum and a Transverse Mercator projection

Let me verify by checking if pyproj has this: Yes, EPSG:3460 is valid.

Actually, I should double-check. Fiji has two main CRS systems:
1. Fiji 1956 / Fiji Central Grid (EPSG:2114) - old
2. Fiji 1986 / Fiji Map Grid (EPSG:3460) - newer

The national metric grid for Fiji would be EPSG:3460 (Fiji 1986 / Fiji Map Grid).

Let me write the solution now.
0:35
Bash ls /work/
fiji_transects_wgs84.geojson
1:19
Let me understand the problem:

1. Some transects cross the antimeridian (180° longitude). When coordinates go from positive longitudes (e.g., 179.9°) to negative ones (e.g., -179.9°), GIS software can render them as arcs spanning the whole globe instead of short local segments.

2. The fix is to "unwrap" the longitudes that cross the antimeridian by adding/subtracting 360° to negative longitudes so that the coordinates are continuous (e.g., -179.9° becomes 180.1°). Then we split the geometry where it jumps across the antimeridian.

3. Then we convert to Fiji's national metric grid (Fiji Geodetic Datum 1986 / Fiji Map Grid - EPSG:3460 or EPSG:15088? Let me think... Fiji Map Grid is EPSG:3460 or EPSG:15088. Let me check. Actually, Fiji uses EPSG:3460 (Fiji 1956 / Fiji Map Grid) or EPSG:15088 (FGD 1942 / Fiji Map Grid) or maybe EPSG:3831 (Fiji 1986 / Fiji Map Grid). Let me look at what's most appropriate.

Actually, Fiji's national metric grid is "Fiji 1956 / Fiji Map Grid" (EPSG:3460) or "Fiji 1986 / Fiji Map Grid" (EPSG:3831). Since the data is in WGS84, the modern one would be "Fiji 1986 / Fiji Map Grid" (EPSG:3831). But actually, there's also EPSG:15088 "Fiji 1956 / Fiji Map Grid". Let me just use EPSG:3460 (Fiji 1956 / Fiji Map Grid) or EPSG:3832 (Fiji 1986 / Fiji Map Grid).

Wait, let me think about this more carefully. The commonly used one for Fiji is "Fiji 1956 / Fiji Map Grid" EPSG:3460. But there's also "Fiji 1986 / Fiji Map Grid" EPSG:3832 (note: 3831 is the geodetic datum, 3832 is the projected).

Actually the user says "Fiji's national metric grid" so likely "Fiji 1956 / Fiji Map Grid" EPSG:3460. But the input is in WGS84, and the Fiji 1956 datum is different from WGS84. Let me use pyproj to transform from WGS84 to Fiji Map Grid.

Actually, the most straightforward approach: Let's just use "Fiji 1956 / Fiji Map Grid" EPSG:3460. But in modern Fiji, they might use "Fiji 1986" datum. Let me check what's standard.

Actually I'll go with EPSG:3460 (Fiji 1956 / Fiji Map Grid) as it's the most well-known "Fiji Map Grid". Let me write the solution.

Here's my approach:

1. Read the GeoJSON
2. For each feature:
   a. Check if the coordinates cross the antimeridian (longitudes go from > 170 to < -170 or vice versa)
   b. For those that do, "unwrap" the longitudes: for coordinates that are negative and part of the segment that should be east of 180°, add 360 to make them continuous
   c. Split into parts where there's a big jump (the antimeridian crossing point)
   d. Create a MultiLineString from the parts
   e. For those that don't cross, still ensure they're MultiLineString
3. Reproject to Fiji Map Grid
4. Calculate length_m for each feature
5. Write out as GeoJSON

The key trick: For transects crossing the antimeridian, coordinates like 179.97, ... -179.51 should become 179.97, ... 180.49 (by adding 360 to the negative values). Then we split at the antimeridian (180° or 0° after unwrapping?).

Wait, let me think more carefully.

For T001:
Original: [178.94, 179.46, 179.97, -179.51, -178.99, -178.48]
After unwrapping (adding 360 to negative lons): [178.94, 179.46, 179.97, 180.49, 181.01, 181.52]

Now the segment goes from 179.97 to 180.49 - this crosses the 180° meridian. We need to split this into:
- Part 1: [178.94, 179.46, 179.97, 180.0] (up to the meridian)
- Part 2: [180.0, 180.49, 181.01, 181.52] (from the meridian)

But actually, a simpler approach: after unwrapping, we can just clip/shift coordinates so that any line that spans the antimeridian gets split into two parts at 180°.

Actually, the standard approach in GIS is:

1. Convert negative longitudes that are "east of 180" by adding 360 (so -179 becomes 181)
2. Find where the line crosses 180° (or 0° in the unwrapped space? Actually we need to find where it crosses 180°)
3. Split the line there

Let me think about step 2 more carefully. After adding 360 to the "far east" negative longitudes:
- For T001: [178.94, 179.46, 179.97, 180.49, 181.01, 181.52]
- The crossing from 179.97 to 180.49 crosses 180°.

So I find the segment that crosses 180° (where one is < 180 and the next is > 180), interpolate the point at 180°, and split the line into two parts.

Then, the coordinates that are > 180 get 360 subtracted to put them back in [-180, 180] range for the output GeoJSON in WGS84 before reprojection.

Wait, actually - for the reprojection, I should do the unwrapping, splitting, then reproject the two parts, then combine them into a MultiLineString.

Let me think about the implementation more carefully.

Actually, a simpler approach using shapely:

1. For each linestring, check if it crosses the antimeridian
2. If it does, "unwrap" by adding 360 to negative longitudes that are part of the eastern side after the antimeridian
3. Split at 180° 
4. Convert back by subtracting 360 from coordinates > 180
5. Make the geometry a MultiLineString
6. Reproject to Fiji Map Grid
7. Calculate length

Let me implement this.

Actually, let me think about the simplest robust approach:

For a LineString that crosses the antimeridian:
- The coordinates go from positive eastings (like 179) to negative westings (like -179)
- I can identify which segment crosses the antimeridian (where absolute difference between consecutive longitudes > 300 degrees or so)
- Split into two segments at that point
- For the segment with negative longitudes, add 360 to bring them to positive values
- This gives me a continuous line in the "unwrapped" coordinate system

Then to reproject to Fiji Map Grid, I need to transform from WGS84. But the unwrapped coordinates (> 180) won't reproject directly... So what I should do is:

1. Identify the crossing point
2. Split into two segments
3. For the segment with negative lons, keep them as-is (they're west of the antimeridian)
4. For the segment with positive lons... they're already positive, keep as-is
5. Now I have two LineStrings, each on the correct side of the antimeridian
6. Create a MultiLineString
7. Reproject each part to Fiji Map Grid
8. Calculate the total length

Wait, but the original data has coordinates on both sides. Let me just do:

1. Read coordinates
2. If the line crosses the antimeridian (detected by a big jump in longitude), split it
3. For the eastern part, keep longitudes as positive (e.g., 179)
4. For the western part, keep longitudes as negative (e.g., -179)
5. Now both parts are valid WGS84 coordinates
6. Combine into MultiLineString
7. Reproject
8. Calculate length

Let me code this up.

Actually, let me use a different approach that's cleaner. I'll use `shapely.ops.transform` and handle the antimeridian by using `split` and `normalize`.

Here's my plan:

```python
import geopandas as gpd
import numpy as np
from shapely.geometry import LineString, MultiLineString, Point
from shapely.ops import transform, split, linemerge
from pyproj import Transformer
from functools import partial
import json

# Read the data
gdf = gpd.read_file('/work/fiji_transects_wgs84.geojson')

# Define the antimeridian fix
def fix_antimeridian_line(line):
    """Fix a LineString that crosses the antimeridian by splitting it."""
    coords = list(line.coords)
    
    # Check if this line crosses the antimeridian
    # Detect by large longitude jumps (> 300 degrees)
    splits = []
    i = 0
    while i < len(coords) - 1:
        lon1, lat1 = coords[i][0], coords[i][1]
        lon2, lat2 = coords[i+1][0], coords[i+1][1]
        
        if abs(lon2 - lon1) > 300:
            # We have a crossing. Split here.
            # Interpolate the point at lon = 180 or -180
            # Going from lon1 to lon2 which is a wrap
            # If lon1 is positive (east of 180 meridian) and lon2 is negative (west)
            # then the line goes from ~179 to ~-179, crossing at 180
            # Actually the crossing is at lon=180 (or lon=-180, same line)
            
            # Calculate the fraction where the line crosses 180
            # If going from positive to negative (east to west):
            # lon1 + t * (lon2 - lon1) = 180  ... wait
            # If lon1 = 179, lon2 = -179, the shortest path goes from 179 to 180 to -179
            # So lon1 + t * (lon2 + 360 - lon1) = 180... hmm
            pass
```

Actually, let me think about this more simply with the unwrapping approach:

The standard approach I've seen work well is:

1. Copy coordinates and detect if there's a large longitude jump
2. Where a jump from positive to negative occurs, shift all subsequent coordinates by +360
3. Then split where the coordinate passes 180
4. Convert back

Let me write the full code:

```python
import geopandas as gpd
from shapely.geometry import LineString, MultiLineString, Point, box
from shapely.ops import transform, split, linemerge, snap
from pyproj import Transformer
import numpy as np
import json

def unwrap_lons(coords):
    """Unwrap longitudes to make lines crossing the antimeridian continuous."""
    result = list(coords)
    for i in range(1, len(result)):
        lon_prev = result[i-1][0]
        lon_curr = result[i][0]
        diff = lon_curr - lon_prev
        if diff > 300:
            # Going from negative (west) to positive (east) - shouldn't happen for Fiji...
            pass
        elif diff < -300:
            # Going from positive (east) to negative (west)
            result[i] = (lon_curr + 360, result[i][1])
    return result

def split_at_180(coords):
    """Split unwrapped coordinates at 180° meridian."""
    parts = []
    current_part = [coords[0]]
    
    for i in range(1, len(coords)):
        lon_prev, lat_prev = current_part[-1]
        lon_curr, lat_curr = coords[i]
        
        if (lon_prev < 180 and lon_curr >= 180) or (lon_prev >= 180 and lon_curr < 180):
            # Crosses 180 - interpolate
            t = (180 - lon_prev) / (lon_curr - lon_prev)
            lat_at_180 = lat_prev + t * (lat_curr - lat_prev)
            current_part.append((180, lat_at_180))
            parts.append(LineString(current_part))
            current_part = [(180, lat_at_180)]
        
        current_part.append(coords[i])
    
    if current_part:
        parts.append(LineString(current_part))
    
    return parts
```

Hmm, but there's a subtlety. For transects like T001, the coordinates go:
178.94, 179.46, 179.97, -179.51, -178.99, -178.48

After unwrapping (adding 360 to the negative ones after the jump):
178.94, 179.46, 179.97, 180.49, 181.01, 181.52

The crossing from 179.97 to 180.49 crosses 180. So we split at 180:
Part 1: 178.94, 179.46, 179.97, 180.0 (interpolated point at 180)
Part 2: 180.0, 180.49, 181.01, 181.52

Now for Part 2, we need to convert back to standard WGS84 coordinates by subtracting 360:
180.0-360 = -180, 180.49-360 = -179.51, etc.

So Part 2 becomes: -180.0, -179.51, -178.99, -178.48

And Part 1 stays as: 178.94, 179.46, 179.97, 180.0

Wait, the 180.0 coordinate is ambiguous. Let me think...

Actually, for WGS84, longitude range is -180 to 180. So 180 is the same as -180. Let me keep the eastern part with +180 and the western part with -180.

Part 1 (east of antimeridian): [(178.94, -17.63), (179.46, -17.62), (179.97, -17.61), (180, -17.60)]
Part 2 (west of antimeridian): [(-180, -17.60), (-179.51, -17.60), (-178.99, -17.58), (-178.48, -17.58)]

Both are valid WGS84 coordinates. Good.

But wait, when reprojecting, both parts will have coordinates in the correct hemisphere. Let me code this up properly.

Let me write a complete Python script:

```python
import geopandas as gpd
from shapely.geometry import LineString, MultiLineString, Point
from pyproj import Transformer
import numpy as np

def split_at_antimeridian(line):
    """Split a LineString that crosses the antimeridian into segments."""
    coords = list(line.coords)
    
    # First, check if this line crosses the antimeridian
    # Look for jumps in longitude > 300 degrees
    has_crossing = False
    for i in range(1, len(coords)):
        if abs(coords[i][0] - coords[i-1][0]) > 300:
            has_crossing = True
            break
    
    if not has_crossing:
        # Doesn't cross, just return as a list with one line
        return [line]
    
    # Unwrap the longitudes to make continuous
    unwrapped = list(coords)
    for i in range(1, len(unwrapped)):
        diff = unwrapped[i][0] - unwrapped[i-1][0]
        if diff > 300:
            # Going from negative to positive: subtract 360 from subsequent
            for j in range(i, len(unwrapped)):
                unwrapped[j] = (unwrapped[j][0] - 360, unwrapped[j][1])
        elif diff < -300:
            # Going from positive to negative: add 360 to subsequent
            for j in range(i, len(unwrapped)):
                unwrapped[j] = (unwrapped[j][0] + 360, unwrapped[j][1])
    
    # Now split at 180 (or -180, but with unwrapped coords, we look for crossing 180)
    parts = []
    current_seg = [unwrapped[0]]
    
    for i in range(1, len(unwrapped)):
        lon_prev, lat_prev = current_seg[-1]
        lon_curr, lat_curr = unwrapped[i]
        
        # Check if the segment crosses 180
        if (lon_prev < 180 <= lon_curr) or (lon_prev >= 180 > lon_curr):
            # Interpolate at 180
            if lon_curr - lon_prev != 0:
                t = (180 - lon_prev) / (lon_curr - lon_prev)
                lat_at_180 = lat_prev + t * (lat_curr - lat_prev)
                
                # Finish current segment
                current_seg.append((180, lat_at_180))
                parts.append(current_seg)
                
                # Start new segment
                current_seg = [(180, lat_at_180)]
            else:
                current_seg.append(unwrapped[i])
        else:
            current_seg.append(unwrapped[i])
    
    if current_seg:
        parts.append(current_seg)
    
    # Convert back to normal longitude range for WGS84
    result = []
    for seg in parts:
        normal_seg = []
        for lon, lat in seg:
            # Normalize to [-180, 180)
            lon_norm = ((lon + 180) % 360) - 180
            normal_seg.append((lon_norm, lat))
        result.append(LineString(normal_seg))
    
    return result
```

Wait, this might not handle the edge case well. Let me think about T001 again.

T001: [178.94, 179.46, 179.97, -179.51, -178.99, -178.48]

Step 1: Check for jumps > 300:
- 179.97 to -179.51: diff = -359.48, abs > 300 ✓

Step 2: Unwrap:
- Going from positive (179.97) to negative (-179.51): diff < -300
- So add 360 to all subsequent: -179.51+360 = 180.49, -178.99+360 = 181.01, -178.48+360 = 181.52
- Unwrapped: [178.94, 179.46, 179.97, 180.49, 181.01, 181.52]

Step 3: Split at 180:
- 178.94 -> 179.46: no crossing
- 179.46 -> 179.97: no crossing
- 179.97 -> 180.49: crosses 180! Interpolate at 180.
  - t = (180-179.97)/(180.49-179.97) = 0.03/0.52 = 0.0577
  - lat = -17.611 + 0.0577 * (-17.596 - (-17.611)) = -17.611 + 0.0577 * 0.015 = -17.610
  - Part 1: (178.94, -17.63), (179.46, -17.62), (179.97, -17.61), (180, -17.610)
  - Part 2: (180, -17.610), (180.49, -17.596), (181.01, -17.580), (181.52, -17.577)

Step 4: Convert back to [-180, 180]:
- Part 1: (178.94, -17.63), (179.46, -17.62), (179.97, -17.61), (-180, -17.610) ... hmm
  
Wait, 180 -> normalize: (180+180)%360-180 = 360%360-180 = -180. So (180, lat) becomes (-180, lat).

But Part 2: (180, -17.610), (180.49, -17.596), (181.01, -17.580), (181.52, -17.577)
- 180 -> -180
- 180.49 -> (180.49+180)%360 - 180 = 360.49%360 - 180 = 0.49 - 180 = -179.51
- 181.01 -> (181.01+180)%360 - 180 = 361.01%360 - 180 = 1.01 - 180 = -178.99
- 181.52 -> (181.52+180)%360 - 180 = 361.52%360 - 180 = 1.52 - 180 = -178.48

So Part 2 becomes: (-180, -17.610), (-179.51, -17.596), (-178.99, -17.580), (-178.48, -17.577)

And Part 1: (178.94, -17.63), (179.46, -17.62), (179.97, -17.61), (-180, -17.610)

Both parts meet at the antimeridian (180 or -180). Good.

Now for T002: [179.07, 179.73, -179.62, -178.96]
- Jump from 179.73 to -179.62: diff = -359.35, abs > 300 ✓
- Unwrap: [179.07, 179.73, 180.38, 181.04]
- Split at 180: 
  - 179.73 -> 180.38: crosses at 180
  - Part 1: (179.07, -17.33), (179.73, -17.17), (180, [-17.17 + t*(-17.007-(-17.171))])
  
Let me just code it carefully.

Actually, the normalization to [-180, 180) is a bit tricky for the point at exactly 180. Let me keep 180 as is (it's the antimeridian) and for the western side use -180 and values between -180 and 180.

Let me just write the code and handle it.

For the projection, Fiji Map Grid is EPSG:3460 (Fiji 1956 / Fiji Map Grid). Let me check if that's appropriate.

Actually, the user said "Fiji's national metric grid" - let me use EPSG:3460 "Fiji 1956 / Fiji Map Grid". The transformation from WGS84 (EPSG:4326) to EPSG:3460 should work.

Let me write the full script now.
1:19
1:19
Bash python3 -c "
python3 -c "
import json
with open('/work/fiji_transects_wgs84.geojson') as f:
    data = json.load(f)
for feat in data['features']:
    tid = feat['properties']['transect_id']
    coords = feat['geometry']['coordinates']
    typ = feat['geometry']['type']
    # check for antimeridian crossing
    jumps = []
    for i in range(1, len(coords)):
        jumps.append(abs(coords[i][0] - coords[i-1][0]))
    max_jump = max(jumps) if jumps else 0
    print(f'{tid} type={typ} npts={len(coords)} max_jump={max_jump:.1f} crosses={feat[\"properties\"][\"crosses_antimeridian_flag\"]}')
"
T001 type=LineString npts=6 max_jump=359.5 crosses=True
T002 type=LineString npts=4 max_jump=359.3 crosses=True
T003 type=LineString npts=5 max_jump=359.2 crosses=True
T004 type=LineString npts=6 max_jump=359.3 crosses=True
T005 type=LineString npts=7 max_jump=359.4 crosses=True
T006 type=LineString npts=7 max_jump=359.4 crosses=True
T007 type=LineString npts=6 max_jump=359.2 crosses=True
T008 type=LineString npts=6 max_jump=359.7 crosses=True
T009 type=LineString npts=6 max_jump=359.5 crosses=True
T010 type=LineString npts=4 max_jump=358.5 crosses=True
T011 type=LineString npts=7 max_jump=0.1 crosses=False
T012 type=LineString npts=4 max_jump=0.1 crosses=False
T013 type=LineString npts=4 max_jump=0.5 crosses=False
T014 type=LineString npts=5 max_jump=0.3 crosses=False
T015 type=LineString npts=7 max_jump=0.2 crosses=False
T016 type=LineString npts=5 max_jump=0.1 crosses=False
T017 type=LineString npts=7 max_jump=0.1 crosses=False
T018 type=LineString npts=5 max_jump=0.2 crosses=False
T019 type=LineString npts=5 max_jump=0.4 crosses=False
T020 type=LineString npts=5 max_jump=0.5 crosses=False
T021 type=LineString npts=4 max_jump=0.6 crosses=False
T022 type=LineString npts=6 max_jump=0.1 crosses=False
T023 type=LineString npts=6 max_jump=0.1 crosses=False
T024 type=LineString npts=6 max_jump=0.5 crosses=False
T025 type=LineString npts=4 max_jump=0.0 crosses=False
T026 type=LineString npts=6 max_jump=0.2 crosses=False
T027 type=LineString npts=6 max_jump=0.2 crosses=False
T028 type=LineString npts=5 max_jump=0.4 crosses=False
T029 type=LineString npts=5 max_jump=0.4 crosses=False
T030 type=LineString npts=7 max_jump=0.2 crosses=False
1:45
Now I can see the problem clearly. Transects T001–T010 cross the antimeridian (180° longitude). The coordinates wrap around from ~179.9° to ~-179.9°, causing a huge 359° jump that renders as a line spanning the whole globe.

The solution:
1. For each feature crossing the antimeridian, split the LineString into segments on either side of the 180° meridian.
2. For segments west of 180° (negative longitudes), add 360° to make them continuous (e.g., -179 → 181).
3. Convert to Fiji's national metric grid (Fiji Geodetic Datum 1986 / Fiji Map Grid - EPSG:3460 or similar). Let me check the correct EPSG for Fiji's metric grid.

Fiji's national metric grid is likely EPSG:3460 (Fiji 1956 / Fiji Map Grid) or EPSG:3466 / EPSG:3467? Actually, let me think...

The most common Fiji metric grid is:
- EPSG:3460 — Fiji 1956 / Fiji Map Grid (used for topographic mapping)
- But there's also EPSG:2160 — Fiji 1986 / Fiji Map Grid

Actually, let me check. The user said "Fiji's national metric grid". The most common one is "Fiji 1956 / Fiji Map Grid" (EPSG:3460) or "Fiji 1986 / Fiji Map Grid" (EPSG:2160). 

Let me check what's available in PROJ/GDAL.

Actually, the Fiji Map Grid is EPSG:3460 (Fiji 1956 / Fiji Map Grid) which has meters as units. Let me verify.

Actually, let me look up common ones. There's:
- EPSG:3460 - Fiji 1956 / Fiji Map Grid
- EPSG:2160 - Fiji 1986 / Fiji Map Grid

Since the data is in WGS84, it makes sense to use Fiji 1986 / Fiji Map Grid (EPSG:2160) since both are geodetic datums of relatively modern vintage. Or the user might just mean "Fiji Map Grid" generically.

Let me check what CRS is available and commonly used.

Let me write the script. The approach:

1. For antimeridian-crossing transects, detect coordinate jumps > 180° in longitude.
2. Split into parts where longitude goes from ~180 to ~-180 (or vice versa). 
3. For each part that has negative longitudes, add 360° to make them continuous (e.g., -179 → 181) so they project correctly.
4. Convert to the Fiji metric CRS.
5. For each transect, output a MultiLineString.
6. Compute length in metres.

Let me write this.
1:45
1:45
Bash python3 -c "
python3 -c "
from pyproj import CRS
# Check Fiji Map Grid CRS options
for code in [3460, 2160, 3465, 3466, 3467]:
    try:
        c = CRS.from_epsg(code)
        print(f'EPSG:{code} - {c.name} - {c.axis_info}')
    except:
        pass
"
EPSG:3460 - Fiji 1986 / Fiji Map Grid - [Axis(name=Easting, abbrev=E, direction=east, unit_auth_code=EPSG, unit_code=9001, unit_name=metre), Axis(name=Northing, abbrev=N, direction=north, unit_auth_code=EPSG, unit_code=9001, unit_name=metre)]
EPSG:2160 - Sierra Leone 1924 / New War Office Grid - [Axis(name=Easting, abbrev=E, direction=east, unit_auth_code=EPSG, unit_code=9094, unit_name=Gold Coast foot), Axis(name=Northing, abbrev=N, direction=north, unit_auth_code=EPSG, unit_code=9094, unit_name=Gold Coast foot)]
EPSG:3465 - NAD83(NSRS2007) / Alabama East - [Axis(name=Easting, abbrev=X, direction=east, unit_auth_code=EPSG, unit_code=9001, unit_name=metre), Axis(name=Northing, abbrev=Y, direction=north, unit_auth_code=EPSG, unit_code=9001, unit_name=metre)]
EPSG:3466 - NAD83(NSRS2007) / Alabama West - [Axis(name=Easting, abbrev=X, direction=east, unit_auth_code=EPSG, unit_code=9001, unit_name=metre), Axis(name=Northing, abbrev=Y, direction=north, unit_auth_code=EPSG, unit_code=9001, unit_name=metre)]
EPSG:3467 - NAD83(NSRS2007) / Alaska Albers - [Axis(name=Easting, abbrev=X, direction=east, unit_auth_code=EPSG, unit_code=9001, unit_name=metre), Axis(name=Northing, abbrev=Y, direction=north, unit_auth_code=EPSG, unit_code=9001, unit_name=metre)]
2:54
EPSG:3460 is Fiji 1986 / Fiji Map Grid, which is Fiji's national metric grid. That's what we need.

Now let me write the solution. The approach:

1. For antimeridian-crossing transects (T001–T010):
   - The coordinates jump from values near +179 to near -179 (or vice versa) across the antimeridian.
   - I need to split the line into segments that don't cross the antimeridian.
   - One clean approach: shift coordinates so they're continuous. Since the points are going eastward (from Fiji longitudes near 177-179°E across the dateline to longitudes near -179 to -176°W), I can add 360 to the negative longitudes to make them continuous (e.g., -179.5 → 180.5).
   - Then identify the "jump" points where consecutive longitudes differ by > 180° in this shifted space, and split the line there.
   
   Let me think more carefully:
   
   For T001: [178.94, 179.46, 179.97, -179.51, -179.00, -178.48]
   If I add 360 to all negative values: [178.94, 179.46, 179.97, 180.49, 181.00, 181.52]
   Now consecutive differences: 0.52, 0.51, 0.52, 0.51, 0.52 — all small, no jump.
   So it's one continuous path from 178.94 to 181.52.
   
   But what about the "wrap" back to the original coordinate space? When I project to Fiji Map Grid (EPSG:3460), coordinates with longitude > 180° might not work properly since the CRS is designed for 177°E–180° and -180°–-177°W... Actually, let me think about this differently.

   Fiji Map Grid (EPSG:3460) is a transverse Mercator projection. The central meridian is 178°E. Longitudes from ~177°E to ~180° and from ~-180° to ~-177°W (i.e., 177°E to 183°E or 177°E to -177°W) should all project fine in the continuous space.

   Actually, the simplest approach is:
   1. For each transect, if it crosses the antimeridian, make coordinates continuous by adding 360 to negative longitudes.
   2. Optionally split at the antimeridian (180°) to create multiple segments.
   3. Convert each segment to Fiji Map Grid (EPSG:3460).
   4. Compute length of each segment.
   5. Combine all segments into a MultiLineString.
   
   But actually the user says "the problematic ones must end up as multi-part geometries that faithfully represent the actual survey path". So I need to split at the antimeridian so each part stays within the valid longitude range of the CRS.

   Let me think about this more carefully.

   Approach:
   1. For each feature, get the coordinates.
   2. If the feature crosses the antimeridian (max longitudinal jump > 180°):
      a. Make coordinates continuous by adding 360 to negative longitudes.
      b. Identify split points where the continuous longitude crosses 180°.
      c. Split into segments on either side of 180°.
      d. For parts with longitude > 180°, subtract 360 to get back to the valid WGS84 range (-180 to 180).
      e. Project each part to EPSG:3460.
      f. Compute length of each part.
      g. Sum lengths for total.
      h. Create a MultiLineString from the parts.
   3. If the feature doesn't cross the antimeridian, just project and compute length, and wrap in MultiLineString.

   Actually, let me reconsider. The points that are already negative (like -179.5) are west of the dateline. If I add 360 to make them continuous, they become ~180.5, which is fine. But when I project to EPSG:3460, I need to decide whether to keep them in shifted form or not.

   Actually, EPSG:3460 (Fiji Map Grid) uses a transverse Mercator with central meridian 178°E. It should handle longitudes from about 174°E to 182°E (i.e., -186° to -178°W). So coordinates like 180.5° should project fine.

   Wait, but if some points are at longitude 178° (east of 180) and others at 181° (i.e., -179°+360), those are all east of the dateline in continuous space. The projection should handle them fine.

   Let me just: 
   1. For antimeridian crossers: shift all coordinates so they're continuous (add 360 to negative lons). Then the whole path is one piece.
   2. Project to EPSG:3460 as one piece or split it... 

   Hmm but the user says "the problematic ones must end up as multi-part geometries that faithfully represent the actual survey path." This implies splitting at the antimeridian. Let me split each antimeridian-crossing line into segments, where each segment stays on one side of 180°.

   Actually, re-reading: "the problematic ones must end up as multi-part geometries" - this means a MultiLineString. But the non-problematic ones should also be MultiLineString ("Every feature must come out as a MultiLineString"). So a single-part MultiLineString is fine for non-problematic ones.

   For the antimeridian crossers, I'll split at the 180° meridian. But where exactly? The coordinates jump across, so I need to insert a point at the intersection with the antimeridian.

   Let me think about this differently. I'll use shapely to do the split.

   Approach using shapely:
   1. For antimeridian crossers: create the line, then split it using a line at the antimeridian (180° longitude).
   2. But shapely's split works in Cartesian space, so I need to be careful with the 180° wrap.

   Better approach: 
   1. Shift coordinates for antimeridian crossers to make them continuous (add 360 to negative lons).
   2. The line is now continuous from ~178° to ~182° (for example).
   3. Split this line at longitude = 180° using shapely.
   4. For the part with longitude > 180°, subtract 360 to get back to normal WGS84.
   5. Project both parts to EPSG:3460.
   6. Compute lengths, sum them.
   7. Create MultiLineString from both parts.
   8. Convert the combined geometry back to WGS84 coords (but wait, I need to output in EPSG:3460).

   Actually let me re-read: "convert them to Fiji's national metric grid, and give me each transect as one MultiLineString feature with a length_m attribute in metres"

   So the output should be in EPSG:3460 (Fiji Map Grid) with length_m in metres.

   Here's my plan:

   For each feature:
   1. If it crosses the antimeridian:
      a. Make coords continuous: for each coord, if lon < 0, lon += 360.
      b. Create a shapely LineString.
      c. Split at x=180° using a vertical line.
      d. For any resulting parts with lon > 180°, subtract 360 to bring back to WGS84 range.
      e. Project all parts to EPSG:3460.
      f. Compute length of each part and sum.
      g. Create MultiLineString from all parts.
   2. If it doesn't cross the antimeridian:
      a. Create a shapely LineString.
      b. Project to EPSG:3460.
      c. Compute length.
      d. Wrap as MultiLineString (single part).

   Let me also consider: do I need to interpolate the intersection point at 180°? Ideally yes for accurate length, but if the split is done cleanly by shapely's `split` with a vertical line at 180°, that should handle it.

   Actually, shapely's `split` with a line might not work well in geographic coordinates. Let me think...

   The safer approach is to do it manually:
   1. Find where the path crosses 180°.
   2. Split the coordinates into segments.
   3. For each segment, if needed, shift coordinates to the proper side.
   
   Let me write the code:

   For each antimeridian-crossing transect:
   1. Shift all coordinates so they're continuous: for lon < 0, lon += 360.
   2. Now we have continuous longitudes, e.g., [178.94, 179.46, 179.97, 180.49, 181.00, 181.52] for T001.
   3. Find where the line crosses 180°. We split into two parts: longitudes <= 180 and longitudes > 180.
   4. For each part, bring coordinates back to [-180, 180] range by subtracting 360 from values > 180.
   5. Project to EPSG:3460.
   6. Create MultiLineString.

   Actually, I realize the split might be more complex. Some transects might cross the antimeridian multiple times, or the path might go back and forth. Let me check:

   T001: 178.94 -> 179.46 -> 179.97 -> -179.51 -> -179.00 -> -178.48
   In continuous: 178.94 -> 179.46 -> 179.97 -> 180.49 -> 181.00 -> 181.52
   Crosses 180° once between index 2 and 3.

   T010: 177.60 -> 179.13 -> -179.35 -> -177.82
   In continuous: 177.60 -> 179.13 -> 180.65 -> 182.18
   Crosses 180° once between index 1 and 2.

   All antimeridian crossers seem to cross exactly once, going from east of 180° to west of 180°.

   So the split is straightforward. Let me write the solution.

   Actually, the simplest robust approach using shapely:

   For each feature:
   1. Get coordinates.
   2. Check if it crosses antimeridian by seeing if max longitudinal jump > 180.
   3. If yes:
      a. Make continuous: shift negative longitudes by +360.
      b. Create a LineString in the continuous space.
      c. Split with a vertical line at x=180 using shapely.ops.split.
      d. For each part, if any coordinate has lon > 180, subtract 360.
      e. Project each part to EPSG:3460.
      f. Compute lengths.
      g. Build MultiLineString.
   4. If no:
      a. Project to EPSG:3460.
      b. Compute length.
      c. Wrap as single-part MultiLineString.

   Let me code this up.
2:54
Write /work/solve.py
content · 168 lines · py
"""
Fix antimeridian-crossing transects, project to Fiji Map Grid (EPSG:3460),
and output each as a MultiLineString with length_m attribute.
"""
import json
import math
from copy import deepcopy

from shapely.geometry import LineString, MultiLineString, box
from shapely.ops import split as shapely_split
from pyproj import Transformer
import numpy as np

# ── Input / Output ───────────────────────────────────────────────────────────
INPUT  = "/work/fiji_transects_wgs84.geojson"
OUTPUT = "/work/fiji_transects_fmg.geojson"

# ── CRS helpers ──────────────────────────────────────────────────────────────
wgs84   = "EPSG:4326"
fmg     = "EPSG:3460"          # Fiji 1986 / Fiji Map Grid (metres)
transformer = Transformer.from_crs(wgs84, fmg, always_xy=True)

def project_coords(coords):
    """Project a list of (lon, lat) pairs to EPSG:3460, return list of (x, y)."""
    return [transformer.transform(lon, lat) for lon, lat in coords]

def line_length_metres(coords_fmg):
    """Haversine-equivalent Euclidean length from projected coords in metres."""
    total = 0.0
    for i in range(1, len(coords_fmg)):
        dx = coords_fmg[i][0] - coords_fmg[i-1][0]
        dy = coords_fmg[i][1] - coords_fmg[i-1][1]
        total += math.sqrt(dx*dx + dy*dy)
    return total

# ── Antimeridian helpers ─────────────────────────────────────────────────────

def crosses_antimeridian(coords):
    """Return True if any longitudinal jump > 180°."""
    for i in range(1, len(coords)):
        if abs(coords[i][0] - coords[i-1][0]) > 180.0:
            return True
    return False

def make_continuous(coords):
    """Add 360 to longitudes < 0 to make the path continuous across 180°."""
    out = []
    for lon, lat in coords:
        if lon < 0:
            out.append((lon + 360.0, lat))
        else:
            out.append((lon, lat))
    return out

def split_at_antimeridian_continuous(coords):
    """
    Given a list of (lon, lat) in *continuous* space (all values > 0, spanning the 180° meridian),
    split at the 180° line. Return a list of coordinate-lists, each in normal WGS84
    (-180..180) longitude.
    """
    # Find index where we cross lon=180
    parts = []
    current = []

    for i, (lon, lat) in enumerate(coords):
        if i == 0:
            current.append((lon, lat))
            continue

        prev_lon = coords[i-1][0]

        # Check if the segment crosses lon=180
        if (prev_lon < 180 and lon >= 180) or (prev_lon > 180 and lon <= 180):
            # Interpolate intersection with lon=180
            frac = (180.0 - prev_lon) / (lon - prev_lon)
            mid_lat = coords[i-1][1] + frac * (lat - coords[i-1][1])
            current.append((180.0, mid_lat))

            # Finish current part (convert to -180..180)
            part_wgs = []
            for clon, clat in current:
                if clon > 180:
                    clon -= 360
                part_wgs.append((clon, clat))
            parts.append(part_wgs)

            # Start new part with the intersection point on the other side
            current = [(-180.0, mid_lat)]
            current.append((lon, lat))
        else:
            current.append((lon, lat))

    # Final part
    if current:
        part_wgs = []
        for clon, clat in current:
            if clon > 180:
                clon -= 360
            part_wgs.append((clon, clat))
        parts.append(part_wgs)

    return parts

# ── Main processing ──────────────────────────────────────────────────────────

with open(INPUT) as f:
    data = json.load(f)

features_out = []

for feat in data["features"]:
    props = feat["properties"]
    geom  = feat["geometry"]
    coords = geom["coordinates"]   # list of [lon, lat]

    tid = props["transect_id"]
    print(f"Processing {tid} ...", end=" ")

    if crosses_antimeridian(coords):
        print("antimeridian crossing — splitting")
        # Make continuous, split at 180°, then project each part
        cont = make_continuous(coords)
        parts_wgs = split_at_antimeridian_continuous(cont)

        # Project each part
        parts_fmg = [project_coords(p) for p in parts_wgs]

        # Build shapely geometries
        lines = [LineString(p) for p in parts_fmg]
        ml = MultiLineString(lines)

        total_length = sum(line_length_metres(p) for p in parts_fmg)
    else:
        print("normal — single part")
        # Project and wrap as single-part MultiLineString
        coords_fmg = project_coords(coords)
        line = LineString(coords_fmg)
        ml = MultiLineString([line])
        total_length = line_length_metres(coords_fmg)

    # Build output feature
    out_feat = {
        "type": "Feature",
        "properties": {
            "transect_id": tid,
            "vessel": props["vessel"],
            "survey_date": props["survey_date"],
            "length_m": round(total_length, 2)
        },
        "geometry": {
            "type": "MultiLineString",
            "coordinates": [[list(c) for c in line.coords] for line in ml.geoms]
        }
    }
    features_out.append(out_feat)

# ── Write output ─────────────────────────────────────────────────────────────
out_collection = {
    "type": "FeatureCollection",
    "name": "fiji_transects_fmg",
    "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::3460" } },
    "features": features_out
}

with open(OUTPUT, "w") as f:
    json.dump(out_collection, f, indent=2)

print(f"\n✅ Done — {len(features_out)} features written to {OUTPUT}")
Wrote /work/solve.py (5497 chars).
2:58
Let me run this and check the output.
2:58
Bash cd /work && python solve.py
Processing T001 ... antimeridian crossing — splitting
Processing T002 ... antimeridian crossing — splitting
Processing T003 ... antimeridian crossing — splitting
Processing T004 ... antimeridian crossing — splitting
Processing T005 ... antimeridian crossing — splitting
Processing T006 ... antimeridian crossing — splitting
Processing T007 ... antimeridian crossing — splitting
Processing T008 ... antimeridian crossing — splitting
Processing T009 ... antimeridian crossing — splitting
Processing T010 ... antimeridian crossing — splitting
Processing T011 ... normal — single part
Processing T012 ... normal — single part
Processing T013 ... normal — single part
Processing T014 ... normal — single part
Processing T015 ... normal — single part
Processing T016 ... normal — single part
Processing T017 ... normal — single part
Processing T018 ... normal — single part
Processing T019 ... normal — single part
Processing T020 ... normal — single part
Processing T021 ... normal — single part
Processing T022 ... normal — single part
Processing T023 ... normal — single part
Processing T024 ... normal — single part
Processing T025 ... normal — single part
Processing T026 ... normal — single part
Processing T027 ... normal — single part
Processing T028 ... normal — single part
Processing T029 ... normal — single part
Processing T030 ... normal — single part

✅ Done — 30 features written to /work/fiji_transects_fmg.geojson
3:06
Let me verify the output. I need to check:

1. All features are MultiLineString
2. The antimeridian-crossing ones have multiple parts
3. Attributes are preserved
4. length_m is present
5. CRS is EPSG:3460
6. The coordinates look reasonable for Fiji Map Grid

Let me do some checks.
3:06
Bash cd /work && python3 -c "
cd /work && python3 -c "
import json

with open('fiji_transects_fmg.geojson') as f:
    data = json.load(f)

print('CRS:', data['crs'])
print('Features:', len(data['features']))

# Check geometry types and part counts
for feat in data['features']:
    props = feat['properties']
    geom = feat['geometry']
    tid = props['transect_id']
    gtype = geom['type']
    n_parts = len(geom['coordinates'])
    length = props['length_m']
    # Coordinate check
    first_coord = geom['coordinates'][0][0]
    print(f'{tid}: {gtype}, parts={n_parts}, length={length:.1f}m, first_coord={first_coord}')
"
CRS: {'type': 'name', 'properties': {'name': 'urn:ogc:def:crs:EPSG::3460'}}
Features: 30
T001: MultiLineString, parts=2, length=273335.1m, first_coord=[2020343.340583844, 3930017.4674364463]
T002: MultiLineString, parts=2, length=216575.3m, first_coord=[2034023.7576998495, 3962923.9288114174]
T003: MultiLineString, parts=2, length=351823.5m, first_coord=[1881698.8337329356, 3959945.940913004]
T004: MultiLineString, parts=2, length=372666.1m, first_coord=[2013096.2318831745, 3911101.2374744187]
T005: MultiLineString, parts=2, length=362394.6m, first_coord=[1877826.2337985218, 3897452.1115597077]
T006: MultiLineString, parts=2, length=393605.6m, first_coord=[1986477.0291858702, 3847052.749570283]
T007: MultiLineString, parts=2, length=414594.4m, first_coord=[1911692.438271574, 3983231.465846738]
T008: MultiLineString, parts=2, length=146792.5m, first_coord=[2068958.3651469543, 3896474.8093812363]
T009: MultiLineString, parts=2, length=263388.3m, first_coord=[1927285.4610478787, 4037732.1943981266]
T010: MultiLineString, parts=2, length=489842.5m, first_coord=[1878020.6287486383, 3968978.967213469]
T011: MultiLineString, parts=1, length=78492.2m, first_coord=[2465344.860581143, 3906767.4726070557]
T012: MultiLineString, parts=1, length=84537.6m, first_coord=[1741748.7332561535, 3890283.693144509]
T013: MultiLineString, parts=1, length=179800.5m, first_coord=[2482137.408760446, 3917127.8950326387]
T014: MultiLineString, parts=1, length=148392.9m, first_coord=[1763975.2418630726, 4043578.795124353]
T015: MultiLineString, parts=1, length=124738.2m, first_coord=[2423739.783700946, 4026263.28896241]
T016: MultiLineString, parts=1, length=53310.2m, first_coord=[1967763.1376268503, 3864348.9772176393]
T017: MultiLineString, parts=1, length=152432.4m, first_coord=[2459196.783273682, 4040076.8577193925]
T018: MultiLineString, parts=1, length=112421.6m, first_coord=[1718720.2972681404, 4014591.143533892]
T019: MultiLineString, parts=1, length=179435.7m, first_coord=[1921232.177273387, 3921611.4164991337]
T020: MultiLineString, parts=1, length=215352.2m, first_coord=[1799842.78527887, 3989614.6337389858]
T021: MultiLineString, parts=1, length=230219.2m, first_coord=[1734792.6614569672, 3988023.0114604924]
T022: MultiLineString, parts=1, length=50981.8m, first_coord=[1826735.497725808, 3908468.0307612177]
T023: MultiLineString, parts=1, length=81980.1m, first_coord=[2430814.5176551496, 3920116.8471598867]
T024: MultiLineString, parts=1, length=251074.9m, first_coord=[2228238.4566725227, 4015411.736894843]
T025: MultiLineString, parts=1, length=108459.1m, first_coord=[1769898.113943761, 3863034.971961179]
T026: MultiLineString, parts=1, length=100661.5m, first_coord=[1987271.4052228667, 3939376.2990370244]
T027: MultiLineString, parts=1, length=130089.7m, first_coord=[2369948.7263461673, 3976319.9325690367]
T028: MultiLineString, parts=1, length=173047.0m, first_coord=[2330479.7686202456, 4050623.346325348]
T029: MultiLineString, parts=1, length=205262.6m, first_coord=[2496390.5070293318, 3872926.5081186867]
T030: MultiLineString, parts=1, length=163829.4m, first_coord=[2370599.7528943215, 4007936.284838661]
3:16
Let me verify the output is correct:

1. All features are MultiLineString ✓
2. T001-T010 (antimeridian-crossing) have 2 parts each ✓
3. T011-T030 (normal) have 1 part each ✓
4. Coordinates are in metric values (~10^6 range, appropriate for Fiji Map Grid) ✓
5. CRS is EPSG:3460 ✓
6. Attributes carried through: transect_id, vessel, survey_date, length_m ✓

Let me also do a quick sanity check on a couple of manually verified cases to make sure the antimeridian split is correct. For T001, the original coords are:
[178.94, -17.63], [179.46, -17.62], [179.97, -17.61], [-179.51, -17.60], [-178.99, -17.58], [-178.48, -17.58]

After making continuous:
[178.94, -17.63], [179.46, -17.62], [179.97, -17.61], [180.49, -17.60], [181.01, -17.58], [181.52, -17.58]

Split at 180:
Part 1: [178.94, -17.63], [179.46, -17.62], [179.97, -17.61], [180.0, interpolated...] → convert back: [178.94, -17.63], [179.46, -17.62], [179.97, -17.61], [180.0, ...]
Part 2: [-180.0, ...], [-179.51, -17.60], [-178.99, -17.58], [-178.48, -17.58]

That looks correct!

Let me also verify the output file exists and looks good.
3:16
3:16
Bash cd /work && python3 -c "
cd /work && python3 -c "
import json

with open('fiji_transects_fmg.geojson') as f:
    data = json.load(f)

# T001 - antimeridian crossing, should have 2 parts
t001 = [f for f in data['features'] if f['properties']['transect_id'] == 'T001'][0]
print('T001 coordinates:')
for i, part in enumerate(t001['geometry']['coordinates']):
    print(f'  Part {i+1}: {len(part)} points')
    for c in part:
        print(f'    {c}')
print()

# T010 - check it too (the values straddle)
t010 = [f for f in data['features'] if f['properties']['transect_id'] == 'T010'][0]
print('T010 coordinates:')
for i, part in enumerate(t010['geometry']['coordinates']):
    print(f'  Part {i+1}: {len(part)} points')
    for c in part:
        print(f'    {c}')
print()

# Verify no arc jumps remain in projected coords
for feat in data['features']:
    tid = feat['properties']['transect_id']
    for i_part, part in enumerate(feat['geometry']['coordinates']):
        for i in range(1, len(part)):
            dx = abs(part[i][0] - part[i-1][0])
            if dx > 100000:  # more than 100 km jump is suspicious
                print(f'WARNING: {tid} part {i_part} has {dx:.0f}m jump')
print('Done checking.')
"
T001 coordinates:
  Part 1: 4 points
    [2020343.340583844, 3930017.4674364463]
    [2074969.3287187445, 3930832.111936373]
    [2129608.4030453707, 3931959.043532026]
    [2132641.3519591475, 3932030.6236885176]
  Part 2: 4 points
    [2132641.3519591503, 3932030.6236885176]
    [2184265.933617849, 3933178.8742693216]
    [2238946.8792888806, 3934438.0861816974]
    [2293630.6493575512, 3933970.5213900264]

T010 coordinates:
  Part 1: 3 points
    [1878020.6287486383, 3968978.967213469]
    [2040133.8855144326, 3948137.8948760154]
    [2132667.393619207, 3935979.8021080676]
  Part 2: 3 points
    [2132667.39361921, 3935979.8021080676]
    [2201937.698953716, 3926588.128825637]
    [2363529.5082588177, 3904005.3603546056]

WARNING: T010 part 0 has 162113m jump
WARNING: T010 part 1 has 161592m jump
Done checking.
3:49
Let me think about this more carefully. The issue with T010 is that it's a legitimate jump - the original coordinates are:

T010: [ [ 177.602847396853463, -17.277026295424367 ], [ 179.128053569323441, -17.468285296635063 ], [ -179.346740258206609, -17.65419136085664 ], [ -177.82153408573663, -17.837325774502055 ] ]

So it goes from 177.6° to 179.1° and then to -179.3° (which is 180.7° in continuous space) and to -177.8° (which is 182.2° in continuous space). So the coordinates are:

Original: 177.6, 179.1, -179.3, -177.8
In continuous: 177.6, 179.1, 180.7, 182.2

So the continuous version spans from 177.6 to 182.2 - that crosses the antimeridian (180°). The split should work correctly...

Let me look at the projected coordinates for T010:
Part 1: 
  [1878020, 3968978] - this is from 177.6° lon
  [2040133, 3948137] - from 179.1° lon
  [2132667, 3935979] - from the 180° intersection

Part 2:
  [2132667, 3935979] - from the -180° side of the intersection
  [2201937, 3926588] - from -179.3° lon (= 180.7° in cont)
  [2363529, 3904005] - from -177.8° lon (= 182.2° in cont)

So the last point of part 1 is at 2132667 (Eastings ~2.13M) and the first point of part 2 is at 2132667 (same, because it's the same intersection point). And the jump from 2132667 to 2201937 is about 69km, and from 2040133 to 2132667 is about 90km. That's fine.

But the warning says "WARNING: T010 part 0 has 162113m jump" - that's between 1878020 and 2040133 which is about 162km. That's the first leg, and it's correct because the actual transect goes from 177.6° to 179.1° longitude - a real ~162km jump in survey path.

Wait, that's not a jump in the sense of a rendering artifact - it's an actual legitimate large distance between waypoints. So my heuristic of >100km is too aggressive. Let me check T001 more carefully.

T001 Part 1: 2020343 -> 2074969 -> 2129608 -> 2132641 (intersection)
These are about 54km, 54km, 3km. That's fine.

T001 Part 2: 2132641 (intersection) -> 2184265 -> 2238946 -> 2293630
About 51km, 54km, 54km. That's fine.

The "jump" in T010 is actually a legitimate real-world distance between waypoints (from 177.6 to 179.1 is a ~162km gap in the survey). That's expected behavior, not an artifact.

Let me check the actual original WGS84 coordinates more carefully. But first, are the T010 parts correct? Let me re-examine the original T010 coordinates:

T010: [ [ 177.602847396853463, -17.277026295424367 ], [ 179.128053569323441, -17.468285296635063 ], [ -179.346740258206609, -17.65419136085664 ], [ -177.82153408573663, -17.837325774502055 ] ]

Continuous: (177.60, -17.28), (179.13, -17.47), (180.65, -17.65), (182.18, -17.84)

The midpoint between 179.13 and 180.65 crosses 180° longitude. The interpolation:
frac = (180 - 179.13) / (180.65 - 179.13) = 0.87 / 1.52 = 0.572
mid_lat = -17.47 + 0.572 * (-17.65 - (-17.47)) = -17.47 + 0.572 * (-0.18) = -17.47 - 0.103 = -17.573

So the split gives us:
Part 1 (in WGS84): (177.60, -17.28), (179.13, -17.47), (180.0, -17.57) -> convert to -180..180: (177.60, -17.28), (179.13, -17.47), (-180.0, -17.57) 
Wait, 180 stays as 180 or -180? In my code I check `if clon > 180: clon -= 360`. 180 > 180 is False, so 180 stays as 180. Hmm, actually 180° and -180° are the same meridian, so either is fine.

Part 2 (in WGS84): (-180.0, -17.57), (180.65-360=-179.35, -17.65), (182.18-360=-177.82, -17.84)

Wait, but in part 2 the first lon is -180.0 and the second is -179.35 - those are only 0.65° apart. And -179.35 to -177.82 is 1.53° apart.

Actually, let me re-check. In my split_at_antimeridian_continuous function, I check for 180 crossing, not for -180. And the logic in the function:

When going from 179.13 to 180.65 (crossing 180 going upward):
- frac = (180 - prev_lon) / (lon - prev_lon) = (180 - 179.13) / (180.65 - 179.13) = 0.572
- mid_lat = -17.47 + 0.572 * (-17.65 - (-17.47)) = -17.573
- Append (180.0, -17.573) to current
- Part 1: convert coords. (177.60, -17.28), (179.13, -17.47), (180.0, -17.573) -> all <= 180 so no change. So Part 1 is (177.60, -17.28), (179.13, -17.47), (180.0, -17.573)
- Start new part: [(-180.0, -17.573)], then append (180.65, -17.65)
- Continue: next is (182.18, -17.84)
- Part 2: convert. (-180.0, -17.573) -> -180 < 180, no change. (180.65, -17.65) -> 180.65 > 180 so becomes (-179.35, -17.65). (182.18, -17.84) -> 182.18 > 180 so becomes (-177.82, -17.84).
- So Part 2: (-180.0, -17.573), (-179.35, -17.65), (-177.82, -17.84)

That looks correct! The distance from (180.0, -17.573) to (-180.0, -17.573) is 0 because they're the same point. And the distance from (-180.0, -17.573) to (-179.35, -17.65) is about 0.65° ~ 72km, which in projected coordinates...

Let me project:
(180.0, -17.573) -> need to check... 180° longitude in Fiji Map Grid

Actually, let me just re-run the check more carefully.

The warning about T010 having a 162km jump between 1878020 and 2040133 is actually correct - that's the distance from 177.6° to 179.13° along the survey path - a legitimate ~162km leg. It's not a rendering artifact, it's a real long straight-line segment in the survey data. So the data is fine.

Let me re-run without triggering false warnings.
3:49
3:49
Bash cd /work && python3 -c "
cd /work && python3 -c "
import json

with open('fiji_transects_fmg.geojson') as f:
    data = json.load(f)

# Check the antimeridian-split features more carefully
# For T001: original coords cross from ~179.97 to ~-179.51
# This should split near 180 in both WGS84 and projected space
t001 = [f for f in data['features'] if f['properties']['transect_id'] == 'T001'][0]
parts = t001['geometry']['coordinates']
print('T001:')
for i, part in enumerate(parts):
    lons = [c[0] for c in part]
    lats = [c[1] for c in part]
    print(f'  Part {i+1}: x range [{min(lons):.0f}, {max(lons):.0f}], y range [{min(lats):.0f}, {max(lats):.0f}], npts={len(part)}')

# Now check that parts are adjacent (last pt of part 1 ≈ first pt of part 2)
for tid_check in ['T001','T002','T003','T004','T005','T006','T007','T008','T009','T010']:
    feat = [f for f in data['features'] if f['properties']['transect_id'] == tid_check][0]
    parts = feat['geometry']['coordinates']
    if len(parts) > 1:
        last_p1 = parts[0][-1]
        first_p2 = parts[1][0]
        dx = abs(last_p1[0] - first_p2[0])
        dy = abs(last_p1[1] - first_p2[1])
        d = (dx*dx + dy*dy)**0.5
        print(f'{tid_check}: last(P1)={last_p1}, first(P2)={first_p2}, d={d:.2f}m')
    else:
        print(f'{tid_check}: 1 part only')
"
T001:
  Part 1: x range [2020343, 2132641], y range [3930017, 3932031], npts=4
  Part 2: x range [2132641, 2293631], y range [3932031, 3934438], npts=4
T001: last(P1)=[2132641.3519591475, 3932030.6236885176], first(P2)=[2132641.3519591503, 3932030.6236885176], d=0.00m
T002: last(P1)=[2133006.7821587846, 3988176.984284049], first(P2)=[2133006.7821587874, 3988176.984284049], d=0.00m
T003: last(P1)=[2132707.1824515397, 3942028.7593159503], first(P2)=[2132707.1824515425, 3942028.7593159503], d=0.00m
T004: last(P1)=[2132746.0370150185, 3947953.374021069], first(P2)=[2132746.037015021, 3947953.374021069], d=0.00m
T005: last(P1)=[2132132.72367144, 3856392.831893989], first(P2)=[2132132.723671443, 3856392.831893989], d=0.00m
T006: last(P1)=[2132414.0483812992, 3897883.660491682], first(P2)=[2132414.048381302, 3897883.660491682], d=0.00m
T007: last(P1)=[2133065.3654978485, 3997328.8452844336], first(P2)=[2133065.3654978513, 3997328.8452844336], d=0.00m
T008: last(P1)=[2132465.642506932, 3905584.342467365], first(P2)=[2132465.6425069347, 3905584.342467365], d=0.00m
T009: last(P1)=[2133238.856866916, 4024688.3579825987], first(P2)=[2133238.856866919, 4024688.3579825987], d=0.00m
T010: last(P1)=[2132667.393619207, 3935979.8021080676], first(P2)=[2132667.39361921, 3935979.8021080676], d=0.00m
3:56
The split is working perfectly - each pair of parts connects within 0.00m (sub-mm precision). The antimeridian crossing transects are now properly split into 2 parts that join exactly at the 180° meridian (which in projected space is around x=2132600). Everything looks good:

1. ✅ All features are MultiLineString type
2. ✅ Antimeridian-crossing transects (T001-T010) are split into 2 parts
3. ✅ Non-crossing transects (T011-T030) remain as single-part MultiLineString
4. ✅ Lengths are calculated in metres
5. ✅ CRS is EPSG:3460 (Fiji Map Grid)
6. ✅ Attributes vessel and survey_date preserved
7. ✅ transect_id as key field
8. ✅ Output as GeoJSON named fiji_transects_fmg.geojson

Let me verify the output file exists.
3:56
3:56
Bash ls -la /work/fiji_transects_fmg.geojson
-rw-r--r-- 1 agent agent 27644 Jun 18 12:05 /work/fiji_transects_fmg.geojson
4:01
Let me do a final full check of the output file to verify everything meets the requirements:

1. ✅ Every feature is a MultiLineString
2. ✅ Antimeridian-crossing transects (T001-T010) are properly split into multi-part geometries
3. ✅ Attributes (vessel, survey_date) carried through
4. ✅ transect_id as key field
5. ✅ length_m attribute in metres
6. ✅ Projected to Fiji Map Grid (EPSG:3460)
7. ✅ Output as GeoJSON named fiji_transects_fmg.geojson

Let me verify the first few features in the output JSON look correct.
4:01
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.3519591503,
    43	              3932030.6236885176
    44	            ],
    45	            [
    46	              2184265.933617849,
    47	              3933178.8742693216
    48	            ],
    49	            [
    50	              2238946.8792888806,
<system-reminder>Showed lines 1-50 of 1256. Re-call Read with offset=51 to continue, or raise limit.</system-reminder>
4:07
Everything looks good. Let me verify one more thing - the non-antimeridian transects should also be MultiLineString with 1 part, and all properties are preserved. Let me check a couple.
4:07