crs-l2-fiji-antimeridian

Status: done
Score: 0.618
Duration: 3:04
Cost: 1.32¢
Model: google/gemma-4-26b-a4b-it

Map (reference ← swipe → agent)

0:00
I'm pulling together per-transect coverage figures for our survey report, but some of the lines in fiji_transects_wgs84.geojson render as arcs spanning the entire globe instead of short local segments. Can you fix the geometries so they render correctly, convert them to Fiji's national metric grid, and give me each transect as one MultiLineString feature with a length_m attribute in metres? Every feature must come out as a MultiLineString, so do not leave any transects as plain LineString, and the problematic ones must end up as multi-part geometries that faithfully represent the actual survey path. Carry the survey attributes (vessel and survey_date) through unchanged, use transect_id as the key field, and write the result as GeoJSON named fiji_transects_fmg.geojson.
0:00
System info
{
  "adapter": "openrouter",
  "model": "google/gemma-4-26b-a4b-it",
  "base_url": "https://openrouter.ai/api/v1"
}
0:01
Bash ls -l
ls -l
total 16
-rw-r--r-- 1 agent agent 14090 Jun 15 20:49 fiji_transects_wgs84.geojson
0:14
Bash head -n 20 fiji_transects_wgs84.geojson
head -n 20 fiji_transects_wgs84.geojson
{
"type": "FeatureCollection",
"name": "fiji_transects_wgs84",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "transect_id": "T001", "vessel": "Taveuni II", "survey_date": "2025-08-15", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 178.941879580782086, -17.632301816020771 ], [ 179.456655009654696, -17.623768593111961 ], [ 179.971430438527307, -17.611072392823225 ], [ -179.513794132600054, -17.596198176111951 ], [ -178.999018703727444, -17.579632069513806 ], [ -178.484243274854833, -17.577321872344196 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T002", "vessel": "Bligh", "survey_date": "2025-08-12", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 179.070288824800542, -17.334767984150709 ], [ 179.725942881740252, -17.171343988196945 ], [ -179.618403061320009, -17.007726843860763 ], [ -178.962749004380299, -16.834850408534852 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T003", "vessel": "Lomaiviti", "survey_date": "2025-08-12", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 177.636947428758333, -17.358837040696127 ], [ 178.462815605623859, -17.416074837434927 ], [ 179.288683782489386, -17.471713663377283 ], [ -179.885448040645059, -17.527675804260884 ], [ -179.059579863779533, -17.590577543375694 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T004", "vessel": "Vanua I", "survey_date": "2025-08-15", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 178.873696381462679, -17.803295238757038 ], [ 179.542254331047587, -17.598477321703708 ], [ -179.789187719367533, -17.405549472537043 ], [ -179.120629769782624, -17.19411411231702 ], [ -178.452071820197745, -16.992332922174427 ], [ -177.783513870612836, -16.795171196845544 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T005", "vessel": "Bligh", "survey_date": "2025-08-19", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 177.596924830927293, -17.923255155112717 ], [ 178.160696712947697, -18.010328270600898 ], [ 178.724468594968101, -18.099224972140938 ], [ 179.288240476988506, -18.17532145144995 ], [ 179.85201235900891, -18.270380145331412 ], [ -179.584215758970686, -18.358875431719703 ], [ -179.020443876950281, -18.437390943572723 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T006", "vessel": "Cakaulevu", "survey_date": "2025-08-13", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 178.622166647099931, -18.382052393687179 ], [ 179.205668025282989, -18.184610858038685 ], [ 179.789169403466019, -17.991866703295319 ], [ -179.627329218350923, -17.789532271264445 ], [ -179.043827840167864, -17.595682751178369 ], [ -178.460326461984835, -17.393483000040479 ], [ -177.876825083801776, -17.203490564162109 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T007", "vessel": "Vanua I", "survey_date": "2025-08-19", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 177.920101619592742, -17.14979850360189 ], [ 178.697177550619955, -17.100522188212071 ], [ 179.474253481647168, -17.053867486482115 ], [ -179.74867058732562, -17.004197778897627 ], [ -178.971594656298407, -16.959481244370728 ], [ -178.194518725271195, -16.911697858015476 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T008", "vessel": "Taveuni II", "survey_date": "2025-08-12", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 179.401134548056262, -17.934415957637167 ], [ 179.67454249485786, -17.899654147497863 ], [ 179.947950441659486, -17.858130727687527 ], [ -179.778641611538916, -17.811143619533269 ], [ -179.50523366473729, -17.768768396969666 ], [ -179.231825717935692, -17.724708363613253 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T009", "vessel": "Vanua I", "survey_date": "2025-08-13", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 178.068433112639013, -16.657838811131668 ], [ 178.561564721122721, -16.689731320158675 ], [ 179.054696329606429, -16.72173241552732 ], [ 179.547827938090137, -16.748891897767297 ], [ -179.959040453426155, -16.775241845025921 ], [ -179.465908844942447, -16.799281899607834 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T010", "vessel": "Vanua I", "survey_date": "2025-08-15", "crosses_antimeridian_flag": true }, "geometry": { "type": "LineString", "coordinates": [ [ 177.602847396853463, -17.277026295424367 ], [ 179.128053569323441, -17.468285296635063 ], [ -179.346740258206609, -17.65419136085664 ], [ -177.82153408573663, -17.837325774502055 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T011", "vessel": "Taveuni II", "survey_date": "2025-08-20", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ -176.863797870333627, -17.7932433540029 ], [ -176.985724572324699, -17.81273654528005 ], [ -177.107651274315742, -17.824236921378098 ], [ -177.229577976306814, -17.835861250474697 ], [ -177.351504678297886, -17.857559546831705 ], [ -177.473431380288929, -17.871769203842408 ], [ -177.595358082280001, -17.890225470404214 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T012", "vessel": "Taveuni II", "survey_date": "2025-08-20", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ 176.312195951017685, -17.976122209608448 ], [ 176.362236324041447, -17.722210270969462 ], [ 176.412276697065209, -17.470306839752467 ], [ 176.462317070088972, -17.226475769084448 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T013", "vessel": "Bligh", "survey_date": "2025-08-19", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ -176.70823848972006, -17.696306145080879 ], [ -177.205026853648661, -17.960082567744127 ], [ -177.701815217577291, -18.210737758337459 ], [ -178.198603581505893, -18.469124252379427 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T014", "vessel": "Lomaiviti", "survey_date": "2025-08-13", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ 176.538496893308832, -16.594358164012139 ], [ 176.861572490827996, -16.71494251806207 ], [ 177.184648088347188, -16.832251271240672 ], [ 177.507723685866381, -16.976116506165042 ], [ 177.830799283385545, -17.090939089364483 ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T015", "vessel": "Vanua I", "survey_date": "2025-08-15", "crosses_antimeridian_flag": false }, "geometry": { "type": "LineString", "coordinates": [ [ -177.278377449219306, -16.724372731501912 ], [ -177.470251917030794, -16.761535677586746 ], [ -177.662126384842253, -16.787182414326686 ], [ -177.854000852653712, -16.818799270133983 ], [ -178.0458753204652, -16.856282470705128 ], [ -178.237749788276659, -16.891308864567598 ], [ -178.429624256088147, -16.914729063096839 ] ] } },
0:35
Write /work/solve.py
content · 238 lines · py
import geopandas as gpd
import pandas as pd
from shapely.geometry import LineString, MultiLineString
import numpy as np

def fix_antimeridian_line(coords):
    """
    Splits a LineString that crosses the antimeridian into multiple LineStrings.
    Coords are (lon, lat).
    """
    if not coords:
        return None
    
    new_lines = []
    current_segment = [coords[0]]
    
    for i in range(1, len(coords)):
        prev_lon = coords[i-1][0]
        curr_lon = coords[i][0]
        
        # Check if there's a jump across the antimeridian
        # If the difference is greater than 180, it's likely a wrap-around
        if abs(curr_lon - prev_lon) > 180:
            # Close the current segment
            if len(current_segment) >= 2:
                new_lines.append(LineString(current_segment))
            
            # Start new segment. 
            # We need to adjust the longitude so it's continuous in the new coordinate space.
            # However, for the purpose of just 'splitting', we can just start with the new coord.
            # But wait, if we want the new segment to be 'local', we should shift all subsequent 
            # coordinates by 360 or -360.
            
            # Let's determine the shift
            shift = 360 if curr_lon < prev_lon else -360
            
            # Instead of complex logic, let's just split and later we'll handle the projection.
            # Actually, the simplest way to handle antimeridian in GeoPandas/Shapely is to 
            # split the line at the jump and adjust the longitude of the new segment.
            
            # But a better way is to shift all longitudes so they are continuous.
            # Let's try this:
            current_segment = []
            # Find the jump
            # Re-evaluating: if we shift the coordinates, we can treat it as a continuous line.
            pass 

    # Let's use a more robust approach for antimeridian splitting.
    return None

def split_antimeridian(line):
    """
    Splits a LineString into multiple LineStrings if it crosses the antimeridian.
    """
    coords = list(line.coords)
    if not coords:
        return [line]
    
    segments = []
    current_segment = [coords[0]]
    
    for i in range(1, len(coords)):
        prev_lon = coords[i-1][0]
        curr_lon = coords[i][0]
        
        # Detect jump
        if abs(curr_lon - prev_lon) > 180:
            # Split!
            if len(current_segment) >= 2:
                segments.append(LineString(current_segment))
            
            # Determine the direction of the jump
            # If prev was 179 and curr is -179, jump is positive (towards 180)
            # If prev was -179 and curr is 179, jump is negative (towards -180)
            # We adjust the longitude of the new segment to be continuous.
            
            # Let's calculate the shift required to make curr_lon continuous with prev_lon
            if curr_lon < prev_lon:
                # Jump from + to - (e.g., 179 -> -179)
                # We want to add 360 to the new coordinates to make them 181, 182...
                # Or rather, we want to keep the geometry "local" but consistent.
                # Actually, if we are going to project to Fiji metric grid (FMG), 
                # we should probably just handle the wrap-around by shifting.
                pass
            
            # Let's simplify: 
            # 1. Normalize all longitudes to a continuous range relative to the first point.
            # 2. Then split the line when the longitude jumps.
            pass

def robust_split_antimeridian(line):
    coords = list(line.coords)
    if len(coords) < 2:
        return [line]
    
    # Step 1: Make longitudes continuous
    normalized_coords = [coords[0]]
    for i in range(1, len(coords)):
        prev_lon, prev_lat = normalized_coords[i-1]
        curr_lon, curr_lat = coords[i]
        
        diff = curr_lon - prev_lon
        if diff > 180:
            diff -= 360
        elif diff < -180:
            diff += 360
        
        normalized_coords.append((prev_lon + diff, curr_lat))
    
    # Step 2: Split into segments where the jump was > 180 (in the original coords)
    # Actually, we can just split where the 'diff' was significant or where the original
    # jump was detected.
    
    segments = []
    current_seg = [normalized_coords[0]]
    
    for i in range(1, len(normalized_coords)):
        prev_orig_lon = coords[i-1][0]
        curr_orig_lon = coords[i][0]
        
        if abs(curr_orig_lon - prev_orig_lon) > 180:
            # Split
            if len(current_seg) >= 2:
                segments.append(LineString(current_seg))
            current_seg = [normalized_coords[i]]
        else:
            current_seg.append(normalized_coords[i])
            
    if len(current_seg) >= 2:
        segments.append(LineString(current_seg))
    
    # If no split happened, just return the original line (in its original lon range)
    # Wait, if we return normalized_coords, they might be outside [-180, 180].
    # But that's fine for projection.
    
    if not segments:
        return [line]
    
    # We need to return segments that are valid (can be projected).
    # The normalized_coords approach is good.
    return segments

# Let's refine the process
def process_transects(input_file, output_file):
    gdf = gpd.read_file(input_file)
    
    # Fiji National Grid (FMG) is often a UTM variant or specific local projection.
    # Checking common Fiji projections. Fiji uses Fiji Grid (FMG) or UTM zones 59S/60S.
    # The prompt says "Fiji's national metric grid". 
    # Usually, this refers to Fiji Grid (EPSG:2977 or similar) or just UTM.
    # Let's look for EPSG:2977 (Fiji Grid).
    # Actually, let's check common ones. 
    # EPSG:2977 is "Fiji Grid".
    target_crs = "EPSG:2977" 
    
    # We will create a new list of features
    new_features = []
    
    for _, row in gdf.iterrows():
        geom = row.geometry
        transect_id = row['transect_id']
        vessel = row['vessel']
        survey_date = row['survey_date']
        
        if geom.geom_type == 'LineString':
            segments = robust_split_antimeridian(geom)
        elif geom.geom_type == 'MultiLineString':
            # If it's already a MultiLineString, we process each part
            segments = []
            for part in geom.geoms:
                segments.extend(robust_split_antimeridian(part))
        else:
            segments = [geom]
            
        # Now we have a list of LineStrings (some might be "normalized" outside -180/180)
        # Create a MultiLineString from these segments
        # Wait, the prompt says "give me each transect as one MultiLineString feature".
        # So for each input row, we get ONE output row.
        
        # BUT, we must project them to FMG first.
        # If we project normalized coords, it should work.
        
        # Create a temporary GeoDataFrame for the segments of this transect
        temp_gdf = gpd.GeoDataFrame({'geometry': segments}, crs="EPSG:4326")
        
        # If the normalized segments are outside [-180, 180], 
        # we might need to wrap them before projection if the CRS doesn't handle it.
        # However, standard projection (pyproj) usually handles longitudes > 180.
        # Let's try.
        
        # Project to FMG
        temp_gdf = temp_gdf.to_crs(target_crs)
        
        # Combine segments into a single MultiLineString
        combined_geom = temp_gdf.geometry.unary_union
        
        # Ensure it's a MultiLineString (unary_union might return LineString if only one segment)
        if combined_geom.geom_type == 'LineString':
            combined_geom = MultiLineString([combined_geom])
        elif combined_geom.geom_type != 'MultiLineString':
            # This handles cases where it might be a GeometryCollection
            # but for our purposes, it should be LineStrings.
            # Let's just force it.
            from shapely.geometry import MultiLineString
            if combined_geom.geom_type == 'GeometryCollection':
                lines = [g for g in combined_geom.geoms if g.geom_type in ['LineString', 'MultiLineString']]
                # Flatten if they are MultiLineStrings
                all_lines = []
                for l in lines:
                    if l.geom_type == 'LineString':
                        all_lines.append(l)
                    else:
                        all_lines.extend(list(l.geoms))
                combined_geom = MultiLineString(all_lines)
        
        # Calculate length in metres
        length_m = combined_geom.length
        
        # Prepare new feature properties
        new_props = row.to_dict()
        new_props['length_m'] = length_m
        # Remove the old geometry to avoid conflict during creation
        if 'geometry' in new_props:
            del new_props['geometry']
            
        new_features.append({
            'type': 'Feature',
            'properties': new_props,
            'geometry': combined_geom
        })
        
    # Create the final GeoDataFrame
    output_gdf = gpd.GeoDataFrame.from_features(new_features, crs=target_crs)
    output_gdf.to_file(output_file, driver='GeoJSON')

if __name__ == "__main__":
    process_transects('fiji_transects_wgs84.geojson', 'fiji_transects_fmg.geojson')
Wrote /work/solve.py (9399 chars).
0:37
Bash python /work/solve.py
python /work/solve.py
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
  combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
  combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
  combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
  combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
  combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
  combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
  combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
  combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
  combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
  combined_geom = temp_gdf.geometry.unary_union
/work/solve.py:195: DeprecationWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
  combined_geom = temp_gdf.geometry.unary_union
Traceback (most recent call last):
  File "/work/solve.py", line 237, in <module>
    process_transects('fiji_transects_wgs84.geojson', 'fiji_transects_fmg.geojson')
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/work/solve.py", line 199, in process_transects
    combined_geom = MultiLineString([combined_geom])
                    ^^^^^^^^^^^^^^^
UnboundLocalError: cannot access local variable 'MultiLineString' where it is not associated with a value
0:40
Edit /work/solve.py
old_string and new_string are identical; no edit to make
0:47
Edit /work/solve.py
Edited /work/solve.py.
1:08
Edit /work/solve.py
old_string not found in /work/solve.py
1:09
Read /work/solve.py
     1	import geopandas as gpd
     2	import pandas as pd
     3	from shapely.geometry import LineString, MultiLineString, GeometryCollection
     4	import numpy as np
     5	
     6	def fix_antimeridian_line(coords):
     7	    """
     8	    Splits a LineString that crosses the antimeridian into multiple LineStrings.
     9	    Coords are (lon, lat).
    10	    """
    11	    if not coords:
    12	        return None
    13	    
    14	    new_lines = []
    15	    current_segment = [coords[0]]
    16	    
    17	    for i in range(1, len(coords)):
    18	        prev_lon = coords[i-1][0]
    19	        curr_lon = coords[i][0]
    20	        
    21	        # Check if there's a jump across the antimeridian
    22	        # If the difference is greater than 180, it's likely a wrap-around
    23	        if abs(curr_lon - prev_lon) > 180:
    24	            # Close the current segment
    25	            if len(current_segment) >= 2:
    26	                new_lines.append(LineString(current_segment))
    27	            
    28	            # Start new segment. 
    29	            # We need to adjust the longitude so it's continuous in the new coordinate space.
    30	            # However, for the purpose of just 'splitting', we can just start with the new coord.
    31	            # But wait, if we want the new segment to be 'local', we should shift all subsequent 
    32	            # coordinates by 360 or -360.
    33	            
    34	            # Let's determine the shift
    35	            shift = 360 if curr_lon < prev_lon else -360
    36	            
    37	            # Instead of complex logic, let's just split and later we'll handle the projection.
    38	            # Actually, the simplest way to handle antimeridian in GeoPandas/Shapely is to 
    39	            # split the line at the jump and adjust the longitude of the new segment.
    40	            
    41	            # But a better way is to shift all longitudes so they are continuous.
    42	            # Let's try this:
    43	            current_segment = []
    44	            # Find the jump
    45	            # Re-evaluating: if we shift the coordinates, we can treat it as a continuous line.
    46	            pass 
    47	
    48	    # Let's use a more robust approach for antimeridian splitting.
    49	    return None
    50	
    51	def split_antimeridian(line):
    52	    """
    53	    Splits a LineString into multiple LineStrings if it crosses the antimeridian.
    54	    """
    55	    coords = list(line.coords)
    56	    if not coords:
    57	        return [line]
    58	    
    59	    segments = []
    60	    current_segment = [coords[0]]
    61	    
    62	    for i in range(1, len(coords)):
    63	        prev_lon = coords[i-1][0]
    64	        curr_lon = coords[i][0]
    65	        
    66	        # Detect jump
    67	        if abs(curr_lon - prev_lon) > 180:
    68	            # Split!
    69	            if len(current_segment) >= 2:
    70	                segments.append(LineString(current_segment))
    71	            
    72	            # Determine the direction of the jump
    73	            # If prev was 179 and curr is -179, jump is positive (towards 180)
    74	            # If prev was -179 and curr is 179, jump is negative (towards -180)
    75	            # We adjust the longitude of the new segment to be continuous.
    76	            
    77	            # Let's calculate the shift required to make curr_lon continuous with prev_lon
    78	            if curr_lon < prev_lon:
    79	                # Jump from + to - (e.g., 179 -> -179)
    80	                # We want to add 360 to the new coordinates to make them 181, 182...
    81	                # Or rather, we want to keep the geometry "local" but consistent.
    82	                # Actually, if we are going to project to Fiji metric grid (FMG), 
    83	                # we should probably just handle the wrap-around by shifting.
    84	                pass
    85	            
    86	            # Let's simplify: 
    87	            # 1. Normalize all longitudes to a continuous range relative to the first point.
    88	            # 2. Then split the line when the longitude jumps.
    89	            pass
    90	
    91	def robust_split_antimeridian(line):
    92	    coords = list(line.coords)
    93	    if len(coords) < 2:
    94	        return [line]
    95	    
    96	    # Step 1: Make longitudes continuous
    97	    normalized_coords = [coords[0]]
    98	    for i in range(1, len(coords)):
    99	        prev_lon, prev_lat = normalized_coords[i-1]
   100	        curr_lon, curr_lat = coords[i]
   101	        
   102	        diff = curr_lon - prev_lon
   103	        if diff > 180:
   104	            diff -= 360
   105	        elif diff < -180:
   106	            diff += 360
   107	        
   108	        normalized_coords.append((prev_lon + diff, curr_lat))
   109	    
   110	    # Step 2: Split into segments where the jump was > 180 (in the original coords)
   111	    # Actually, we can just split where the 'diff' was significant or where the original
   112	    # jump was detected.
   113	    
   114	    segments = []
   115	    current_seg = [normalized_coords[0]]
   116	    
   117	    for i in range(1, len(normalized_coords)):
   118	        prev_orig_lon = coords[i-1][0]
   119	        curr_orig_lon = coords[i][0]
   120	        
   121	        if abs(curr_orig_lon - prev_orig_lon) > 180:
   122	            # Split
   123	            if len(current_seg) >= 2:
   124	                segments.append(LineString(current_seg))
   125	            current_seg = [normalized_coords[i]]
   126	        else:
   127	            current_seg.append(normalized_coords[i])
   128	            
   129	    if len(current_seg) >= 2:
   130	        segments.append(LineString(current_seg))
   131	    
   132	    # If no split happened, just return the original line (in its original lon range)
   133	    # Wait, if we return normalized_coords, they might be outside [-180, 180].
   134	    # But that's fine for projection.
   135	    
   136	    if not segments:
   137	        return [line]
   138	    
   139	    # We need to return segments that are valid (can be projected).
   140	    # The normalized_coords approach is good.
   141	    return segments
   142	
   143	# Let's refine the process
   144	def process_transects(input_file, output_file):
   145	    gdf = gpd.read_file(input_file)
   146	    
   147	    # Fiji National Grid (FMG) is often a UTM variant or specific local projection.
   148	    # Checking common Fiji projections. Fiji uses Fiji Grid (FMG) or UTM zones 59S/60S.
   149	    # The prompt says "Fiji's national metric grid". 
   150	    # Usually, this refers to Fiji Grid (EPSG:2977 or similar) or just UTM.
   151	    # Let's look for EPSG:2977 (Fiji Grid).
   152	    # Actually, let's check common ones. 
   153	    # EPSG:2977 is "Fiji Grid".
   154	    target_crs = "EPSG:2977" 
   155	    
   156	    # We will create a new list of features
   157	    new_features = []
   158	    
   159	    for _, row in gdf.iterrows():
   160	        geom = row.geometry
   161	        transect_id = row['transect_id']
   162	        vessel = row['vessel']
   163	        survey_date = row['survey_date']
   164	        
   165	        if geom.geom_type == 'LineString':
   166	            segments = robust_split_antimeridian(geom)
   167	        elif geom.geom_type == 'MultiLineString':
   168	            # If it's already a MultiLineString, we process each part
   169	            segments = []
   170	            for part in geom.geoms:
   171	                segments.extend(robust_split_antimeridian(part))
   172	        else:
   173	            segments = [geom]
   174	            
   175	        # Now we have a list of LineStrings (some might be "normalized" outside -180/180)
   176	        # Create a MultiLineString from these segments
   177	        # Wait, the prompt says "give me each transect as one MultiLineString feature".
   178	        # So for each input row, we get ONE output row.
   179	        
   180	        # BUT, we must project them to FMG first.
   181	        # If we project normalized coords, it should work.
   182	        
   183	        # Create a temporary GeoDataFrame for the segments of this transect
   184	        temp_gdf = gpd.GeoDataFrame({'geometry': segments}, crs="EPSG:4326")
   185	        
   186	        # If the normalized segments are outside [-180, 180], 
   187	        # we might need to wrap them before projection if the CRS doesn't handle it.
   188	        # However, standard projection (pyproj) usually handles longitudes > 180.
   189	        # Let's try.
   190	        
   191	        # Project to FMG
   192	        temp_gdf = temp_gdf.to_crs(target_crs)
   193	        
   194	        # Combine segments into a single MultiLineString
   195	        combined_geom = temp_gdf.geometry.unary_union
   196	        
   197	        # Ensure it's a MultiLineString (unary_union might return LineString if only one segment)
   198	        if combined_geom.geom_type == 'LineString':
   199	            combined_geom = MultiLineString([combined_geom])
   200	        elif combined_geom.geom_type != 'MultiLineString':
   201	            # This handles cases where it might be a GeometryCollection
   202	            # but for our purposes, it should be LineStrings.
   203	            # Let's just force it.
   204	            from shapely.geometry import MultiLineString
   205	            if combined_geom.geom_type == 'GeometryCollection':
   206	                lines = [g for g in combined_geom.geoms if g.geom_type in ['LineString', 'MultiLineString']]
   207	                # Flatten if they are MultiLineStrings
   208	                all_lines = []
   209	                for l in lines:
   210	                    if l.geom_type == 'LineString':
   211	                        all_lines.append(l)
   212	                    else:
   213	                        all_lines.extend(list(l.geoms))
   214	                combined_geom = MultiLineString(all_lines)
   215	        
   216	        # Calculate length in metres
   217	        length_m = combined_geom.length
   218	        
   219	        # Prepare new feature properties
   220	        new_props = row.to_dict()
   221	        new_props['length_m'] = length_m
   222	        # Remove the old geometry to avoid conflict during creation
   223	        if 'geometry' in new_props:
   224	            del new_props['geometry']
   225	            
   226	        new_features.append({
   227	            'type': 'Feature',
   228	            'properties': new_props,
   229	            'geometry': combined_geom
   230	        })
   231	        
   232	    # Create the final GeoDataFrame
   233	    output_gdf = gpd.GeoDataFrame.from_features(new_features, crs=target_crs)
   234	    output_gdf.to_file(output_file, driver='GeoJSON')
   235	
   236	if __name__ == "__main__":
   237	    process_transects('fiji_transects_wgs84.geojson', 'fiji_transects_fmg.geojson')
1:19
Write /work/solve.py
content · 119 lines · py
import geopandas as gpd
import pandas as pd
from shapely.geometry import LineString, MultiLineString, GeometryCollection
import numpy as np

def robust_split_antimeridian(line):
    """
    Splits a LineString into multiple LineStrings if it crosses the antimeridian.
    """
    coords = list(line.coords)
    if len(coords) < 2:
        return [line]
    
    # Step 1: Make longitudes continuous
    normalized_coords = [coords[0]]
    for i in range(1, len(coords)):
        prev_lon, prev_lat = normalized_coords[i-1]
        curr_lon, curr_lat = coords[i]
        
        diff = curr_lon - prev_lon
        if diff > 180:
            diff -= 360
        elif diff < -180:
            diff += 360
        
        normalized_coords.append((prev_lon + diff, curr_lat))
    
    # Step 2: Split into segments where the jump was detected
    segments = []
    current_seg = [normalized_coords[0]]
    
    for i in range(1, len(normalized_coords)):
        prev_orig_lon = coords[i-1][0]
        curr_orig_lon = coords[i][0]
        
        # Detect if the original jump was an antimeridian crossing
        if abs(curr_orig_lon - prev_orig_lon) > 180:
            # Split
            if len(current_seg) >= 2:
                segments.append(LineString(current_seg))
            current_seg = [normalized_coords[i]]
        else:
            current_seg.append(normalized_coords[i])
            
    if len(current_seg) >= 2:
        segments.append(LineString(current_seg))
    
    return segments if segments else [line]

def process_transects(input_file, output_file):
    gdf = gpd.read_file(input_file)
    
    # Fiji Grid (FMG) - EPSG:2977
    target_crs = "EPSG:2977" 
    
    new_features = []
    
    for _, row in gdf.iterrows():
        geom = row.geometry
        
        if geom.geom_type == 'LineString':
            segments = robust_split_antimeridian(geom)
        elif geom.geom_type == 'MultiLineString':
            segments = []
            for part in geom.geoms:
                segments.extend(robust_split_antimeridian(part))
        else:
            segments = [geom]
            
        # Create a temporary GeoDataFrame for the segments of this transect
        # IMPORTANT: Since the segments might have longitudes > 180 or < -180,
        # we still define them as EPSG:4326. Proj should handle it.
        temp_gdf = gpd.GeoDataFrame({'geometry': segments}, crs="EPSG:4326")
        
        # Project to FMG
        temp_gdf = temp_gdf.to_crs(target_crs)
        
        # Combine segments into a single geometry
        # unary_union can return LineString or MultiLineString
        combined_geom = temp_gdf.geometry.union_all()
        
        # Ensure it's a MultiLineString
        if combined_geom.geom_type == 'LineString':
            combined_geom = MultiLineString([combined_geom])
        elif combined_geom.geom_type == 'GeometryCollection':
            # Extract only lines
            lines = []
            for g in combined_geom.geoms:
                if g.geom_type == 'LineString':
                    lines.append(g)
                elif g.geom_type == 'MultiLineString':
                    lines.extend(list(g.geoms))
            combined_geom = MultiLineString(lines)
        elif combined_geom.geom_type != 'MultiLineString':
             # Fallback for safety
             combined_geom = MultiLineString([combined_geom])

        # Calculate length in metres
        length_m = combined_geom.length
        
        # Prepare new feature properties
        new_props = row.to_dict()
        new_props['length_m'] = length_m
        if 'geometry' in new_props:
            del new_props['geometry']
            
        new_features.append({
            'type': 'Feature',
            'properties': new_props,
            'geometry': combined_geom
        })
        
    # Create the final GeoDataFrame
    output_gdf = gpd.GeoDataFrame.from_features(new_features, crs=target_crs)
    output_gdf.to_file(output_file, driver='GeoJSON')

if __name__ == "__main__":
    process_transects('fiji_transects_wgs84.geojson', 'fiji_transects_fmg.geojson')
Wrote /work/solve.py (4117 chars).
1:22
Bash python /work/solve.py
python /work/solve.py
(no output)
1:27
Bash head -n 20 fiji_transects_fmg.geojson
head -n 20 fiji_transects_fmg.geojson
{
"type": "FeatureCollection",
"name": "fiji_transects_fmg",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::2977" } },
"features": [
{ "type": "Feature", "properties": { "transect_id": "T001", "vessel": "Taveuni II", "survey_date": "2025-08-15T00:00:00", "crosses_antimeridian_flag": true, "length_m": 242148.17462422466 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2577818.856368530075997, 7808045.933654122054577 ], [ -2517744.839244871865958, 7818720.514956817030907 ], [ -2457959.618174184113741, 7829649.209022815339267 ] ], [ [ -2398415.969535391777754, 7840591.110509935766459 ], [ -2339097.414767996408045, 7851489.390455324202776 ], [ -2279720.962942073587328, 7860430.453650631010532 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T002", "vessel": "Bligh", "survey_date": "2025-08-12T00:00:00", "crosses_antimeridian_flag": true, "length_m": 160320.48503040406 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2568571.367820397950709, 7846831.475579218938947 ], [ -2494815.333217027131468, 7878702.454537282697856 ] ], [ [ -2421194.358998955693096, 7910035.447528526186943 ], [ -2347860.872700769919902, 7941939.787312176078558 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T003", "vessel": "Lomaiviti", "survey_date": "2025-08-12T00:00:00", "crosses_antimeridian_flag": true, "length_m": 294940.8152454049 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2737259.021241321694106, 7816272.578033835627139 ], [ -2638431.858519812580198, 7825394.063890357501805 ], [ -2540319.581941521726549, 7834153.894859520718455 ] ], [ [ -2442858.046751122921705, 7842329.020383344963193 ], [ -2345905.603365985210985, 7849135.118691245093942 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T004", "vessel": "Vanua I", "survey_date": "2025-08-15T00:00:00", "crosses_antimeridian_flag": true, "length_m": 329398.56369995116 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2582407.215880405623466, 7785839.181658865883946 ], [ -2508233.587048316374421, 7823377.507483461871743 ] ], [ [ -2433906.98726980574429, 7858842.509372072294354 ], [ -2359990.324205769691616, 7895939.639540609903634 ], [ -2285968.765630466397852, 7931271.657026222907007 ], [ -2211945.76174630690366, 7965472.228640222921968 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T005", "vessel": "Bligh", "survey_date": "2025-08-19T00:00:00", "crosses_antimeridian_flag": true, "length_m": 337273.70270080183 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2730085.261179696302861, 7745776.327364278957248 ], [ -2661819.787075642962009, 7746489.832580281421542 ], [ -2593883.202560970094055, 7746760.809876747429371 ], [ -2526561.439846133813262, 7748375.548134338110685 ], [ -2459207.692624073475599, 7747464.851115341298282 ] ], [ [ -2392326.301384323276579, 7747142.532282743602991 ], [ -2325963.332817469257861, 7747812.099709572270513 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T006", "vessel": "Cakaulevu", "survey_date": "2025-08-13T00:00:00", "crosses_antimeridian_flag": true, "length_m": 362346.84209049615 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2599992.844046104233712, 7710152.494721526280046 ], [ -2535990.336608688347042, 7745663.094245363026857 ], [ -2471907.25352796446532, 7780096.101796898059547 ] ], [ [ -2408021.86162570444867, 7815198.070424247533083 ], [ -2343992.843024155590683, 7848789.023065808229148 ], [ -2280128.756940491497517, 7882913.996305481530726 ], [ -2216075.889947671908885, 7915112.088575566187501 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T007", "vessel": "Vanua I", "survey_date": "2025-08-19T00:00:00", "crosses_antimeridian_flag": true, "length_m": 368700.1434123707 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2707958.766985294874758, 7847662.46371332090348 ], [ -2617025.080956507474184, 7868560.6145184263587 ], [ -2526493.880338669288903, 7888512.674169298261404 ] ], [ [ -2436457.20139694865793, 7908226.102616477757692 ], [ -2346756.589748611208051, 7926750.916424177587032 ], [ -2257520.004844188690186, 7945076.104863223619759 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T008", "vessel": "Taveuni II", "survey_date": "2025-08-12T00:00:00", "crosses_antimeridian_flag": true, "length_m": 130059.39143118286 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2518182.288068630266935, 7779851.174654245376587 ], [ -2487017.445418053306639, 7789178.679573908448219 ], [ -2456022.297383859753609, 7799241.849767371080816 ] ], [ [ -2425166.019995928741992, 7809880.026913054287434 ], [ -2394257.700750786811113, 7819872.499677928164601 ], [ -2363415.248306550085545, 7829983.100506925955415 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T009", "vessel": "Vanua I", "survey_date": "2025-08-13T00:00:00", "crosses_antimeridian_flag": true, "length_m": 235691.09624016113 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2700142.167306652758271, 7911204.870228514075279 ], [ -2640985.695807782001793, 7916482.104912839829922 ], [ -2582072.975177893880755, 7921554.017558207735419 ], [ -2523489.242259116843343, 7927027.587514635175467 ] ], [ [ -2465150.952466951683164, 7932409.003623221069574 ], [ -2407078.076770232990384, 7937881.784215446561575 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T010", "vessel": "Vanua I", "survey_date": "2025-08-15T00:00:00", "crosses_antimeridian_flag": true, "length_m": 362782.57626249874 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2743004.244949294254184, 7825703.487932769581676 ], [ -2559206.512722389306873, 7831590.124320032075047 ] ], [ [ -2377995.683071207255125, 7836498.912986122071743 ], [ -2199142.433008896652609, 7840154.2901915544644 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T011", "vessel": "Taveuni II", "survey_date": "2025-08-20T00:00:00", "crosses_antimeridian_flag": false, "length_m": 85100.654515888978 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2090510.237545256037265, 7860563.718294717371464 ], [ -2104083.355092440266162, 7856360.095066314563155 ], [ -2117792.864098150283098, 7853092.239760281518102 ], [ -2131508.181665613316, 7849795.275641735643148 ], [ -2145064.558770742733032, 7845283.005933348089457 ], [ -2158751.349236745852977, 7841647.831423315219581 ], [ -2172374.122675319202244, 7837490.689390176907182 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T012", "vessel": "Taveuni II", "survey_date": "2025-08-20T00:00:00", "crosses_antimeridian_flag": false, "length_m": 96678.572314157165 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2881358.679590006358922, 7711997.136072006076574 ], [ -2881147.548506676219404, 7744723.712745074182749 ], [ -2880792.575384852476418, 7777199.373172726482153 ], [ -2880163.700538771227002, 7808666.804016925394535 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T013", "vessel": "Bligh", "survey_date": "2025-08-19T00:00:00", "crosses_antimeridian_flag": false, "length_m": 195242.20241007773 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2074347.516229145694524, 7874463.33793031796813 ], [ -2126661.583137985318899, 7835388.280761742964387 ], [ -2179084.771710069850087, 7797497.99072393681854 ], [ -2231266.005162506829947, 7758301.353639220818877 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T014", "vessel": "Lomaiviti", "survey_date": "2025-08-13T00:00:00", "crosses_antimeridian_flag": false, "length_m": 168768.7882640101 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2884554.542054513935, 7889146.833948561921716 ], [ -2843184.396975137759, 7880624.016131930053234 ], [ -2802044.054834145121276, 7872489.953316772356629 ], [ -2760519.013364502228796, 7861046.430769282393157 ], [ -2719749.847674670163542, 7853185.46901720110327 ] ] ] } },
{ "type": "Feature", "properties": { "transect_id": "T015", "vessel": "Vanua I", "survey_date": "2025-08-15T00:00:00", "crosses_antimeridian_flag": false, "length_m": 136075.16315041631 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ -2154859.157849435694516, 7981607.483935452997684 ], [ -2176367.295431756414473, 7974281.162554035894573 ], [ -2198072.658420251682401, 7968293.111701854504645 ], [ -2219700.689059948083013, 7961552.54660858400166 ], [ -2241249.826771907042712, 7954069.077079316601157 ], [ -2262853.85095702810213, 7946839.612698352895677 ], [ -2284666.763064748141915, 7940965.200932309031487 ] ] ] } },
3:04