crs-l2-fiji-antimeridian
Map (reference ← swipe → agent)
0:00
I'm pulling together per-transect coverage figures for our survey report, but some of the lines in fiji_transects_wgs84.geojson render as arcs spanning the entire globe instead of short local segments. Can you fix the geometries so they render correctly, convert them to Fiji's national metric grid, and give me each transect as one MultiLineString feature with a length_m attribute in metres?
Every feature must come out as a MultiLineString, so do not leave any transects as plain LineString, and the problematic ones must end up as multi-part geometries that faithfully represent the actual survey path. Carry the survey attributes (vessel and survey_date) through unchanged, use transect_id as the key field, and write the result as GeoJSON named fiji_transects_fmg.geojson.
0:00
System info
{
"adapter": "openrouter",
"model": "google/gemma-4-26b-a4b-it",
"base_url": "https://openrouter.ai/api/v1"
}
0:01
Bash
ls -F
ls -F
fiji_transects_wgs84.geojson
0:08
Write
/work/inspect.py
content · 16 lines · py
import geopandas as gpd
def inspect_geojson(file_path):
gdf = gpd.read_file(file_path)
print("Columns:", gdf.columns.tolist())
print("\nCRS:", gdf.crs)
print("\nGeometry types:", gdf.geometry.type.unique())
print("\nFirst few rows:")
print(gdf.head())
print("\nGeometry sample (to check for long arcs):")
for idx, row in gdf.head(5).iterrows():
print(f"ID: {row.get('transect_id', idx)}, Type: {row.geometry.type}, Bounds: {row.geometry.bounds}")
if __name__ == "__main__":
inspect_geojson('fiji_transects_wgs84.geojson')
Wrote /work/inspect.py (568 chars).
0:09
Bash
python /work/inspect.py
python /work/inspect.py
Traceback (most recent call last):
File "/work/inspect.py", line 1, in <module>
import geopandas as gpd
File "/usr/local/lib/python3.14/site-packages/geopandas/__init__.py", line 3, in <module>
from geopandas.geoseries import GeoSeries
File "/usr/local/lib/python3.14/site-packages/geopandas/geoseries.py", line 7, in <module>
import numpy as np
File "/usr/local/lib/python3.14/site-packages/numpy/__init__.py", line 112, in <module>
from numpy.__config__ import show_config
File "/usr/local/lib/python3.14/site-packages/numpy/__config__.py", line 4, in <module>
from numpy._core._multiarray_umath import (
File "/usr/local/lib/python3.14/site-packages/numpy/_core/__init__.py", line 24, in <module>
from . import multiarray
File "/usr/local/lib/python3.14/site-packages/numpy/_core/multiarray.py", line 115, in <module>
@array_function_from_c_func_and_dispatcher(_multiarray_umath.empty_like)
File "/usr/local/lib/python3.14/site-packages/numpy/_core/overrides.py", line 185, in decorator
return array_function_dispatch(
File "/usr/local/lib/python3.14/site-packages/numpy/_core/overrides.py", line 161, in decorator
doc = inspect.cleandoc(dispatcher.__doc__)
AttributeError: module 'inspect' has no attribute 'cleandoc' (consider renaming '/work/inspect.py' since it has the same name as the standard library module named 'inspect' and prevents importing that standard library module)
0:22
Bash
mv /work/inspect.py /work/inspect_data.py
mv /work/inspect.py /work/inspect_data.py python /work/inspect_data.py
Columns: ['transect_id', 'vessel', 'survey_date', 'crosses_antimeridian_flag', 'geometry'] CRS: EPSG:4326 Geometry types: <ArrowStringArray> ['LineString'] Length: 1, dtype: str First few rows: transect_id ... geometry 0 T001 ... LINESTRING (178.94188 -17.6323, 179.45666 -17.... 1 T002 ... LINESTRING (179.07029 -17.33477, 179.72594 -17... 2 T003 ... LINESTRING (177.63695 -17.35884, 178.46282 -17... 3 T004 ... LINESTRING (178.8737 -17.8033, 179.54225 -17.5... 4 T005 ... LINESTRING (177.59692 -17.92326, 178.1607 -18.... [5 rows x 5 columns] Geometry sample (to check for long arcs): ID: T001, Type: LineString, Bounds: (-179.51379413260005, -17.63230181602077, 179.9714304385273, -17.577321872344196) ID: T002, Type: LineString, Bounds: (-179.61840306132, -17.33476798415071, 179.72594288174025, -16.83485040853485) ID: T003, Type: LineString, Bounds: (-179.88544804064506, -17.590577543375694, 179.2886837824894, -17.358837040696127) ID: T004, Type: LineString, Bounds: (-179.78918771936753, -17.803295238757038, 179.5422543310476, -16.795171196845544) ID: T005, Type: LineString, Bounds: (-179.58421575897069, -18.437390943572723, 179.8520123590089, -17.923255155112717)
/work/inspect_data.py:12: ShapelyDeprecationWarning: The 'type' attribute is deprecated, and will be removed in the future. You can use the 'geom_type' attribute instead.
print(f"ID: {row.get('transect_id', idx)}, Type: {row.geometry.type}, Bounds: {row.geometry.bounds}")
0:46
Write
/work/solve.py
content · 131 lines · py
import geopandas as gpd
from shapely.geometry import LineString, MultiLineString
from shapely.ops import split
import numpy as np
def fix_antimeridian_crossing(line, lon_boundary=180.0):
"""
Splits a LineString that crosses the antimeridian into multiple LineStrings.
"""
coords = list(line.coords)
new_segments = []
current_segment = []
for i in range(len(coords)):
if i > 0:
# Check for jump across antimeridian
# A jump is considered if the difference between consecutive longitudes
# is very large (e.g., > 180)
diff = coords[i][0] - coords[i-1][0]
if abs(diff) > 180:
# Close current segment
if current_segment:
new_segments.append(LineString(current_segment))
current_segment = []
# Handle the jump
# If diff > 0, it went from -180 area to +180 area
# If diff < 0, it went from +180 area to -180 area
# We adjust the longitude to be continuous in a local sense
if diff > 0:
# Jumped from e.g. 179 to -179 (actually the diff would be -358)
# Wait, if coords[i] is -179 and coords[i-1] is 179, diff is -358
# If diff < -180, it's a jump from positive to negative (wrapping around)
# We add 360 to the new coord to make it continuous for calculation,
# but we'll actually split it.
pass
# Actually, a better way for GeoJSON is to detect the jump and split.
# If we detect a jump, we start a new segment.
# To handle the wrapping, we can normalize the longitudes relative to the first point.
pass
# Let's rethink.
pass
def split_antimeridian_geometry(geom, lon_boundary=180.0):
"""
Correctly splits a LineString crossing the antimeridian.
"""
if geom.geom_type != 'LineString':
return geom
coords = list(geom.coords)
segments = []
current_coords = [coords[0]]
for i in range(1, len(coords)):
prev_lon = coords[i-1][0]
curr_lon = coords[i][0]
# Check if it crosses the antimeridian
# The jump is detected if the distance between longitudes is > 180
if abs(curr_lon - prev_lon) > 180:
# Split here
segments.append(LineString(current_coords))
current_coords = [coords[i]]
# Normalize the new segment's longitude relative to the previous one
# to keep it "close" in a coordinate sense, though for the final
# GeoJSON we want the actual WGS84 lon.
# However, a LineString that crosses the antimeridian is problematic.
# The task says "fix the geometries so they render correctly".
# Rendering correctly in most GIS software means splitting the line
# at the antimeridian.
else:
current_coords.append(coords[i])
if current_coords:
segments.append(LineString(current_coords))
if len(segments) == 1:
return segments[0]
else:
return MultiLineString(segments)
def process_transects(input_file, output_file):
gdf = gpd.read_file(input_file)
# 1. Fix antimeridian crossings
# We apply a function that returns a LineString or MultiLineString
gdf['geometry'] = gdf['geometry'].apply(lambda x: split_antimeridian_geometry(x))
# 2. Convert to Fiji's national metric grid (FMG)
# Searching for Fiji projected CRS. EPSG:2978 is Fiji Red Beach (often used).
# Let's try to find a suitable one or use a common one.
# Actually, the user says "Fiji's national metric grid".
# Usually, this refers to Fiji Grid (EPSG:2978 or similar).
# I'll use EPSG:2978 for Fiji Red Beach if available, or a standard UTM zone for Fiji.
# Fiji is roughly between 160E and 180E.
# Let's use EPSG:2978 (Fiji Red Beach) if it's the standard.
# If not, I'll use a generic projection or check.
# Actually, EPSG:2978 is indeed Fiji Red Beach.
target_crs = "EPSG:2978"
gdf = gdf.to_crs(target_crs)
# 3. Calculate length in metres
gdf['length_m'] = gdf.geometry.length
# 4. Ensure all are MultiLineString
def ensure_multilinestring(geom):
if geom.geom_type == 'LineString':
return MultiLineString([geom])
return geom
gdf['geometry'] = gdf['geometry'].apply(ensure_multilinestring)
# 5. Save to GeoJSON
# Keep only required columns (plus transect_id, vessel, survey_date, length_m)
# The prompt says "Carry the survey attributes (vessel and survey_date) through unchanged"
# and "use transect_id as the key field".
# It doesn't explicitly say to discard 'crosses_antimeridian_flag'.
# Usually, it's safer to keep all existing attributes unless told otherwise.
gdf.to_file(output_file, driver='GeoJSON')
print(f"Saved to {output_file}")
if __name__ == "__main__":
process_transects('fiji_transects_wgs84.geojson', 'fiji_transects_fmg.geojson')
Wrote /work/solve.py (5232 chars).
0:47
Bash
python /work/solve.py
python /work/solve.py
Saved to fiji_transects_fmg.geojson
0:53
Write
/work/inspect_output.py
content · 17 lines · py
import geopandas as gpd
def inspect_output(file_path):
gdf = gpd.read_file(file_path)
print("Columns:", gdf.columns.tolist())
print("\nCRS:", gdf.crs)
print("\nGeometry types:", gdf.geometry.type.unique())
print("\nFirst few rows:")
print(gdf.head())
print("\nChecking for length_m attribute:")
print(gdf[['transect_id', 'length_m']].head())
print("\nChecking geometry types specifically:")
print(gdf.geometry.apply(lambda x: x.geom_type).value_counts())
if __name__ == "__main__":
inspect_output('fiji_transects_fmg.geojson')
Wrote /work/inspect_output.py (573 chars).
0:54
Bash
python /work/inspect_output.py
python /work/inspect_output.py
Columns: ['transect_id', 'vessel', 'survey_date', 'crosses_antimeridian_flag', 'length_m', 'geometry'] CRS: EPSG:2978 Geometry types: <ArrowStringArray> ['MultiLineString'] Length: 1, dtype: str First few rows: transect_id ... geometry 0 T001 ... MULTILINESTRING ((-4057186.979 7501490.882, -3... 1 T002 ... MULTILINESTRING ((-4050666.36 7546185.035, -39... 2 T003 ... MULTILINESTRING ((-4238464.044 7494401.467, -4... 3 T004 ... MULTILINESTRING ((-4060091.6 7475969.833, -398... 4 T005 ... MULTILINESTRING ((-4222895.197 7415185.388, -4... [5 rows x 6 columns] Checking for length_m attribute: transect_id length_m 0 T001 272818.690274 1 T002 180927.646942 2 T003 333940.946315 3 T004 370592.214148 4 T005 381398.117761 Checking geometry types specifically: geometry MultiLineString 30 Name: count, dtype: int64
2:07
Write
/work/solve.py
content · 75 lines · py
import geopandas as gpd
from shapely.geometry import LineString, MultiLineString
def split_antimeridian_geometry(geom):
"""
Correctly splits a LineString crossing the antimeridian.
The input geom is in WGS84 (EPSG:4326).
"""
if geom.geom_type != 'LineString':
return geom
coords = list(geom.coords)
segments = []
current_coords = [coords[0]]
for i in range(1, len(coords)):
prev_lon = coords[i-1][0]
curr_lon = coords[i][0]
# Check if it crosses the antimeridian
# The jump is detected if the absolute difference is > 180
if abs(curr_lon - prev_lon) > 180:
# Split here
if current_coords:
segments.append(LineString(current_coords))
current_coords = [coords[i]]
else:
current_coords.append(coords[i])
if current_coords:
segments.append(LineString(current_coords))
if len(segments) == 1:
return segments[0]
else:
return MultiLineString(segments)
def ensure_multilinestring(geom):
"""
Ensures all geometries are MultiLineString.
"""
if geom.geom_type == 'LineString':
return MultiLineString([geom])
elif geom.geom_type == 'MultiLineString':
return geom
else:
# If it's something else, we'll try to handle it, but based on input it's LineString
return geom
def process_transects(input_file, output_file):
# Read input
gdf = gpd.read_file(input_file)
# 1. Fix antimeridian crossings in WGS84
# We must do this BEFORE projecting to metric, because the projection
# will interpret the long arcs as real paths.
gdf['geometry'] = gdf['geometry'].apply(split_antimeridian_geometry)
# 2. Convert to Fiji's national metric grid (Fiji Red Beach, EPSG:2978)
target_crs = "EPSG:2978"
gdf = gdf.to_crs(target_crs)
# 3. Calculate length in metres (now that we are in a metric CRS)
gdf['length_m'] = gdf.geometry.length
# 4. Ensure all are MultiLineString
gdf['geometry'] = gdf['geometry'].apply(ensure_multilinestring)
# 5. Save to GeoJSON
gdf.to_file(output_file, driver='GeoJSON')
print(f"Successfully processed and saved to {output_file}")
if __name__ == "__main__":
process_transects('fiji_transects_wgs84.geojson', 'fiji_transects_fmg.geojson')
Wrote /work/solve.py (2405 chars).
2:09
Bash
python /work/solve.py
python /work/solve.py
Successfully processed and saved to fiji_transects_fmg.geojson
2:11
Bash
python /work/inspect_output.py
python /work/inspect_output.py
Columns: ['transect_id', 'vessel', 'survey_date', 'crosses_antimeridian_flag', 'length_m', 'geometry'] CRS: EPSG:2978 Geometry types: <ArrowStringArray> ['MultiLineString'] Length: 1, dtype: str First few rows: transect_id ... geometry 0 T001 ... MULTILINESTRING ((-4057186.979 7501490.882, -3... 1 T002 ... MULTILINESTRING ((-4050666.36 7546185.035, -39... 2 T003 ... MULTILINESTRING ((-4238464.044 7494401.467, -4... 3 T004 ... MULTILINESTRING ((-4060091.6 7475969.833, -398... 4 T005 ... MULTILINESTRING ((-4222895.197 7415185.388, -4... [5 rows x 6 columns] Checking for length_m attribute: transect_id length_m 0 T001 272818.690274 1 T002 180927.646942 2 T003 333940.946315 3 T004 370592.214148 4 T005 381398.117761 Checking geometry types specifically: geometry MultiLineString 30 Name: count, dtype: int64
2:28