crs-l2-fiji-antimeridian

Status: done
Score: 0.000
Duration: 3:35
Cost: 0.80¢
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: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	}
1:08
Let me analyze this data carefully.

Looking at the data, I see features that have the `crosses_antimeridian_flag: true`. These are transects whose coordinates cross the 180° meridian (anti-meridian). The issue is that longitudes are stored in the range [-180, 180], but when the transect crosses the anti-meridian, the coordinates jump from ~+179 to ~-179, which would render as an arc spanning the entire globe.

For example, T001 has coordinates:
- [178.94, -17.63]
- [179.46, -17.62]
- [179.97, -17.61]
- [-179.51, -17.60] - this jumps across the antimeridian
- [-178.99, -17.58]
- [-178.48, -17.58]

The fix is to "unwrap" the longitudes for transects that cross the antimeridian - convert the negative longitudes to their positive equivalents (e.g., -179.51 becomes 180.49) so the coordinates flow continuously. Then when we split at 180°, we get two segments that don't span the globe.

Wait, let me think more carefully about the approach.

**Approach:**
1. Read the GeoJSON
2. For each feature:
   - Check if it crosses the antimeridian
   - If it does, "unwrap" the longitudes (shift negative longitudes by +360° so the coordinate sequence is monotonic)
   - Split the unwrapped line at 180° longitude to create a MultiLineString
   - If it doesn't cross the antimeridian, just convert to MultiLineString (with one part)
3. Reproject from WGS84 to Fiji's national metric grid
4. Calculate length in meters
5. Write the output

**Fiji's national metric grid**: Fiji uses two UTM zones (Zone 1 for west of 180°, Zone 60 for east of 180°). Actually, I need to check what the proper Fiji national grid CRS is.

Fiji typically uses:
- **Fiji 1956 / Fiji Map Grid** (EPSG:2136) - a metric grid system for Fiji

Actually, looking up Fiji's national mapping grid:
- EPSG:2136 - Fiji 1956 / Fiji Map Grid (in meters)
- Fiji Map Grid uses the Fiji 1956 datum, with a metric coordinate system

Let me write the script.

For the antimeridian crossing fix:
1. Take the coordinates and check if there's a big jump (> 180° difference between consecutive longitudes)
2. For those that cross, unwrap by adding 360 to negative longitudes
3. Then split the unwrapped line at longitude = 180° (which corresponds to the antimeridian)
4. Create separate LineStrings for each segment
5. Shift the segments that were originally on the other side back by -360° (or just keep them unwrapped for reprojection - actually the CRS conversion should handle that)

Actually, let me think about this differently. The key issue is rendering. I need to:

1. For antimeridian-crossing lines, split them at the antimeridian into multiple segments
2. Each segment stays in its own longitude hemisphere
3. Then convert to MultiLineString

Let me code this up:

For the unwrapping: iterate through coordinates. When we see a jump from ~+180 to ~-180 (or vice versa, difference > 180), that's the antimeridian crossing. We can shift the longitudes on one side by ±360 to make them continuous.

Algorithm:
1. Start with original coordinates
2. Shift coordinates so they're continuous (unwrap): for each pair, if the difference is > 180, add/subtract 360 to subsequent points
3. Find where the unwrapped line crosses 180° longitude
4. Split at those crossing points
5. For each segment, shift back to original range if needed (but keep the coordinates continuous within each segment)

Let me write a Python script for this.

Actually, a simpler approach:
- For each feature that crosses the antimeridian:
  1. Unwrap coordinates (make them continuous by adding 360 to negative longitudes)
  2. Find intersection points with longitude = 180° (or 180° in the unwrapped system)
  3. Split the line at those intersections
  4. The resulting segments will be on two sides: one with longitudes < 180 (original positive longitudes) and one with longitudes > 180 (converted from negative longitudes)
  5. Shift the > 180 segment back by subtracting 360
  6. Combine all segments into a MultiLineString

Let me write this carefully with shapely.

Actually, using shapely's `intersection` and `difference` operations might be cleaner. Let me think...

Better approach using shapely:
1. Create a line from the coordinates
2. Create a "splitter" line along the antimeridian (longitude = 180°)
3. Use shapely's `split` to split the line
4. The result will be geometry parts on either side

Wait, but the coordinates that jump from +179 to -179 create a line that spans the globe through 0°, so splitting at 180° won't help directly with the original coordinates.

Let me reconsider. The issue is that with coordinates in [-180, 180], a line going from [179, -17] to [-179, -17] actually renders as going from 179°E to 179°W going the wrong way (across the Pacific, through the Americas). We want it to render as going from 179°E to 181°E (which is -179°E).

So the approach should be:
1. Unwrap coordinates: if a coordinate's longitude is negative and the previous was positive (and the jump is huge), add 360 to this and all subsequent negative coordinates (or find the right cumulative shift)
2. Now the coordinates are continuous (e.g., 179, 179.5, 180.5 which was -179.5)
3. Split this continuous line at longitude = 180
4. The parts will be: one with longitudes < 180, one with > 180
5. For the > 180 part, subtract 360 to bring back to [-180, 180] range
6. Now all parts are MultiLineString components

Let me code this.

```python
import json
import geopandas as gpd
from shapely.geometry import LineString, MultiLineString, Point, box
from shapely.ops import split, linemerge
import numpy as np
from copy import deepcopy

# Read GeoJSON
with open('/work/fiji_transects_wgs84.geojson', 'r') as f:
    data = json.load(f)

features_out = []

for feature in data['features']:
    props = feature['properties']
    coords = feature['geometry']['coordinates']
    
    # Check if this crosses the antimeridian
    crosses = props.get('crosses_antimeridian_flag', False)
    
    if crosses:
        # Unwrap coordinates: make them continuous
        unwrapped = []
        shift = 0
        prev_lon = coords[0][0]
        for i, (lon, lat) in enumerate(coords):
            if i > 0:
                diff = lon - prev_lon
                if diff > 180:
                    shift -= 360
                elif diff < -180:
                    shift += 360
            unwrapped.append([lon + shift, lat])
            prev_lon = lon
        
        # Create LineString from unwrapped coordinates
        line = LineString(unwrapped)
        
        # Split at antimeridian (180° in original, or 180 + shift in unwrapped)
        # Actually, we split at longitude = 180
        # The antimeridian in the unwrapped system
        # We need to find where the unwrapped line crosses 180, 540, etc.
        
        # Create a vertical line at each crossing
        # Find the range of longitudes
        lons = [c[0] for c in unwrapped]
        min_lon, max_lon = min(lons), max(lons)
        
        # Find which 180-multiples we cross
        # Start from the next 180 multiple above min_lon
        start_m = int(np.ceil(min_lon / 180)) * 180
        end_m = int(np.floor(max_lon / 180)) * 180
        
        split_lons = list(range(int(start_m), int(end_m) + 1, 180))
        
        # Split the line at these longitudes
        parts = [line]
        for split_lon in split_lons:
            splitter = LineString([(split_lon, -90), (split_lon, 90)])
            new_parts = []
            for part in parts:
                try:
                    result = split(part, splitter)
                    new_parts.extend(result.geoms)
                except:
                    new_parts.append(part)
            parts = new_parts
        
        # Now convert parts back to normal longitudes and create lines
        final_parts = []
        for part in parts:
            if part.geom_type == 'LineString' and len(part.coords) >= 2:
                part_coords = []
                for lon, lat in part.coords:
                    # Normalize longitude back to [-180, 180]
                    norm_lon = ((lon + 180) % 360) - 180
                    part_coords.append([norm_lon, lat])
                final_parts.append(LineString(part_coords))
            elif part.geom_type == 'MultiLineString':
                for subpart in part.geoms:
                    if len(subpart.coords) >= 2:
                        part_coords = []
                        for lon, lat in subpart.coords:
                            norm_lon = ((lon + 180) % 360) - 180
                            part_coords.append([norm_lon, lat])
                        final_parts.append(LineString(part_coords))
        
        # Remove degenerate parts
        final_parts = [p for p in final_parts if p.length > 0]
        
        if len(final_parts) == 1:
            geom = final_parts[0]
        else:
            geom = MultiLineString(final_parts)
    else:
        # Just wrap in MultiLineString if not already
        geom = LineString(coords)
    
    features_out.append({
        'type': 'Feature',
        'properties': {
            'transect_id': props['transect_id'],
            'vessel': props['vessel'],
            'survey_date': props['survey_date']
        },
        'geometry': {
            'type': 'MultiLineString' if (crosses and len(final_parts) > 1) or (not crosses) else 'MultiLineString',
            'coordinates': []
        }
    })

# Actually, let me use geopandas instead which handles CRS conversion more cleanly
```

Hmm, this is getting complicated. Let me use a cleaner approach with geopandas and shapely.

Actually, let me use the shapely `split` approach correctly, and then use geopandas for CRS transformation and length calculation.

Let me write the final script:

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

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

# Fiji Map Grid is EPSG:2136
# But we need to first fix geometries, then reproject

results = []

for idx, row in gdf.iterrows():
    geom = row.geometry
    props = {
        'transect_id': row['transect_id'],
        'vessel': row['vessel'],
        'survey_date': row['survey_date']
    }
    crosses = row.get('crosses_antimeridian_flag', False)
    
    if crosses and geom.geom_type == 'LineString':
        coords = list(geom.coords)
        
        # Unwrap longitudes
        unwrapped = []
        shift = 0
        for i, (lon, lat) in enumerate(coords):
            if i > 0:
                prev_lon = coords[i-1][0]
                diff = lon - prev_lon
                if diff > 180:
                    shift -= 360
                elif diff < -180:
                    shift += 360
            unwrapped.append((lon + shift, lat))
        
        # Create unwrapped line
        unwrapped_line = LineString(unwrapped)
        
        # Find all 180° crossings
        lons = [c[0] for c in unwrapped]
        min_lon, max_lon = min(lons), max(lons)
        
        # Find 180 multiples between min and max
        split_vals = []
        m_start = int(np.ceil(min_lon / 180)) * 180
        m_end = int(np.floor(max_lon / 180)) * 180
        split_vals = list(range(int(m_start), int(m_end) + 1, 180))
        
        # Split at each
        parts = [unwrapped_line]
        for sv in split_vals:
            splitter = LineString([(sv, -90), (sv, 90)])
            new_parts = []
            for part in parts:
                try:
                    result = split(part, splitter)
                    new_parts.extend(result.geoms)
                except:
                    new_parts.append(part)
            parts = new_parts
        
        # Normalize longitudes back
        normalized_parts = []
        for part in parts:
            if part.geom_type == 'LineString' and len(part.coords) >= 2:
                pcoords = []
                for lon, lat in part.coords:
                    norm_lon = ((lon + 180) % 360) - 180
                    pcoords.append((norm_lon, lat))
                normalized_parts.append(LineString(pcoords))
        
        # Filter out degenerate
        normalized_parts = [p for p in normalized_parts if p.length > 1e-10]
        
        if len(normalized_parts) > 1:
            out_geom = MultiLineString(normalized_parts)
        else:
            out_geom = normalized_parts[0]
    else:
        out_geom = geom
    
    # Ensure MultiLineString
    if out_geom.geom_type == 'LineString':
        out_geom = MultiLineString([out_geom])
    
    results.append({'geometry': out_geom, **props})

# Create output GeoDataFrame
out_gdf = gpd.GeoDataFrame(results, geometry='geometry', crs='EPSG:4326')

# Reproject to Fiji Map Grid (EPSG:2136)
out_gdf = out_gdf.to_crs('EPSG:2136')

# Calculate length in meters
out_gdf['length_m'] = out_gdf.geometry.length

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

Wait, let me check if EPSG:2136 is the right one. Fiji 1956 / Fiji Map Grid. Let me verify.

Actually, let me check whether EPSG:2136 covers the whole Fiji area, and whether it's the standard national grid for Fiji.

From memory, Fiji Map Grid (FMG) uses the Fiji 1956 datum and is a transverse Mercator projection. Let me use it.

But actually, for some of these lines near 180°, the UTM zones for Fiji are:
- Zone 60 (UTM 60S) covers 174°E to 180°
- Zone 1 (UTM 1S) covers 180° to 186°E (which is -180 to -174°W)

So using a single UTM-based projection like EPSG:2136 (Fiji Map Grid) which is designed for Fiji would be the right approach.

Let me also double-check by looking up EPSG:2136 more carefully... Actually, let me just run the script and see what happens.

I also need to be careful about which parts of the antimeridian-crossing transects get which side. Let me trace through T001:

Original coords: [178.94, -17.63], [179.46, -17.62], [179.97, -17.61], [-179.51, -17.60], [-179.00, -17.58], [-178.48, -17.58]

Unwrapped: 
- [178.94, -17.63] (shift=0)
- [179.46, -17.62] (diff from prev = 0.52, fine)
- [179.97, -17.61] (diff from prev = 0.51, fine)
- [-179.51, -17.60] (diff from prev = -359.48, so shift += 360) → [180.49, -17.60]
- [-179.00, -17.58] (shift=360) → [181.00, -17.58]
- [-178.48, -17.58] (shift=360) → [181.52, -17.58]

So unwrapped: 178.94, 179.46, 179.97, 180.49, 181.00, 181.52

Split at 180: 
- Part 1: 178.94→179.97 (before 180), intersection at 180
  After normalizing: 178.94, 179.46, 179.97 - all fine
- Part 2: 180 → 181.52 (after 180)
  After normalizing: 180 - 360 = -180, 180.49-360 = -179.51, 181.00-360 = -179.00, 181.52-360 = -178.48
  → [-180, -17.60], [-179.51, -17.60], [-179.00, -17.58], [-178.48, -17.58]

But wait, the split should give us an intersection point at exactly 180° longitude. Let me make sure the split operation adds that vertex.

The split function in shapely should add intersection points correctly.

Let me also think about the intersection point normalization. The intersection point is at (180, some_lat). When normalized, 180 → -180 (since ((180+180)%360)-180 = (360%360)-180 = 0-180 = -180). Hmm, that's -180 which is fine.

Actually wait, ((180+180)%360)-180 = 360%360-180 = 0-180 = -180. Okay good.

But 180 is actually on the boundary. Let me think... The point at exactly longitude 180 should stay as 180 or -180. Let me handle this more carefully.

Actually for practical purposes, -180 and 180 are the same line. The important thing is that our parts don't span the globe.

Let me just write and test the script.

Actually, I realize there might be an issue with the split at exactly 180 for the parts. Let me also think about what happens when the intersection point is at 180 and we normalize:

Part 1 intersection: (180, some_lat) → ((180+180)%360)-180 = -180
Part 2 intersection: (180, some_lat) → -180 as well

So both parts share the point at -180 longitude, which connects them correctly.

Let me write the script properly now.

Actually, I just realized I should also handle the case where a transect doesn't cross the antimeridian but the geometry is already a MultiLineString or needs to be converted.

Let me write it all cleanly.

One more thing: the CRS. Let me check what Fiji's national grid CRS is called.

Fiji Map Grid - I believe it's EPSG:2136. Let me just use that.

Let me also think about whether any of the transects that DON'T have the flag set might still have issues. Looking at the data:

T011 through T030 all have `crosses_antimeridian_flag: false` and their coordinates are all on one side of 180°... let me verify a few:
- T011: all longitudes are negative (-176 to -177) - fine, west side
- T013: all negative (-176 to -178) - fine
- T015: all negative (-177 to -178) - fine
- T024: all negative (-179 to -176) - fine but wait, T024 has -179.1 which is very close... let me check if it crosses 180.

T024: [-179.108...], [-178.65...], [-178.19...], [-177.74...], [-177.28...], [-176.82...]
All are between -180 and 0, so no antimeridian crossing. Good.

OK let me write the script.

Actually, hmm, I wonder about the split function. Let me use a different approach: instead of splitting, I can manually cut the unwrapped line at each 180° crossing.

Here's a cleaner approach:

```python
def split_at_antimeridian(coords):
    """Split a line crossing the antimeridian into multiple segments."""
    # Unwrap
    unwrapped = []
    shift = 0
    for i, (lon, lat) in enumerate(coords):
        if i > 0:
            diff = lon - coords[i-1][0]
            if diff > 180:
                shift -= 360
            elif diff < -180:
                shift += 360
        unwrapped.append((lon + shift, lat))
    
    # Find intersection points with multiples of 180
    segments = []
    current_seg = []
    
    for i in range(len(unwrapped) - 1):
        x1, y1 = unwrapped[i]
        x2, y2 = unwrapped[i + 1]
        
        current_seg.append((x1, y1))
        
        # Check if this segment crosses a 180 boundary
        crosses = []
        # Find all 180-multiples between x1 and x2
        start_mlt = int(np.ceil(min(x1, x2) / 180)) * 180
        end_mlt = int(np.floor(max(x1, x2) / 180)) * 180
        
        for mlt in np.arange(start_mlt, end_mlt + 1, 180):
            if min(x1, x2) < mlt < max(x1, x2):
                # Linear interpolation
                t = (mlt - x1) / (x2 - x1)
                y_interp = y1 + t * (y2 - y1)
                crosses.append((mlt, y_interp))
        
        for cp in crosses:
            current_seg.append(cp)
            # Normalize current segment and add
            seg_coords = [((((x+180)%360)-180), y) for x, y in current_seg]
            segments.append(LineString(seg_coords))
            current_seg = [cp]
    
    # Add last point
    current_seg.append(unwrapped[-1])
    seg_coords = [((((x+180)%360)-180), y) for x, y in current_seg]
    segments.append(LineString(seg_coords))
    
    # Filter degenerate
    segments = [s for s in segments if s.length > 1e-10]
    
    return segments
```

This is cleaner and avoids the shapely split function which can be unreliable. Let me finalize the script.

Let me also think about what to do for T010 which has coords: [177.60, -17.28], [179.13, -17.47], [-179.35, -17.65], [-177.82, -17.84]
Unwrapped: 177.60, 179.13, 180.65 (= -179.35+360), 182.18 (= -177.82+360)
Split at 180: segment 1 = 177.60→179.13→180 (interp), segment 2 = 180→180.65→182.18
Norm segment 1: 177.60, 179.13, -180
Norm segment 2: -180, -179.35, -177.82

That looks correct!

OK let me write the final script now.

Let me also handle the case where we might get a 0-length segment or very small segment from the interpolation.

And one more thing: the split_at_antimeridian approach above might not work correctly if there are multiple 180-crossings in one segment of the original line... but looking at the data, each transect with the flag crosses the antimeridian exactly once, so this should be fine.

Actually, I need to be more careful. Let me re-examine the segment accumulation logic.

For T001 unwrapped: 178.94, 179.46, 179.97, 180.49, 181.00, 181.52

Iteration:
i=0: (178.94, 179.46). current_seg = [178.94]. No 180 crossing between 178.94 and 179.46. current_seg = [178.94].
i=1: (179.46, 179.97). current_seg = [178.94, 179.46]. No 180 crossing between 179.46 and 179.97. current_seg = [178.94, 179.46].
i=2: (179.97, 180.49). current_seg = [178.94, 179.46, 179.97]. 180 is between 179.97 and 180.49. 
  Interpolate: t=(180-179.97)/(180.49-179.97)=0.03/0.52=0.0577, y=-17.61+0.0577*(-17.60+17.61)=-17.61+0.000577=-17.6094
  cross point = (180, -17.6094)
  current_seg = [178.94, 179.46, 179.97, 180, -17.6094]
  Norm: [(178.94, -17.63), (179.46, -17.62), (179.97, -17.61), (-180, -17.6094)]
  Segment 1 = LineString([178.94, -17.63], ...)
  current_seg = [180, -17.6094]

i=3: (180.49, 181.00). current_seg = [180, -17.6094, 180.49, -17.60]. No 180 crossing between 180.49 and 181.00 (both > 180). current_seg = [180, -17.6094, 180.49, -17.60]
i=4: (181.00, 181.52). current_seg = [180, -17.6094, 180.49, -17.60, 181.00, -17.58]. No 180 crossing between 181.00 and 181.52. current_seg already contains all remaining points.

After loop: current_seg = [180, -17.6094, 180.49, -17.60, 181.00, -17.58, 181.52, -17.58]
Norm: [-180, -17.6094, -179.51, -17.60, -179.00, -17.58, -178.48, -17.58]
Segment 2 = LineString([-180, -17.6094], [-179.51, -17.60], [-179.00, -17.58], [-178.48, -17.58])

So we get 2 segments:
1. (178.94, -17.63) → (179.46, -17.62) → (179.97, -17.61) → (-180, -17.609)
2. (-180, -17.609) → (-179.51, -17.60) → (-179.00, -17.58) → (-178.48, -17.58)

These form a MultiLineString. When rendered, the first segment goes from 178.94 eastward to -180 (the antimeridian), and the second goes from -180 westward to -178.48. Perfect!

Now let me write the final script.

Let me think about what CRS to use. Let me search for the right EPSG code.

Fiji Map Grid (FMG) - EPSG:2136 (Fiji 1956 / Fiji Map Grid)
This uses the Fiji 1956 datum and transverse Mercator projection.

Actually wait, I should double check that. Let me just try it.

Also, regarding the output: "Carry the survey attributes (vessel and survey_date) through unchanged, use transect_id as the key field"

And: "Every feature must come out as a MultiLineString, so do not leave any transects as plain LineString"

So even non-crossing transects should be wrapped in MultiLineString.

Let me finalize:

```python
import json
import numpy as np
import geopandas as gpd
from shapely.geometry import LineString, MultiLineString, shape

def split_at_antimeridian(coords):
    """
    Split a LineString that crosses the antimeridian (180° longitude)
    into segments that don't cross it.
    Returns a list of LineString segments.
    """
    # Unwrap longitudes to make the coordinate sequence continuous
    unwrapped = []
    shift = 0.0
    for i, (lon, lat) in enumerate(coords):
        if i > 0:
            diff = lon - coords[i-1][0]
            if diff > 180:
                shift -= 360
            elif diff < -180:
                shift += 360
        unwrapped.append((lon + shift, lat))
    
    # Walk through unwrapped segments, splitting at each 180° multiple
    segments = []
    current = []
    
    for i in range(len(unwrapped)):
        x1, y1 = unwrapped[i]
        
        if i == 0:
            current.append((x1, y1))
            continue
        
        x0, y0 = unwrapped[i-1]
        
        # Find all 180 multiples between x0 and x1
        lo, hi = min(x0, x1), max(x0, x1)
        # Multiples of 180 strictly between lo and hi
        start_m = int(np.ceil(lo / 180)) * 180
        end_m = int(np.floor(hi / 180)) * 180
        crossings = list(np.arange(start_m, end_m + 1, 180))
        crossings = [c for c in crossings if lo < c < hi]
        
        if not crossings:
            current.append((x1, y1))
        else:
            # Append points up to each crossing, closing the current segment
            prev_x, prev_y = x0, y0
            for cx in crossings:
                t = (cx - prev_x) / (x1 - prev_x)
                cy = prev_y + t * (y1 - prev_y)
                current.append((cx, cy))
                # Normalize and finalize current segment
                seg_coords = [((((x + 180) % 360) - 180), y) for x, y in current]
                seg = LineString(seg_coords)
                if seg.length > 1e-10:
                    segments.append(seg)
                # Start new segment from this crossing point
                current = [(cx, cy)]
                prev_x, prev_y = cx, cy
            
            # Add the current endpoint
            current.append((x1, y1))
    
    # Finalize last segment
    if len(current) >= 2:
        seg_coords = [((((x + 180) % 360) - 180), y) for x, y in current]
        seg = LineString(seg_coords)
        if seg.length > 1e-10:
            segments.append(seg)
    
    return segments


# Read input
with open('/work/fiji_transects_wgs84.geojson') as f:
    data = json.load(f)

features_out = []

for feat in data['features']:
    props = feat['properties']
    geom = feat['geometry']
    coords = geom['coordinates']
    crosses = props.get('crosses_antimeridian_flag', False)
    
    if crosses and geom['type'] == 'LineString':
        parts = split_at_antimeridian(coords)
        if len(parts) == 0:
            continue  # skip degenerate
        out_geom = MultiLineString(parts) if len(parts) > 1 else parts[0]
    else:
        out_geom = shape(geom)
    
    # Ensure MultiLineString
    if out_geom.geom_type == 'LineString':
        out_geom = MultiLineString([out_geom])
    
    features_out.append({
        'type': 'Feature',
        'properties': {
            'transect_id': props['transect_id'],
            'vessel': props['vessel'],
            'survey_date': props['survey_date']
        },
        'geometry': {
            'type': 'MultiLineString',
            'coordinates': [
                [list(c) for c in part.coords]
                for part in out_geom.geoms
            ]
        }
    })

# Write intermediate fixed GeoJSON
fc = {
    'type': 'FeatureCollection',
    'features': features_out
}
with open('/work/fiji_fixed_wgs84.geojson', 'w') as f:
    json.dump(fc, f)

# Now use geopandas to reproject and add length
gdf = gpd.read_file('/work/fiji_fixed_wgs84.geojson')
gdf = gdf.set_crs('EPSG:4326')
gdf = gdf.to_crs('EPSG:2136')
gdf['length_m'] = gdf.geometry.length.round(2)
gdf.to_file('/work/fiji_transects_fmg.geojson', driver='GeoJSON')
```

OK, I think this approach is solid. Let me code it up properly and test.

Wait, I realized I should just use geopandas throughout instead of manually creating the output geojson. Let me simplify.

Actually, the manual approach for the splitting is needed since geopandas doesn't have a built-in antimeridian splitter. Let me keep the split function and then use geopandas for the CRS conversion and length calculation.

Let me finalize and write the script.
1:08
Write /work/solve.py
content · 143 lines · py
import json
import numpy as np
import geopandas as gpd
from shapely.geometry import LineString, MultiLineString, shape


def split_at_antimeridian(coords):
    """
    Split a LineString that crosses the antimeridian (180° longitude)
    into segments on each side. Returns a list of LineString segments.
    """
    # --- Unwrap longitudes to make the coordinate sequence continuous ---
    unwrapped = []
    shift = 0.0
    for i, (lon, lat) in enumerate(coords):
        if i > 0:
            diff = lon - coords[i - 1][0]
            if diff > 180:
                shift -= 360
            elif diff < -180:
                shift += 360
        unwrapped.append((lon + shift, lat))

    # --- Walk through, splitting at every 180° multiple ---
    segments = []

    # Helper: close and save the current segment
    def close_segment(pts):
        """Normalise longitudes in pts back to [-180, 180) and return a LineString."""
        norm = [((((x + 180) % 360) - 180), y) for x, y in pts]
        ls = LineString(norm)
        return ls if ls.length > 1e-10 else None

    current = [unwrapped[0]]

    for i in range(1, len(unwrapped)):
        x0, y0 = unwrapped[i - 1]
        x1, y1 = unwrapped[i]

        # All 180-multiples strictly between x0 and x1
        lo, hi = min(x0, x1), max(x0, x1)
        start_m = int(np.ceil(lo / 180)) * 180
        end_m = int(np.floor(hi / 180)) * 180
        crossings = list(np.arange(start_m, end_m + 1, 180))
        crossings = [c for c in crossings if lo < c < hi]

        if not crossings:
            current.append((x1, y1))
        else:
            prev_x, prev_y = x0, y0
            for cx in crossings:
                # Interpolate intersection point
                t = (cx - prev_x) / (x1 - prev_x)
                cy = prev_y + t * (y1 - prev_y)
                current.append((cx, cy))

                # Close current segment
                seg = close_segment(current)
                if seg is not None:
                    segments.append(seg)

                # Start a new segment from this crossing point
                current = [(cx, cy)]
                prev_x, prev_y = cx, cy

            # Add the far endpoint
            current.append((x1, y1))

    # Close the last segment
    if len(current) >= 2:
        seg = close_segment(current)
        if seg is not None:
            segments.append(seg)

    return segments


# ---------------------------------------------------------------------------
# 1.  Read input GeoJSON
# ---------------------------------------------------------------------------
with open("/work/fiji_transects_wgs84.geojson") as f:
    src = json.load(f)

# ---------------------------------------------------------------------------
# 2.  Fix antimeridian-crossing geometries; guarantee MultiLineString
# ---------------------------------------------------------------------------
features_out = []

for feat in src["features"]:
    props = feat["properties"]
    g = feat["geometry"]
    coords = g["coordinates"]
    crosses = props.get("crosses_antimeridian_flag", False)

    if crosses and g["type"] == "LineString":
        parts = split_at_antimeridian(coords)
        if not parts:
            continue  # skip degenerate
        out_geom = MultiLineString(parts) if len(parts) > 1 else parts[0]
    else:
        out_geom = shape(g)

    # Unconditionally wrap into MultiLineString
    if out_geom.geom_type == "LineString":
        out_geom = MultiLineString([out_geom])

    features_out.append(
        {
            "type": "Feature",
            "properties": {
                "transect_id": props["transect_id"],
                "vessel": props["vessel"],
                "survey_date": props["survey_date"],
            },
            "geometry": {
                "type": "MultiLineString",
                "coordinates": [
                    [list(c) for c in part.coords] for part in out_geom.geoms
                ],
            },
        }
    )

# Save intermediate fixed (WGS84) file for debugging / handoff
fixed_fc = {"type": "FeatureCollection", "features": features_out}
with open("/work/fiji_fixed_wgs84.geojson", "w") as f:
    json.dump(fixed_fc, f)

# ---------------------------------------------------------------------------
# 3.  Reproject to Fiji Map Grid (EPSG:2136) and compute length
# ---------------------------------------------------------------------------
gdf = gpd.read_file("/work/fiji_fixed_wgs84.geojson")
gdf = gdf.set_crs("EPSG:4326")
gdf = gdf.to_crs("EPSG:2136")

gdf["length_m"] = gdf.geometry.length.round(2)

# ---------------------------------------------------------------------------
# 4.  Write final output
# ---------------------------------------------------------------------------
gdf.to_file("/work/fiji_transects_fmg.geojson", driver="GeoJSON")

print("Done.  Output written to fiji_transects_fmg.geojson")
Wrote /work/solve.py (4888 chars).
1:19
Let me run this and check the output.
1:19
Bash cd /work && python solve.py 2>&1
Done.  Output written to fiji_transects_fmg.geojson
1:37
Good, the script ran without errors. Let me verify the output now.

1. Check it's valid GeoJSON
2. Check all features are MultiLineString
3. Check transect_id, vessel, survey_date are preserved
4. Check length_m is present
5. Check the CRS is EPSG:2136 (Fiji Map Grid) 
6. Check the antimeridian-crossing transects are properly split
1:37
Bash cd /work && python -c "
cd /work && python -c "
import json
with open('fiji_transects_fmg.geojson') as f:
    data = json.load(f)

print('Number of features:', len(data['features']))
types = {}
multi_count = 0
for f in data['features']:
    gt = f['geometry']['type']
    types[gt] = types.get(gt, 0) + 1
    if gt == 'MultiLineString':
        n = len(f['geometry']['coordinates'])
        multi_count += 1
    assert 'length_m' in f['properties'], 'missing length_m'
    assert 'transect_id' in f['properties'], 'missing transect_id'
    assert 'vessel' in f['properties'], 'missing vessel'
    assert 'survey_date' in f['properties'], 'missing survey_date'

print('Geometry types:', types)
print('MultiLineString features:', multi_count)
print('All have length_m:', all('length_m' in f['properties'] for f in data['features']))
" 2>&1
Number of features: 30
Geometry types: {'MultiLineString': 30}
MultiLineString features: 30
All have length_m: True
1:56
All 30 features are MultiLineString, all have the required attributes. Let me check which ones were actually split into multiple parts (antimeridian crossers) to make sure the fix worked properly.
1:56
Bash cd /work && python -c "
cd /work && python -c "
import json
with open('fiji_transects_fmg.geojson') as f:
    data = json.load(f)

for feat in data['features']:
    tid = feat['properties']['transect_id']
    n_parts = len(feat['geometry']['coordinates'])
    lm = feat['properties']['length_m']
    # Check coordinate ranges
    lons = [c[0] for part in feat['geometry']['coordinates'] for c in part]
    lats = [c[1] for part in feat['geometry']['coordinates'] for c in part]
    print(f'{tid}: {n_parts} part(s), length_m={lm}')
    print(f'  coords: lon [{min(lons):.1f}, {max(lons):.1f}], lat [{min(lats):.1f}, {max(lats):.1f}]')
" 2>&1
T001: 2 part(s), length_m=896625.68
  coords: lon [23681.9, 920132.9], lat [-60923832.3, -60908385.5]
T002: 2 part(s), length_m=710444.21
  coords: lon [187568.3, 875394.5], lat [-61194208.5, -61016397.0]
T003: 2 part(s), length_m=1154168.04
  coords: lon [224189.2, 1375120.7], lat [-61005976.8, -60920076.2]
T004: 2 part(s), length_m=1222435.48
  coords: lon [-225287.2, 943825.6], lat [-61203146.8, -60846297.1]
T005: 2 part(s), length_m=1188846.33
  coords: lon [213844.8, 1387555.0], lat [-60800923.3, -60612352.9]
T006: 2 part(s), length_m=1291137.44
  coords: lon [-190249.6, 1030869.2], lat [-61055261.7, -60636058.1]
T007: 2 part(s), length_m=1360035.68
  coords: lon [-80804.6, 1276814.4], lat [-61162992.0, -61082501.2]
T008: 2 part(s), length_m=481530.8
  coords: lon [284643.0, 760502.2], lat [-60871950.3, -60798559.3]
T009: 2 part(s), length_m=864053.48
  coords: lon [363441.1, 1225883.4], lat [-61261373.7, -61208713.1]
T010: 2 part(s), length_m=1606876.65
  coords: lon [-205712.8, 1387227.1], lat [-61035597.7, -60824554.7]
T011: 1 part(s), length_m=257433.68
  coords: lon [-539630.6, -284109.2], lat [-60834059.7, -60803934.6]
T012: 1 part(s), length_m=277384.19
  coords: lon [1785451.9, 1834001.5], lat [-61049901.9, -60776801.6]
T013: 1 part(s), length_m=589707.67
  coords: lon [-594658.8, -70958.6], lat [-60868111.4, -60597057.0]
T014: 1 part(s), length_m=486879.72
  coords: lon [1308115.2, 1761726.5], lat [-61279886.2, -61103691.2]
T015: 1 part(s), length_m=409125.59
  coords: lon [-402671.1, 1443.4], lat [-61225792.9, -61163014.0]
T016: 1 part(s), length_m=174892.97
  coords: lon [918044.2, 1092341.6], lat [-60694172.7, -60686588.5]
T017: 1 part(s), length_m=499940.74
  coords: lon [-518902.8, -251500.4], lat [-61271242.8, -60848870.0]
T018: 1 part(s), length_m=368875.43
  coords: lon [1654819.3, 1910097.8], lat [-61184584.3, -60918339.5]
T019: 1 part(s), length_m=588730.78
  coords: lon [1245251.8, 1818828.1], lat [-61012963.9, -60880377.0]
T020: 1 part(s), length_m=706542.7
  coords: lon [986144.3, 1643816.1], lat [-61102972.1, -60844851.0]
T021: 1 part(s), length_m=755355.61
  coords: lon [1255685.5, 1857249.0], lat [-61097475.2, -60640660.6]
T022: 1 part(s), length_m=167270.73
  coords: lon [1443797.7, 1555230.5], lat [-60961468.3, -60836841.4]
T023: 1 part(s), length_m=268879.11
  coords: lon [-426323.6, -175537.6], lat [-60877690.8, -60782046.8]
T024: 1 part(s), length_m=823501.33
  coords: lon [-556091.9, 238518.8], lat [-61189392.7, -60973543.7]
T025: 1 part(s), length_m=355870.41
  coords: lon [1741516.9, 1745101.9], lat [-61043371.7, -60687519.4]
T026: 1 part(s), length_m=330249.05
  coords: lon [1028670.6, 1355947.9], lat [-60938945.5, -60896915.5]
T027: 1 part(s), length_m=426692.28
  coords: lon [-226455.7, 160274.3], lat [-61061764.3, -60881749.9]
T028: 1 part(s), length_m=567553.22
  coords: lon [-646708.2, -96693.4], lat [-61305308.3, -61165957.5]
T029: 1 part(s), length_m=673211.52
  coords: lon [-641597.8, -120053.3], lat [-61148798.3, -60723211.2]
T030: 1 part(s), length_m=537360.82
  coords: lon [-228458.4, 213445.0], lat [-61165464.1, -60859921.5]
2:10
Wait, something is wrong. The lat values are around -60,000,000 to -61,000,000 meters. That's way too far south for Fiji. Let me check what EPSG:2136 actually is.

EPSG:2136 is "Fiji 1956 / Fiji Map Grid" - a projected CRS for Fiji. Let me check the standard parameters.

Actually, looking at the coordinates, those are huge negative numbers in the millions. That's not right for a metric grid. Fiji is in the southern hemisphere so y (northing) should be positive if using a standard false northing, or negative if false northing is 0. But -60 million meters is extreme.

Let me check - EPSG:2136 uses the "Fiji 1956" datum, which is a local datum. The projection is likely a Transverse Mercator with a central meridian of 177°E or 178°E or 180°... Let me look more carefully.

Actually, EPSG:2136 - Fiji 1956 / Fiji Map Grid. The projection is "Fiji Map Grid" which is a Transverse Mercator projection with:
- Central Meridian = 178°E (some sources say 180°...)

Let me check by looking at the actual EPSG definition. Actually, I think the issue is that EPSG:2136 uses a Hotine Oblique Mercator projection, not a standard Transverse Mercator. The definition is quite specific.

Wait, let me reconsider. The lat values around -60 million suggest something is off. For a location around 18°S in Fiji, the y coordinate should be something like:
- If false northing is 0, then y ≈ -18° in meters = -18 * 111,000 ≈ -2,000,000 meters
- But -60,000,000 is way off.

Actually, maybe I'm using the wrong EPSG code. Let me think about what "Fiji's national metric grid" actually is.

The modern Fiji national grid is EPSG:2136 "Fiji 1956 / Fiji Map Grid", but there's also:
- EPSG:3460 "Fiji 1986 / Fiji Map Grid"
- Or maybe they use a more recent CRS

Wait, let me look at the longitude values more carefully. For T011 which is at longitudes around -176.8 to -177.6 degrees, the projected x values are -539,630 to -284,109. That seems plausible for an Oblique Mercator projection centered on Fiji.

But the y values at -60,800,000 to -61,200,000 are way off. These look like they might be in millimeters rather than meters, or something else is wrong.

Actually, I think the issue might be that pyproj / geopandas is using a different datum transformation and the coordinates are computed incorrectly. Let me check what pyproj says about EPSG:2136.

Actually, let me look at this more carefully. The Fiji Map Grid coordinates should look like:
- Easting: around 100,000 to 900,000 meters (typical for a projected CRS with a false easting)
- Northing: positive values in the southern hemisphere, or negative if false northing is 0

For Oblique Mercator projections, the coordinates can be quite different from UTM. Let me check.

Actually, I realize the y values are about -60 million, which is about the right magnitude if you consider degrees * 111,111 * 3 = degrees * 333,333... No, that doesn't make sense.

Let me actually check the EPSG definition parameters for 2136.

From the EPSG database, Fiji 1956 / Fiji Map Grid (EPSG:2136) is:
- Method: Hotine Oblique Mercator (variant A)
- Latitude of projection center: -17° 50' (approx 17.8333°S)
- Longitude of projection center: 178° 20' (178.3333°E) 
- Azimuth: 332.5°
- Scale factor: 1.0
- False easting: 300,000
- False northing: 500,000

Hmm, but the coordinates I'm getting look very wrong. Let me just run a quick check with pyproj to see what's happening.

Actually, let me just try using EPSG:3460 (Fiji 1986 / Fiji Map Grid) instead, or maybe I should look at what the actual coordinates should be.

Wait, let me reconsider. The pyproj transformation from WGS84 to Fiji 1956 might be using a bad transformation or no transformation. Let me check by reprojecting a single point.
2:10
Bash cd /work && python -c "
cd /work && python -c "
import pyproj
# Check EPSG:2136 parameters
crs = pyproj.CRS.from_epsg(2136)
print(crs.to_wkt(pretty=True))
print()

# Also check EPSG:3460
crs2 = pyproj.CRS.from_epsg(3460)
print(crs2.to_wkt(pretty=True))
"
PROJCRS["Accra / Ghana National Grid",
    BASEGEOGCRS["Accra",
        DATUM["Accra",
            ELLIPSOID["War Office",6378300,296,
                LENGTHUNIT["metre",1]]],
        PRIMEM["Greenwich",0,
            ANGLEUNIT["degree",0.0174532925199433]],
        ID["EPSG",4168]],
    CONVERSION["Ghana National Grid",
        METHOD["Transverse Mercator",
            ID["EPSG",9807]],
        PARAMETER["Latitude of natural origin",4.66666666666667,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8801]],
        PARAMETER["Longitude of natural origin",-1,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8802]],
        PARAMETER["Scale factor at natural origin",0.99975,
            SCALEUNIT["unity",1],
            ID["EPSG",8805]],
        PARAMETER["False easting",900000,
            LENGTHUNIT["Gold Coast foot",0.304799710181509],
            ID["EPSG",8806]],
        PARAMETER["False northing",0,
            LENGTHUNIT["Gold Coast foot",0.304799710181509],
            ID["EPSG",8807]]],
    CS[Cartesian,2],
        AXIS["(E)",east,
            ORDER[1],
            LENGTHUNIT["Gold Coast foot",0.304799710181509]],
        AXIS["(N)",north,
            ORDER[2],
            LENGTHUNIT["Gold Coast foot",0.304799710181509]],
    USAGE[
        SCOPE["Engineering survey, topographic mapping."],
        AREA["Ghana - onshore."],
        BBOX[4.67,-3.25,11.16,1.23]],
    ID["EPSG",2136]]

PROJCRS["Fiji 1986 / Fiji Map Grid",
    BASEGEOGCRS["Fiji 1986",
        DATUM["Fiji Geodetic Datum 1986",
            ELLIPSOID["WGS 72",6378135,298.26,
                LENGTHUNIT["metre",1]]],
        PRIMEM["Greenwich",0,
            ANGLEUNIT["degree",0.0174532925199433]],
        ID["EPSG",4720]],
    CONVERSION["Fiji Map Grid",
        METHOD["Transverse Mercator",
            ID["EPSG",9807]],
        PARAMETER["Latitude of natural origin",-17,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8801]],
        PARAMETER["Longitude of natural origin",178.75,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8802]],
        PARAMETER["Scale factor at natural origin",0.99985,
            SCALEUNIT["unity",1],
            ID["EPSG",8805]],
        PARAMETER["False easting",2000000,
            LENGTHUNIT["metre",1],
            ID["EPSG",8806]],
        PARAMETER["False northing",4000000,
            LENGTHUNIT["metre",1],
            ID["EPSG",8807]]],
    CS[Cartesian,2],
        AXIS["(E)",east,
            ORDER[1],
            LENGTHUNIT["metre",1]],
        AXIS["(N)",north,
            ORDER[2],
            LENGTHUNIT["metre",1]],
    USAGE[
        SCOPE["Cadastre, engineering survey, topographic mapping."],
        AREA["Fiji - onshore. Includes Viti Levu, Vanua Levu, Taveuni, the Yasawa Group, the Kadavu Group, the Lau Islands and Rotuma Islands."],
        BBOX[-20.81,176.81,-12.42,-178.15]],
    ID["EPSG",3460]]
2:31
Ah, the issue is clear. EPSG:2136 is not Fiji Map Grid - it's Accra / Ghana National Grid. Fiji Map Grid is EPSG:3460. Let me fix the CRS.

Also, looking at the coordinate output from the current file, the lat values are around -60,000,000 which is clearly wrong for Fiji (which should be around 3,000,000 to 4,000,000 northing for FMG). The eastings should be around 2,000,000. So clearly EPSG:2136 was wrong.

Let me also look more carefully at the antimeridian splitting. The coordinates with longitudes crossing from positive (east of 180) to negative (west of 180) represent transects that cross the 180° meridian (antimeridian). The issue is that in WGS84, coordinates like [179.97..., -17.61...] then [-179.51..., -17.60...] appear to jump across the whole globe (from near 180°E to near 180°W), when really they're just crossing the antimeridian.

For the Fiji Map Grid (EPSG:3460), the central meridian is 178.75°E, so this should handle Fiji data well.

Let me fix the script to use EPSG:3460 instead of EPSG:2136.
2:31
2:31
Edit /work/solve.py
Edited /work/solve.py.
2:44
Wait, let me also check more carefully: there are "Fiji 1986 / Fiji Map Grid" (EPSG:3460) and "Fiji 1956 / Fiji Map Grid" (EPSG:2135). The most common modern one is EPSG:3460. Let me check EPSG:2135 to see what it is.
2:44
Bash python -c "
python -c "
import pyproj
crs = pyproj.CRS.from_epsg(2135)
print(crs.to_wkt(pretty=True))
"
PROJCRS["NZGD2000 / UTM zone 60S",
    BASEGEOGCRS["NZGD2000",
        DATUM["New Zealand Geodetic Datum 2000",
            ELLIPSOID["GRS 1980",6378137,298.257222101,
                LENGTHUNIT["metre",1]]],
        PRIMEM["Greenwich",0,
            ANGLEUNIT["degree",0.0174532925199433]],
        ID["EPSG",4167]],
    CONVERSION["UTM zone 60S",
        METHOD["Transverse Mercator",
            ID["EPSG",9807]],
        PARAMETER["Latitude of natural origin",0,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8801]],
        PARAMETER["Longitude of natural origin",177,
            ANGLEUNIT["degree",0.0174532925199433],
            ID["EPSG",8802]],
        PARAMETER["Scale factor at natural origin",0.9996,
            SCALEUNIT["unity",1],
            ID["EPSG",8805]],
        PARAMETER["False easting",500000,
            LENGTHUNIT["metre",1],
            ID["EPSG",8806]],
        PARAMETER["False northing",10000000,
            LENGTHUNIT["metre",1],
            ID["EPSG",8807]]],
    CS[Cartesian,2],
        AXIS["(E)",east,
            ORDER[1],
            LENGTHUNIT["metre",1]],
        AXIS["(N)",north,
            ORDER[2],
            LENGTHUNIT["metre",1]],
    USAGE[
        SCOPE["Engineering survey, topographic mapping."],
        AREA["New Zealand - offshore between 174°E and 180°E."],
        BBOX[-54.32,174,-26.42,180]],
    ID["EPSG",2135]]
2:51
OK so the correct EPSG code for Fiji Map Grid is **3460** (not 2136 which was Ghana's grid). Let me re-run the script.
2:51
2:51
Bash cd /work && python solve.py 2>&1
Done.  Output written to fiji_transects_fmg.geojson
2:59
Let me verify the output now - expecting coordinate values on the order of ~1e6 for easting (Fiji Map Grid has false easting 2,000,000) and ~4e6 for northing (false northing 4,000,000).
2:59
Bash cd /work && python -c "
cd /work && python -c "
import json
with open('fiji_transects_fmg.geojson') as f:
    data = json.load(f)

print('Number of features:', len(data['features']))
types = {}
multi_count = 0
for feat in data['features']:
    gt = feat['geometry']['type']
    types[gt] = types.get(gt, 0) + 1
    if gt == 'MultiLineString':
        n_parts = len(feat['geometry']['coordinates'])
        multi_count += 1

print('Geometry types:', types)
print('All MultiLineString:', multi_count == len(data['features']))

print()
print('Feature details:')
for feat in data['features']:
    props = feat['properties']
    tid = props['transect_id']
    n_parts = len(feat['geometry']['coordinates'])
    lm = props['length_m']
    all_coords = [c for part in feat['geometry']['coordinates'] for c in part]
    eastings = [c[0] for c in all_coords]
    northings = [c[1] for c in all_coords]
    print(f'{tid}: {n_parts} part(s), length_m={lm:.1f}  easting [{min(eastings):.1f}, {max(eastings):.1f}]  northing [{min(northings):.1f}, {max(northings):.1f}]')
" 2>&1
Number of features: 30
Geometry types: {'MultiLineString': 30}
All MultiLineString: True

Feature details:
T001: 2 part(s), length_m=273335.1  easting [2020343.3, 2293630.6]  northing [3930017.5, 3934438.1]
T002: 2 part(s), length_m=216575.3  easting [2034023.8, 2243773.3]  northing [3962923.9, 4016860.3]
T003: 2 part(s), length_m=351823.5  easting [1881698.8, 2232501.1]  northing [3933301.5, 3959945.9]
T004: 2 part(s), length_m=372666.1  easting [2013096.2, 2369647.4]  northing [3911101.2, 4019426.6]
T005: 2 part(s), length_m=362394.6  easting [1877826.2, 2235528.3]  northing [3839483.5, 3897452.1]
T006: 2 part(s), length_m=393605.6  easting [1986477.0, 2358907.3]  northing [3847052.7, 3974351.6]
T007: 2 part(s), length_m=414594.4  easting [1911692.4, 2325581.1]  northing [3983231.5, 4007239.7]
T008: 2 part(s), length_m=146792.5  easting [2068958.4, 2214051.7]  northing [3896474.8, 3918654.1]
T009: 2 part(s), length_m=263388.3  easting [1927285.5, 2190162.2]  northing [4021349.8, 4037732.2]
T010: 2 part(s), length_m=489842.5  easting [1878020.6, 2363529.5]  northing [3904005.4, 3968979.0]
T011: 1 part(s), length_m=78492.2  easting [2387423.5, 2465344.9]  northing [3897686.5, 3906767.5]
T012: 1 part(s), length_m=84537.6  easting [1741748.7, 1756655.2]  northing [3890283.7, 3973496.0]
T013: 1 part(s), length_m=179800.5  easting [2322350.7, 2482137.4]  northing [3834700.4, 3917127.9]
T014: 1 part(s), length_m=148392.9  easting [1763975.2, 1902160.3]  northing [3989702.4, 4043578.8]
T015: 1 part(s), length_m=124738.2  easting [2300505.4, 2423739.8]  northing [4007278.2, 4026263.3]
T016: 1 part(s), length_m=53310.2  easting [1967763.1, 2020890.9]  northing [3862426.3, 3864774.4]
T017: 1 part(s), length_m=152432.4  easting [2377499.6, 2459196.8]  northing [3911400.2, 4040076.9]
T018: 1 part(s), length_m=112421.6  easting [1718720.3, 1796415.9]  northing [3933347.3, 4014591.1]
T019: 1 part(s), length_m=179435.7  easting [1746468.6, 1921232.2]  northing [3921611.4, 3962251.7]
T020: 1 part(s), length_m=215352.2  easting [1799842.8, 2000196.1]  northing [3910677.6, 3989614.6]
T021: 1 part(s), length_m=230219.2  easting [1734792.7, 1917953.2]  northing [3848549.9, 3988023.0]
T022: 1 part(s), length_m=50981.8  easting [1826735.5, 1860749.0]  northing [3908468.0, 3946407.5]
T023: 1 part(s), length_m=81980.1  easting [2354312.1, 2430814.5]  northing [3891057.5, 3920116.8]
T024: 1 part(s), length_m=251074.9  easting [2228238.5, 2470420.2]  northing [3949291.0, 4015411.7]
T025: 1 part(s), length_m=108459.1  easting [1768950.1, 1769898.1]  northing [3863035.0, 3971489.9]
T026: 1 part(s), length_m=100661.5  easting [1887498.9, 1987271.4]  northing [3926697.1, 3939376.3]
T027: 1 part(s), length_m=130089.7  easting [2251971.1, 2369948.7]  northing [3921591.4, 3976319.9]
T028: 1 part(s), length_m=173047.0  easting [2330479.8, 2498125.9]  northing [4007925.0, 4050623.3]
T029: 1 part(s), length_m=205262.6  easting [2337541.9, 2496390.5]  northing [3872926.5, 4002897.0]
T030: 1 part(s), length_m=163829.4  easting [2235752.4, 2370599.8]  northing [3914958.1, 4007936.3]
3:18
Now the coordinates look correct. The eastings are around 1.7M to 2.5M and northings are around 3.8M to 4.0M, which makes sense for Fiji Map Grid (EPSG:3460) with false easting 2,000,000 and false northing 4,000,000. The values are in metres, which is correct.

Let me verify the output file and do a final sanity check.
3:18
3:18
Bash cd /work && ls -la fiji_transects_fmg.geojson && head -1 fiji_transects_fmg.geoj...
cd /work && ls -la fiji_transects_fmg.geojson && head -1 fiji_transects_fmg.geojson && python -c "
import json
with open('fiji_transects_fmg.geojson') as f:
    data = json.load(f)
# verify all required properties
required = ['transect_id', 'vessel', 'survey_date', 'length_m']
for feat in data['features']:
    p = feat['properties']
    for r in required:
        assert r in p, f'{feat[\"properties\"].get(\"transect_id\")} missing {r}'
    assert feat['geometry']['type'] == 'MultiLineString', f'{p[\"transect_id\"]} not MultiLineString'
print('All 30 features valid: MultiLineString, all attributes present.')
"
-rw-r--r-- 1 agent agent 16588 Jun 17 22:08 fiji_transects_fmg.geojson
{
All 30 features valid: MultiLineString, all attributes present.
3:33
Everything checks out. Let me also verify the antimeridian-crossing transects are indeed multi-part. From the output, T001-T010 are flagged as crossing the antimeridian and they all have 2 parts. The rest have 1 part. That's correct.

Let me summarize what was done.
3:33