Initial commit — multi-tenant filtering, port constraints, chart bbox
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "lh-preview",
|
||||
"runtimeExecutable": "python",
|
||||
"runtimeArgs": ["-m", "http.server", "7722", "--directory", "C:\\Temp"],
|
||||
"port": 7722
|
||||
}
|
||||
]
|
||||
}
|
||||
+118
-1
@@ -159,7 +159,124 @@
|
||||
"WebFetch(domain:opencpn.org)",
|
||||
"WebFetch(domain:sailingissues.com)",
|
||||
"WebFetch(domain:raw.githubusercontent.com)",
|
||||
"WebFetch(domain:www.safe-skipper.com)"
|
||||
"WebFetch(domain:www.safe-skipper.com)",
|
||||
"PowerShell(dir \"C:\\\\Users\\\\aerom\\\\\" -Name | Where-Object { $_ -notmatch \"^\\\\.\" })",
|
||||
"Bash(python converter.py --help)",
|
||||
"Bash(python build_barranquilla.py)",
|
||||
"Bash(python check_s57.py dist/CO1CO01M/CO1CO01M.000)",
|
||||
"Bash(curl -s http://localhost:5000/charts/cells)",
|
||||
"Bash(echo \"EXIT: $?\")",
|
||||
"Bash(curl -s http://localhost:5503/charts/cells)",
|
||||
"Bash(curl -s -X POST http://localhost:5503/charts/rebuild)",
|
||||
"Bash(curl -s http://localhost:5503/charts/rebuild/BARRANQUILLA)",
|
||||
"Bash(curl -s -X POST \"http://localhost:5503/charts/cells/BARRANQUILLA/rebuild\")",
|
||||
"Bash(curl -s \"http://localhost:5503/charts/cells\")",
|
||||
"Bash(curl -s -o /tmp/features_test.json \"http://localhost:5503/charts/features\")",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit -m ' *)",
|
||||
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\([c['id'] for c in d]\\)\")",
|
||||
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:5503/charts/cells/BARRANQUILLA/features)",
|
||||
"Bash(curl -s http://localhost:5503/charts/cells/BARRANQUILLA)",
|
||||
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('status:', d.get\\('status'\\), 'count:', d.get\\('feature_count'\\)\\)\")",
|
||||
"Bash(curl -s http://localhost:5503/charts/features)",
|
||||
"Bash(curl -s -X POST http://localhost:5503/charts/rebuild-cache)",
|
||||
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); cells=d.get\\('rebuilt',[]\\); print\\(f'Rebuilt {len\\(cells\\)} cells'\\)\")",
|
||||
"Bash(node --check frontend/js/map.js)",
|
||||
"Bash(grep -v \"^$\")",
|
||||
"Bash(git checkout *)",
|
||||
"mcp__Claude_in_Chrome__tabs_context_mcp",
|
||||
"mcp__Claude_Preview__preview_start",
|
||||
"Bash(Get-Content \"D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\ui\\\\js\\\\colreg_ref.js\")",
|
||||
"Bash(Measure-Object -Line)",
|
||||
"Bash(Select-Object -ExpandProperty Lines)",
|
||||
"WebFetch(domain:www.gov.uk)",
|
||||
"WebFetch(domain:continuouswave.com)",
|
||||
"WebFetch(domain:charts.noaa.gov)",
|
||||
"WebFetch(domain:www.charts.noaa.gov)",
|
||||
"Bash(curl -s \"https://gis.charttools.noaa.gov/arcgis/rest/services/MCS/ENCOnline/MapServer/exts/MaritimeChartService/MapServer/0/query?where=1%3D1&geometry=%7B%22xmin%22%3A-80.5%2C%22ymin%22%3A25.5%2C%22xmax%22%3A-80.0%2C%22ymax%22%3A25.9%7D&geometryType=esriGeometryEnvelope&spatialRel=esriSpatialRelIntersects&outFields=CELL_NAME%2CCELL_TITLE%2CSCALE%2CEDITION&f=json\")",
|
||||
"Bash(curl -s \"https://gis.charttools.noaa.gov/arcgis/rest/services/MCS/ENCOnline/MapServer/0/query?where=1%3D1&geometry=%7B%22xmin%22%3A-80.5%2C%22ymin%22%3A25.5%2C%22xmax%22%3A-80.0%2C%22ymax%22%3A25.9%7D&geometryType=esriGeometryEnvelope&spatialRel=esriSpatialRelIntersects&outFields=CELL_NAME%2CCELL_TITLE%2CSCALE%2CEDITION&f=json\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); [print\\(f['attributes']\\) for f in d.get\\('features',[]\\)]\")",
|
||||
"Bash(curl -s \"https://encdirect.noaa.gov/arcgis/rest/services/encdirect/enc_cells/MapServer/0/query?where=1%3D1&geometry=-80.5,25.5,-80.0,25.9&geometryType=esriGeometryEnvelope&spatialRel=esriSpatialRelIntersects&outFields=*&f=json\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d, indent=2\\)\\)\")",
|
||||
"Bash(curl -s \"https://gis.charttools.noaa.gov/arcgis/rest/services/MCS/ENCOnline/MapServer/0?f=json\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(d.get\\('name',''\\), d.get\\('description',''\\)\\)\")",
|
||||
"Bash(curl -v \"https://gis.charttools.noaa.gov/arcgis/rest/services/MCS/ENCOnline/MapServer?f=json\")",
|
||||
"Bash(curl -s \"https://gis.charttools.noaa.gov/arcgis/rest/services?f=json\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); [print\\(s['name'], s['type']\\) for s in d.get\\('services',[]\\)]\")",
|
||||
"Bash(curl -s \"https://gis.charttools.noaa.gov/arcgis/rest/services/MCS/ENCOnline/MapServer/exts/MaritimeChartService/MapServer/0/query?where=1%3D1&geometry=-80.5%2C25.5%2C-80.0%2C25.9&geometryType=esriGeometryEnvelope&spatialRel=esriSpatialRelIntersects&outFields=*&f=json\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d,indent=2\\)\\)\")",
|
||||
"Bash(curl -s \"https://gis.charttools.noaa.gov/arcgis/rest/services/MCS/ENCOnline/MapServer/exts/MaritimeChartService?f=json\")",
|
||||
"Bash(curl -s \"https://gis.charttools.noaa.gov/arcgis/rest/services?f=pjson\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); [print\\(s\\) for s in d]\")",
|
||||
"Bash(curl -s \"https://gis.charttools.noaa.gov/arcgis/rest/services/MCS?f=pjson\")",
|
||||
"Bash(curl -s \"https://gis.charttools.noaa.gov/arcgis/rest/services/MCS/NOAAChartDisplay/MapServer?f=pjson\")",
|
||||
"Bash(curl -s \"https://charts.noaa.gov/ENCs/US3FL28M_19115.xml\")",
|
||||
"Bash(curl -s \"https://charts.noaa.gov/ENCs/US4FL2AI_19115.xml\")",
|
||||
"Bash(curl -s \"https://charts.noaa.gov/ENCs/ENCsIndv.shtml\")",
|
||||
"Bash(curl -s \"https://charts.noaa.gov/ENCs/US4FL2BI_19115.xml\")",
|
||||
"Bash(dir \"D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\data\\\\charts\" /s /b)",
|
||||
"Bash(dir \"D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\backend\" /s /b *.py)",
|
||||
"Bash(Select-String -Path \"D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\ui\\\\lib\\\\maplibre-gl.js\" -Pattern \"\\\\\"version\\\\\"\")",
|
||||
"Bash(Select-Object -First 3)",
|
||||
"Bash(venv\\\\Scripts\\\\python.exe -c \"import weasyprint; print\\('weasyprint ok'\\)\")",
|
||||
"Bash(venv\\\\Scripts\\\\python.exe -c \"import xhtml2pdf; print\\('xhtml2pdf ok'\\)\")",
|
||||
"Bash(venv\\\\Scripts\\\\python.exe -c \"import pdfkit; print\\('pdfkit ok'\\)\")",
|
||||
"Bash(Get-Content \"D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\main.py\")",
|
||||
"Bash(Select-String -Pattern \"crew_list|crew_add|passengers_list\")",
|
||||
"Bash(Select-Object -First 5)",
|
||||
"Bash(del /Q \"D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\__pycache__\\\\main.cpython-*.pyc\")",
|
||||
"Bash(cd /d \"D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\")",
|
||||
"Bash(python -c \"import ast; ast.parse\\(open\\('backend/sensors/sensor_state.py',encoding='utf-8'\\).read\\(\\)\\); print\\('sensor_state OK'\\)\")",
|
||||
"Bash(python -c \"import ast; ast.parse\\(open\\('backend/sensors/nmea0183_reader.py',encoding='utf-8'\\).read\\(\\)\\); print\\('nmea0183_reader OK'\\)\")",
|
||||
"Bash(python -c \"import ast; ast.parse\\(open\\('backend/sensors/nmea_router.py',encoding='utf-8'\\).read\\(\\)\\); print\\('nmea_router OK'\\)\")",
|
||||
"Bash(python -c \"import ast; ast.parse\\(open\\(r'D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\backend\\\\sensors\\\\nmea0183_reader.py',encoding='utf-8'\\).read\\(\\)\\); print\\('nmea0183_reader OK'\\)\")",
|
||||
"Bash(python -c \"import ast; ast.parse\\(open\\(r'D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\backend\\\\sensors\\\\nmea_router.py',encoding='utf-8'\\).read\\(\\)\\); print\\('nmea_router OK'\\)\")",
|
||||
"Bash(python -c \"import ast; ast.parse\\(open\\(r'D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\main.py',encoding='utf-8'\\).read\\(\\)\\); print\\('main.py OK'\\)\")",
|
||||
"Bash(node -e \"require\\('fs'\\).readFileSync\\(String.raw\\\\`D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\ui\\\\js\\\\sim.js\\\\`, 'utf8'\\); console.log\\('sim.js OK'\\)\")",
|
||||
"Bash(node --input-type=module)",
|
||||
"Bash(node -e \"var src=require\\('fs'\\).readFileSync\\(String.raw\\\\`D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\ui\\\\js\\\\app.js\\\\`,'utf8'\\); new Function\\(src\\); console.log\\('app.js OK'\\)\")",
|
||||
"Bash(node -e \"var s=require\\('fs'\\).readFileSync\\(String.raw\\\\`D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\ui\\\\js\\\\app.js\\\\`,'utf8'\\); new Function\\(s\\); console.log\\('app.js OK'\\)\")",
|
||||
"Bash(python -c \"import ast; ast.parse\\(open\\(r'D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\backend\\\\sensors\\\\sensor_state.py',encoding='utf-8'\\).read\\(\\)\\); print\\('sensor_state OK'\\)\")",
|
||||
"Bash(node -e \"var s=require\\('fs'\\).readFileSync\\(String.raw\\\\`D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\ui\\\\js\\\\crew.js\\\\`,'utf8'\\); new Function\\(s\\); console.log\\('crew.js OK'\\)\")",
|
||||
"Bash(python -m py_compile \"D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\main.py\")",
|
||||
"Bash(python -c \"import ast; ast.parse\\(open\\('main.py', encoding='utf-8'\\).read\\(\\)\\); print\\('main.py OK'\\)\")",
|
||||
"WebFetch(domain:ceehydrosystems.com)",
|
||||
"Bash(python _regen_ctg.py)",
|
||||
"Bash(findstr /n \"survey\\\\|Survey\\\\|lp-sv\\\\|lp-pane\" \"D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\ui\\\\index.html\")",
|
||||
"Bash(findstr /n \"survey\" \"D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\ui\\\\index.html\")",
|
||||
"Bash(python -c \"from backend.routers.charts import router; print\\('charts.py OK'\\)\")",
|
||||
"Bash(python3 -c \"import pdfplumber; p=pdfplumber.open\\(r'C:\\\\Users\\\\aerom\\\\Downloads\\\\Notice_to_Mariners_Anual_2023.pdf'\\); print\\(p.pages[0].extract_text\\(\\)[:2000]\\)\")",
|
||||
"Bash(python -c \"import pdfplumber; p=pdfplumber.open\\(r'C:\\\\Users\\\\aerom\\\\Downloads\\\\Notice_to_Mariners_Anual_2023.pdf'\\); print\\(p.pages[0].extract_text\\(\\)[:2000]\\)\")",
|
||||
"Bash(dir \"D:\\\\Proyectos Software\\\\QGISS57Converter\\\\capas_ctg\")",
|
||||
"Bash(dir \"C:\\\\AidsMonitoring\\\\charts\")",
|
||||
"Bash(python -c \"import sys; sys.stdout.reconfigure\\(encoding='utf-8', errors='replace'\\); [print\\(l.rstrip\\(\\)\\) for l in sys.stdin]\")",
|
||||
"Bash(python -c \"import sys; [print\\(f'{i+1:4d} {l}',end=''\\) for i,l in enumerate\\(sys.stdin\\)]\")",
|
||||
"Bash(powershell -command \"\\(Get-Content 'C:\\\\AidsMonitoring\\\\frontend\\\\js\\\\menu.js'\\).Count\")",
|
||||
"Bash(powershell -command \"\\(Get-Content 'C:\\\\AidsMonitoring\\\\frontend\\\\index.html'\\).Count\")",
|
||||
"Bash(powershell -command \"Get-ChildItem 'C:\\\\AidsMonitoring\\\\charts' | Select-Object Name\")",
|
||||
"Bash(powershell -command \"Get-ChildItem 'D:\\\\Proyectos Software\\\\AR ECDIS' | Select-Object Name\")",
|
||||
"Bash(powershell -command \"Get-ChildItem 'D:\\\\Proyectos Software\\\\AR ECDIS\\\\webecdis\\\\data' | Select-Object Name\")",
|
||||
"WebFetch(domain:www.puertocartagena.com)",
|
||||
"WebFetch(domain:www.paracay.com)",
|
||||
"WebFetch(domain:www.openstreetmap.org)",
|
||||
"WebFetch(domain:cecoldodigital.dimar.mil.co)",
|
||||
"Bash(where ogrinfo *)",
|
||||
"Bash(C:\\\\Python313\\\\python.exe -c \"from osgeo import ogr; print\\('gdal OK'\\)\")",
|
||||
"Bash(C:\\\\Users\\\\aerom\\\\AppData\\\\Local\\\\Python\\\\bin\\\\python.exe -c \"from osgeo import ogr; print\\('gdal OK'\\)\")",
|
||||
"Bash(\"C:/Python313/python.exe\" -c \"from osgeo import ogr; print\\('gdal OK'\\)\")",
|
||||
"Bash(\"C:/Users/aerom/AppData/Local/Python/bin/python.exe\" -c \"from osgeo import ogr; print\\('gdal OK'\\)\")",
|
||||
"Read(//c/Program Files/**)",
|
||||
"Read(//c/PROGRA~1/**)",
|
||||
"Bash(python -c \"import geopandas; print\\('geopandas OK'\\)\")",
|
||||
"Bash(grep -rn \"capas_ctg\\\\|BAHÍA_DE_CARTAGENA\" \"D:/Proyectos Software/AR ECDIS/webecdis/\" --include=\"*.py\" ! -path \"*/venv/*\")",
|
||||
"Bash(python _patch_ecdis_cartagena.py)",
|
||||
"Bash(python build_ecdis_manual.py capas_ctg \"BAHÍA_DE_CARTAGENA\")",
|
||||
"Bash(python -c \"import main\")",
|
||||
"Bash(python -c \"from models.user import User, Role; print\\(list\\(Role\\)\\)\")",
|
||||
"Bash(taskkill /F /IM python.exe /T)",
|
||||
"Bash(Start-Sleep -Milliseconds 500)",
|
||||
"Bash(curl -s http://localhost:5503/health)",
|
||||
"Bash(git -C \"C:/AidsMonitoring\" status)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ build/
|
||||
.env
|
||||
*.env
|
||||
|
||||
# Base de datos SQLite (contiene usuarios y contraseñas — NO subir)
|
||||
*.db
|
||||
*.sqlite
|
||||
|
||||
# ENC / chart binary exchange sets (large S-57 binaries)
|
||||
Cartas/*/ENC_ROOT/**/*.000
|
||||
Cartas/*/ENC_ROOT/**/*.001
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
for chart in ["BARRANQUILLA", "BAHÍA_DE_CARTAGENA"]:
|
||||
p = Path(r"D:\Proyectos Software\AR ECDIS\webecdis\data\charts\manual") / chart / "LIGHTS.geojson"
|
||||
if not p.exists():
|
||||
print(f"{chart}: no LIGHTS.geojson"); continue
|
||||
feats = json.loads(p.read_text(encoding="utf-8"))["features"]
|
||||
with_orient = [f for f in feats if f["properties"].get("ORIENT") is not None]
|
||||
print(f"\n{chart}: {len(with_orient)}/{len(feats)} features have ORIENT")
|
||||
for f in with_orient[:6]:
|
||||
pr = f["properties"]
|
||||
print(f" {pr.get('OBJNAM','?'):30s} ORIENT={pr['ORIENT']}")
|
||||
@@ -0,0 +1,20 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# Check AidsMonitoring features.geojson
|
||||
f = json.loads(Path(r"C:\AidsMonitoring\charts\BAHÍA_DE_CARTAGENA\features.geojson").read_text(encoding="utf-8"))
|
||||
print("=== AidsMonitoring features.geojson ===")
|
||||
for feat in f["features"]:
|
||||
p = feat["properties"]
|
||||
if p.get("orient") is not None:
|
||||
print(f" layer={p.get('layer')} name={p.get('name')} orient={p['orient']} aid_type={p.get('aid_type')}")
|
||||
if p.get("aid_type") in ("LEADING_LINE", "LIGHT_POINT"):
|
||||
print(f" [LIGHT] name={p.get('name')} orient={p.get('orient')} light_desc={p.get('light_desc')}")
|
||||
|
||||
# Check LIGHTS.csv for any ORIENT values
|
||||
import csv
|
||||
print("\n=== LIGHTS.csv ORIENT column ===")
|
||||
with open(r"D:\Proyectos Software\QGISS57Converter\capas_ctg\LIGHTS.csv", newline="", encoding="utf-8-sig") as f2:
|
||||
for row in csv.DictReader(f2):
|
||||
orient = row.get("ORIENT","").strip()
|
||||
print(f" {row['OBJNAM']:35s} feat_type={row['feat_type']:8s} ORIENT={orient!r}")
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Patch ECDIS Cartagena BOYCAR.geojson: set CATCAM from updated BOYCAR.csv.
|
||||
Run once after updating the CSV with the CATCAM column.
|
||||
"""
|
||||
import json, csv
|
||||
from pathlib import Path
|
||||
|
||||
GEOJSON = Path(r"D:\Proyectos Software\AR ECDIS\webecdis\data\charts\manual\BAHÍA_DE_CARTAGENA\BOYCAR.geojson")
|
||||
CSV = Path(r"D:\Proyectos Software\QGISS57Converter\capas_ctg\BOYCAR.csv")
|
||||
|
||||
# Load CSV catcam mapping: OBJNAM -> CATCAM int
|
||||
catcam_by_name = {}
|
||||
with open(CSV, newline="", encoding="utf-8-sig") as f:
|
||||
for row in csv.DictReader(f):
|
||||
name = row.get("OBJNAM", "").strip()
|
||||
catcam = row.get("CATCAM", "").strip()
|
||||
if name and catcam:
|
||||
catcam_by_name[name] = int(catcam)
|
||||
|
||||
print("CSV catcam map:")
|
||||
for k, v in catcam_by_name.items():
|
||||
print(f" {k} -> {v}")
|
||||
|
||||
|
||||
def infer_catcam(name: str) -> int:
|
||||
"""Fallback inference from DIMAR naming convention."""
|
||||
n = name.upper()
|
||||
if any(x in n for x in [" SE", "(SE)", "ESTE"]):
|
||||
return 2
|
||||
if any(x in n for x in [" SO", "(SO)", " SW", "OESTE", " BB"]):
|
||||
return 4
|
||||
if any(x in n for x in [" SS", "(SS)", " VS", "SUR"]):
|
||||
return 3
|
||||
return 1 # North default
|
||||
|
||||
|
||||
fc = json.loads(GEOJSON.read_text(encoding="utf-8"))
|
||||
updated = 0
|
||||
for feat in fc["features"]:
|
||||
p = feat["properties"]
|
||||
name = p.get("OBJNAM", "")
|
||||
if p.get("CATCAM") is None:
|
||||
catcam = catcam_by_name.get(name) or infer_catcam(name)
|
||||
p["CATCAM"] = catcam
|
||||
updated += 1
|
||||
print(f" PATCHED: {name!r} -> CATCAM={catcam}")
|
||||
|
||||
GEOJSON.write_text(json.dumps(fc, ensure_ascii=False), encoding="utf-8")
|
||||
print(f"\nDone. Updated {updated} features in {GEOJSON}")
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Regenerate features.geojson for Bahia de Cartagena directly from the
|
||||
source CSVs, bypassing the GDAL S-57 round-trip that loses LITCHR/SIGPER/
|
||||
VALNMR for BOYLAT and CATCAM for BOYCAR.
|
||||
"""
|
||||
import csv
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
CSV_DIR = Path(r"D:\Proyectos Software\QGISS57Converter\capas_ctg")
|
||||
OUT_FILE = Path(r"C:\AidsMonitoring\charts\BAHÍA_DE_CARTAGENA\features.geojson")
|
||||
META_FILE= Path(r"C:\AidsMonitoring\charts\BAHÍA_DE_CARTAGENA\meta.json")
|
||||
|
||||
# ── S-57 attribute lookup tables ──────────────────────────────────────────────
|
||||
LITCHR_MAP = {
|
||||
1:"F", 2:"Fl", 3:"LFl", 4:"Q", 5:"VQ", 6:"UQ", 7:"Iso",
|
||||
8:"Oc", 9:"IQ", 10:"IVQ", 11:"IUQ", 12:"Mo", 13:"FFl",
|
||||
14:"FlLFl", 15:"OcFl", 16:"FLFl", 25:"Al.Oc", 26:"Al.LFl",
|
||||
27:"Al.Fl", 28:"Al.Grp",
|
||||
}
|
||||
COLOUR_MAP = {
|
||||
1:"W", 2:"K", 3:"R", 4:"G", 5:"B", 6:"Y",
|
||||
7:"Gy", 8:"Br", 9:"Amb", 10:"Vi", 11:"Or", 12:"Mg",
|
||||
}
|
||||
|
||||
# CSV feat_type → canonical S-57 layer name
|
||||
FEAT_TYPE_TO_LAYER = {
|
||||
"BOYSPEC": "BOYSPP", # DIMAR CSV uses BOYSPEC; IHO S-57 is BOYSPP
|
||||
}
|
||||
|
||||
# Layer → feature category
|
||||
CATEGORY_MAP = {
|
||||
"BOYLAT": "buoy", "BOYCAR": "buoy", "BOYISD": "buoy",
|
||||
"BOYSPP": "buoy", "BOYSAW": "buoy",
|
||||
"BCNLAT": "beacon", "BCNCAR": "beacon",
|
||||
"LIGHTS": "light",
|
||||
"LNDMRK": "landmark",
|
||||
}
|
||||
|
||||
|
||||
def _num(v):
|
||||
if v is None or str(v).strip() == "":
|
||||
return None
|
||||
try:
|
||||
return float(v)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _int(v):
|
||||
n = _num(v)
|
||||
return int(n) if n is not None else None
|
||||
|
||||
|
||||
def _fmt(v):
|
||||
"""Format a number: drop trailing .0 for whole numbers."""
|
||||
try:
|
||||
f = float(v)
|
||||
return str(int(f)) if f == int(f) else str(f)
|
||||
except Exception:
|
||||
return str(v)
|
||||
|
||||
|
||||
def make_light_desc(litchr, siggrp, colour, sigper, valnmr):
|
||||
"""Build the compact light description string (e.g. 'Fl G 3s 3M')."""
|
||||
parts = []
|
||||
import re as _re2
|
||||
lc_int = _int(litchr)
|
||||
lc = LITCHR_MAP.get(lc_int, str(lc_int)) if lc_int is not None else ""
|
||||
sg_raw = str(siggrp or "").strip()
|
||||
sg_m = _re2.search(r'\d+', sg_raw)
|
||||
sg = int(sg_m.group()) if sg_m else None
|
||||
suffix = "+" if "+" in sg_raw else ""
|
||||
if sg is not None:
|
||||
lc = f"{lc}({sg}){suffix}"
|
||||
col_int = _int(colour)
|
||||
col_str = COLOUR_MAP.get(col_int, "") if col_int is not None else ""
|
||||
if lc:
|
||||
parts.append(f"{lc} {col_str}".strip())
|
||||
sp = _num(sigper)
|
||||
if sp is not None:
|
||||
parts.append(f"{_fmt(sp)}s")
|
||||
rng = _num(valnmr)
|
||||
if rng is not None:
|
||||
parts.append(f"{_fmt(rng)}M")
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def infer_catcam(siggrp, name):
|
||||
"""
|
||||
Cardinal buoy direction from SIGGRP (most reliable).
|
||||
Q(9)+LFl = West, Q(6)+LFl = South, Q(3) = East, Q = North
|
||||
DIMAR name convention (fallback):
|
||||
SS/VS → S, SN/VN → N, SE → E, SO → W
|
||||
"""
|
||||
# Extract first integer from SIGGRP even if it looks like "(6)+" or "(9)+LFl"
|
||||
import re as _re
|
||||
sg_str = str(siggrp or "")
|
||||
m = _re.search(r'\d+', sg_str)
|
||||
sg = int(m.group()) if m else None
|
||||
if sg is not None:
|
||||
if sg == 9: return "W"
|
||||
if sg == 6: return "S"
|
||||
if sg == 3: return "E"
|
||||
return "N" # Q without group or Q(1) = North cardinal
|
||||
n = (name or "").upper()
|
||||
if any(x in n for x in ("SUR", " SS", "(SS)", " VS", "VS ")): return "S"
|
||||
if any(x in n for x in ("ESTE", " SE", "(SE)")): return "E"
|
||||
if any(x in n for x in ("OESTE", " SO", "(SO)", " SW", "(SW)")): return "W"
|
||||
if any(x in n for x in ("NORTE", " SN", "(SN)", " VN", "VN ")): return "N"
|
||||
return "N"
|
||||
|
||||
|
||||
def infer_catlam(colour):
|
||||
"""IALA B (Americas): green=port=1, red=stbd=2."""
|
||||
c = _int(colour)
|
||||
if c == 4: return 1 # green → port
|
||||
if c == 3: return 2 # red → starboard
|
||||
return None
|
||||
|
||||
|
||||
def classify(layer, props):
|
||||
catlam = props.get("catlam")
|
||||
catcam = props.get("catcam")
|
||||
if layer in ("BOYLAT", "BCNLAT"):
|
||||
if catlam == 1: return "LATERAL_PORT"
|
||||
if catlam == 2: return "LATERAL_STBD"
|
||||
if catlam == 3: return "LATERAL_PREF_STBD"
|
||||
if catlam == 4: return "LATERAL_PREF_PORT"
|
||||
return "LATERAL_UNKNOWN"
|
||||
if layer in ("BOYCAR", "BCNCAR"):
|
||||
if catcam == "N": return "CARDINAL_N"
|
||||
if catcam == "E": return "CARDINAL_E"
|
||||
if catcam == "S": return "CARDINAL_S"
|
||||
if catcam == "W": return "CARDINAL_W"
|
||||
return "CARDINAL_UNKNOWN"
|
||||
if layer in ("BOYISD", "BCNISD"): return "ISOLATED_DANGER"
|
||||
if layer == "BOYSAW": return "SAFE_WATER"
|
||||
if layer in ("BOYSPP", "BOYSPEC"): return "SPECIAL"
|
||||
if layer == "LIGHTS": return "LIGHT_POINT"
|
||||
if layer == "LNDMRK": return "LANDMARK"
|
||||
if layer.startswith("BCN"): return "BEACON_GENERIC"
|
||||
if layer.startswith("BOY"): return "BUOY_GENERIC"
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
# ── Main loop ─────────────────────────────────────────────────────────────────
|
||||
features = []
|
||||
csv_files = sorted(CSV_DIR.glob("*.csv"))
|
||||
print(f"Processing {len(csv_files)} CSV files from {CSV_DIR}")
|
||||
|
||||
for csv_file in csv_files:
|
||||
row_count = 0
|
||||
with open(csv_file, newline="", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
feat_type = (row.get("feat_type") or csv_file.stem).strip()
|
||||
layer = FEAT_TYPE_TO_LAYER.get(feat_type, feat_type)
|
||||
if not layer:
|
||||
continue
|
||||
|
||||
lon = _num(row.get("lon"))
|
||||
lat = _num(row.get("lat"))
|
||||
if lon is None or lat is None:
|
||||
continue
|
||||
|
||||
category = CATEGORY_MAP.get(layer, "buoy")
|
||||
|
||||
litchr = row.get("LITCHR", "").strip()
|
||||
siggrp = row.get("SIGGRP", "").strip()
|
||||
colour = row.get("COLOUR", "").strip()
|
||||
sigper = row.get("SIGPER", "").strip()
|
||||
valnmr = row.get("VALNMR", "").strip()
|
||||
height = row.get("HEIGHT", "").strip()
|
||||
orient_raw = row.get("ORIENT", "").strip()
|
||||
name = row.get("OBJNAM", "").strip()
|
||||
inform = row.get("INFORM", "").strip()
|
||||
|
||||
colour_int = _int(colour)
|
||||
colours = [colour_int] if colour_int is not None else []
|
||||
light_desc = make_light_desc(litchr, siggrp, colour, sigper, valnmr)
|
||||
|
||||
props = {
|
||||
"layer": layer,
|
||||
"category": category,
|
||||
"name": name or None,
|
||||
"info": inform or None,
|
||||
"light_desc": light_desc or None,
|
||||
"range_nm": _num(valnmr),
|
||||
"height_m": _num(height),
|
||||
"colours": colours,
|
||||
"colour_code": colour_int,
|
||||
}
|
||||
|
||||
# Layer-specific S-57 attributes
|
||||
if layer in ("BOYCAR", "BCNCAR"):
|
||||
# Prefer explicit CATCAM column (1=N,2=E,3=S,4=W); fall back to inference
|
||||
catcam_raw = row.get("CATCAM", "").strip()
|
||||
catcam_int = _int(catcam_raw)
|
||||
_CATCAM_INT = {1: "N", 2: "E", 3: "S", 4: "W"}
|
||||
if catcam_int in _CATCAM_INT:
|
||||
props["catcam"] = _CATCAM_INT[catcam_int]
|
||||
else:
|
||||
props["catcam"] = infer_catcam(siggrp, name)
|
||||
if layer in ("BOYLAT", "BCNLAT"):
|
||||
props["catlam"] = infer_catlam(colour)
|
||||
|
||||
orient_num = _num(orient_raw)
|
||||
if orient_num is not None:
|
||||
props["orient"] = orient_num
|
||||
|
||||
props["aid_type"] = classify(layer, props)
|
||||
|
||||
# Strip None values (but keep empty lists like colours=[])
|
||||
props = {k: v for k, v in props.items() if v is not None}
|
||||
|
||||
features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {"type": "Point", "coordinates": [lon, lat]},
|
||||
"properties": props,
|
||||
})
|
||||
row_count += 1
|
||||
|
||||
print(f" {csv_file.name}: {row_count} features")
|
||||
|
||||
# ── Write features.geojson ────────────────────────────────────────────────────
|
||||
fc = {"type": "FeatureCollection", "features": features}
|
||||
OUT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(OUT_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(fc, f, ensure_ascii=False, indent=2)
|
||||
print(f"\nWrote {len(features)} features to {OUT_FILE}")
|
||||
|
||||
# ── Update meta.json ──────────────────────────────────────────────────────────
|
||||
if features:
|
||||
lons = [ft["geometry"]["coordinates"][0] for ft in features]
|
||||
lats = [ft["geometry"]["coordinates"][1] for ft in features]
|
||||
bbox = [min(lons), min(lats), max(lons), max(lats)]
|
||||
try:
|
||||
meta = json.loads(META_FILE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
meta = {}
|
||||
meta["feature_count"] = len(features)
|
||||
meta["bbox"] = bbox
|
||||
META_FILE.write_text(json.dumps(meta), encoding="utf-8")
|
||||
print(f"Updated meta.json: {len(features)} features, bbox={bbox}")
|
||||
|
||||
# ── Quick QA: show light_desc for first 5 BOYLAT and all BOYCAR ──────────────
|
||||
print("\n── BOYLAT sample ──")
|
||||
for ft in [f for f in features if f["properties"].get("layer") == "BOYLAT"][:5]:
|
||||
p = ft["properties"]
|
||||
print(f" {p['name']:30s} {p.get('light_desc','(none)'):20s} "
|
||||
f"aid_type={p['aid_type']}")
|
||||
|
||||
print("\n── BOYCAR (all) ──")
|
||||
for ft in [f for f in features if f["properties"].get("layer") == "BOYCAR"]:
|
||||
p = ft["properties"]
|
||||
print(f" {p['name']:35s} {p.get('light_desc','(none)'):20s} "
|
||||
f"catcam={p.get('catcam','?')} aid_type={p['aid_type']}")
|
||||
|
||||
print("\n── LNDMRK (all) ──")
|
||||
for ft in [f for f in features if f["properties"].get("layer") == "LNDMRK"]:
|
||||
p = ft["properties"]
|
||||
print(f" {p['name']:35s} {p.get('light_desc','(none)'):22s} "
|
||||
f"height={p.get('height_m','?')}m range={p.get('range_nm','?')}NM")
|
||||
Binary file not shown.
Binary file not shown.
+233
-24
@@ -16,18 +16,20 @@ load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), '..', '.env'))
|
||||
|
||||
from database import engine, SessionLocal, ensure_column
|
||||
from models.aid import Aid
|
||||
from models.vessel import Vessel, VesselTrack, RecordingEvent
|
||||
from models.vessel import Vessel, VesselTrack, AtonTrack, RecordingEvent
|
||||
from models.lamp import Lamp
|
||||
from models.contact import Contact, AlertReport
|
||||
from models.org import Port, Company, BuoyOwnership
|
||||
import models.aid
|
||||
import models.vessel
|
||||
import models.user
|
||||
import models.lamp
|
||||
import models.contact
|
||||
import models.org
|
||||
from routers import aids
|
||||
from routers.auth import seed_users, get_current_user
|
||||
from routers.auth import seed_users, get_current_user, require_admin
|
||||
from models.user import User
|
||||
from fastapi import Depends
|
||||
from fastapi import Depends, Query as _Query
|
||||
from database import get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from routers import auth as auth_router
|
||||
@@ -35,6 +37,8 @@ from routers import charts as charts_router
|
||||
from routers import equipment as equipment_router
|
||||
from routers import lamps as lamps_router
|
||||
from routers import contacts as contacts_router
|
||||
from routers import tracks as tracks_router
|
||||
from routers import org as org_router
|
||||
from services.ais_simulator import run_simulator, MIAMI_AIDS
|
||||
from services.alert_engine import evaluate_vessel, evaluate_aid_movement, aid_alert_state
|
||||
from services.gps_reader import GPSReader
|
||||
@@ -50,11 +54,25 @@ models.vessel.Base.metadata.create_all(bind=engine)
|
||||
models.user.Base.metadata.create_all(bind=engine)
|
||||
models.lamp.Base.metadata.create_all(bind=engine)
|
||||
models.contact.Base.metadata.create_all(bind=engine)
|
||||
models.org.Base.metadata.create_all(bind=engine)
|
||||
# Additive migrations for columns added after first install
|
||||
ensure_column("aids", "lamp_id", "TEXT")
|
||||
ensure_column("aids", "lamp_id", "TEXT")
|
||||
ensure_column("users", "prefs_json", "TEXT")
|
||||
ensure_column("users", "company_id", "TEXT")
|
||||
|
||||
# Each entry: {"ws": WebSocket, "company_id": str|None}
|
||||
# company_id=None means superadmin/admin — sees ALL traffic.
|
||||
connected_clients: list[dict] = []
|
||||
|
||||
# Ownership cache: company_id → set of MMSI strings the company owns.
|
||||
# Rebuilt at startup and refreshed via POST /org/refresh (or when ownership changes).
|
||||
_ownership_cache: dict[str, set] = {} # company_id → {mmsi, ...}
|
||||
_aid_ownership_cache: dict[str, set] = {} # company_id → {aid_id, ...}
|
||||
|
||||
# Track throttle: last persisted point per MMSI
|
||||
_vessel_track_last: dict[str, dict] = {} # mmsi → {ts, lat, lon}
|
||||
_aton_track_last: dict[str, dict] = {} # mmsi → {ts, lat, lon}
|
||||
|
||||
connected_clients: list[WebSocket] = []
|
||||
# Source of truth for runtime config — mutated via POST /settings.
|
||||
config = settings_store.SETTINGS
|
||||
|
||||
@@ -69,6 +87,47 @@ _battery_alert_state: dict[str, str | None] = {}
|
||||
vessels_state: dict = {}
|
||||
aids_state: dict = {}
|
||||
|
||||
def _build_ownership_cache(db):
|
||||
"""Rebuild the in-memory ownership maps (MMSI and aid_id) from the DB."""
|
||||
_ownership_cache.clear()
|
||||
_aid_ownership_cache.clear()
|
||||
rows = db.query(BuoyOwnership).all()
|
||||
for row in rows:
|
||||
if row.mmsi:
|
||||
_ownership_cache.setdefault(row.company_id, set()).add(row.mmsi)
|
||||
if row.aid_id:
|
||||
_aid_ownership_cache.setdefault(row.company_id, set()).add(row.aid_id)
|
||||
|
||||
|
||||
def seed_ports():
|
||||
"""Ensure canonical Colombian ports exist in the DB."""
|
||||
PORTS = [
|
||||
{"id": "port-barranquilla", "name": "Barranquilla",
|
||||
"center_lat": 11.0041, "center_lon": -74.8070, "default_zoom": 12.0,
|
||||
"chart_name": None},
|
||||
{"id": "port-cartagena", "name": "Cartagena",
|
||||
"center_lat": 10.3997, "center_lon": -75.5144, "default_zoom": 12.0,
|
||||
"chart_name": "BAHÍA_DE_CARTAGENA"},
|
||||
{"id": "port-santamarta", "name": "Santa Marta",
|
||||
"center_lat": 11.2408, "center_lon": -74.2110, "default_zoom": 12.0,
|
||||
"chart_name": None},
|
||||
{"id": "port-buenaventura", "name": "Buenaventura",
|
||||
"center_lat": 3.8800, "center_lon": -77.0311, "default_zoom": 12.0,
|
||||
"chart_name": None},
|
||||
{"id": "port-tumaco", "name": "Tumaco",
|
||||
"center_lat": 1.8189, "center_lon": -78.7619, "default_zoom": 12.0,
|
||||
"chart_name": None},
|
||||
]
|
||||
db = SessionLocal()
|
||||
try:
|
||||
for p in PORTS:
|
||||
if not db.query(Port).filter(Port.id == p["id"]).first():
|
||||
db.add(Port(**p))
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def seed_contacts():
|
||||
"""Pre-load the operator company (INSM) as an OWNER contact so the
|
||||
REPORT flow has someone to notify out of the box."""
|
||||
@@ -113,16 +172,47 @@ def seed_aids():
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
async def broadcast(message: dict):
|
||||
def _client_may_see(client: dict,
|
||||
owned_mmsi: str | None = None,
|
||||
owned_aid_id: str | None = None) -> bool:
|
||||
"""Return True if this client is allowed to receive this message.
|
||||
|
||||
- owned_mmsi → vessel/AtoN traffic filtered by MMSI ownership
|
||||
- owned_aid_id → aid-position traffic filtered by aid_id ownership
|
||||
- neither → system/alert message: always delivered
|
||||
"""
|
||||
cid = client.get("company_id")
|
||||
if cid is None:
|
||||
return True # admin/superadmin: sees all
|
||||
if owned_mmsi is not None:
|
||||
return owned_mmsi in _ownership_cache.get(cid, set())
|
||||
if owned_aid_id is not None:
|
||||
return owned_aid_id in _aid_ownership_cache.get(cid, set())
|
||||
return True # non-filtered message
|
||||
|
||||
|
||||
async def broadcast(message: dict,
|
||||
owned_mmsi: str | None = None,
|
||||
owned_aid_id: str | None = None):
|
||||
"""
|
||||
Send *message* to connected WebSocket clients.
|
||||
|
||||
owned_mmsi → filter vessel/AtoN traffic by MMSI (company users see only their own)
|
||||
owned_aid_id → filter aid-position updates by aid_id
|
||||
Admins (company_id=None) always receive everything.
|
||||
"""
|
||||
data = json.dumps(message)
|
||||
dead = []
|
||||
for ws in connected_clients:
|
||||
for client in connected_clients:
|
||||
if not _client_may_see(client, owned_mmsi, owned_aid_id):
|
||||
continue
|
||||
try:
|
||||
await ws.send_text(data)
|
||||
await client["ws"].send_text(data)
|
||||
except Exception:
|
||||
dead.append(ws)
|
||||
for ws in dead:
|
||||
connected_clients.remove(ws)
|
||||
dead.append(client)
|
||||
for c in dead:
|
||||
if c in connected_clients:
|
||||
connected_clients.remove(c)
|
||||
|
||||
async def _persist_recording(db, alert: dict):
|
||||
"""Save or close a RecordingEvent row when auto-recording triggers."""
|
||||
@@ -157,23 +247,39 @@ async def process_message(msg: dict):
|
||||
db = SessionLocal()
|
||||
try:
|
||||
if msg["type"] == "vessel":
|
||||
vessels_state[msg["mmsi"]] = msg
|
||||
mmsi = msg["mmsi"]
|
||||
vessels_state[mmsi] = msg
|
||||
aids_list = list(aids_state.values())
|
||||
alerts = evaluate_vessel(msg, aids_list, config)
|
||||
await broadcast(msg)
|
||||
await broadcast(msg) # vessels = public traffic, no filter
|
||||
for alert in alerts:
|
||||
await broadcast({"type": "alert", **alert})
|
||||
if alert["tipo"] in ("GRABACION_INICIADA", "GRABACION_FINALIZADA"):
|
||||
await _persist_recording(db, alert)
|
||||
|
||||
# ── Auto-persist VesselTrack (DVR) ───────────────────────────────
|
||||
lat, lon = msg.get("lat"), msg.get("lon")
|
||||
if lat is not None and lon is not None:
|
||||
now = datetime.utcnow()
|
||||
prev = _vessel_track_last.get(mmsi)
|
||||
elapsed = (now - prev["ts"]).total_seconds() if prev else 9999
|
||||
if elapsed >= 10:
|
||||
db.add(VesselTrack(
|
||||
mmsi=mmsi, timestamp=now,
|
||||
lat=lat, lon=lon,
|
||||
sog=msg.get("sog"), cog=msg.get("cog"),
|
||||
heading=msg.get("heading"),
|
||||
))
|
||||
db.commit()
|
||||
_vessel_track_last[mmsi] = {"ts": now, "lat": lat, "lon": lon}
|
||||
|
||||
elif msg["type"] == "aton":
|
||||
# Real AIS Type 21 / Type 8 from hardware receiver
|
||||
entry = process_aton_message(msg)
|
||||
if entry:
|
||||
aton_state[entry["mmsi"]] = entry
|
||||
await broadcast({"type": "aton", **entry})
|
||||
|
||||
mmsi = entry["mmsi"]
|
||||
aton_state[mmsi] = entry
|
||||
await broadcast({"type": "aton", **entry}, owned_mmsi=mmsi)
|
||||
|
||||
# Auto-upsert Aid row on first sight (Type 21 carries name + position).
|
||||
# The user must then assign a lamp via the right panel.
|
||||
@@ -243,6 +349,23 @@ async def process_message(msg: dict):
|
||||
})
|
||||
_battery_alert_state[mmsi] = new_state
|
||||
|
||||
# ── Auto-persist AtonTrack (DVR) ─────────────────────────────
|
||||
at_lat = entry.get("lat")
|
||||
at_lon = entry.get("lon")
|
||||
if at_lat is not None and at_lon is not None:
|
||||
now = datetime.utcnow()
|
||||
prev = _aton_track_last.get(mmsi)
|
||||
elapsed = (now - prev["ts"]).total_seconds() if prev else 9999
|
||||
if elapsed >= 30: # AtoN updates less frequent → 30s threshold
|
||||
db.add(AtonTrack(
|
||||
mmsi=mmsi, timestamp=now,
|
||||
lat=at_lat, lon=at_lon,
|
||||
voltage_v=entry.get("voltage_v"),
|
||||
off_position=entry.get("off_position"),
|
||||
))
|
||||
db.commit()
|
||||
_aton_track_last[mmsi] = {"ts": now, "lat": at_lat, "lon": at_lon}
|
||||
|
||||
elif msg["type"] == "aid_position":
|
||||
aid_id = msg["id"]
|
||||
if aid_id in aids_state:
|
||||
@@ -258,7 +381,7 @@ async def process_message(msg: dict):
|
||||
aid.desplazamiento_m = msg["desplazamiento_m"]
|
||||
aid.en_posicion = msg["en_posicion"]
|
||||
db.commit()
|
||||
await broadcast(msg)
|
||||
await broadcast(msg) # aid positions = public, no filter
|
||||
for alert in alert_list:
|
||||
await broadcast({"type": "alert", **alert})
|
||||
finally:
|
||||
@@ -296,6 +419,10 @@ async def lifespan(app: FastAPI):
|
||||
seed_aids()
|
||||
seed_users()
|
||||
seed_contacts()
|
||||
seed_ports()
|
||||
db = SessionLocal()
|
||||
_build_ownership_cache(db)
|
||||
db.close()
|
||||
db = SessionLocal()
|
||||
for aid in db.query(Aid).all():
|
||||
aids_state[aid.id] = {
|
||||
@@ -337,9 +464,34 @@ app.include_router(charts_router.router)
|
||||
app.include_router(equipment_router.router)
|
||||
app.include_router(lamps_router.router)
|
||||
app.include_router(contacts_router.router)
|
||||
app.include_router(tracks_router.router)
|
||||
app.include_router(org_router.router)
|
||||
|
||||
@app.post("/org/refresh")
|
||||
async def refresh_ownership(
|
||||
_user: User = Depends(require_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Rebuild the in-memory MMSI ownership cache from DB. Call after assigning/removing buoys."""
|
||||
_build_ownership_cache(db)
|
||||
return {"ok": True, "companies": len(_ownership_cache),
|
||||
"total_mmsis": sum(len(v) for v in _ownership_cache.values())}
|
||||
|
||||
|
||||
def _require_recording_permission(user: User):
|
||||
"""ADMIN, SUPERADMIN, or CLIENT_ADMIN may start/stop recordings."""
|
||||
from models.user import Role
|
||||
if user.role not in (Role.ADMIN, Role.SUPERADMIN, Role.CLIENT_ADMIN):
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=403,
|
||||
detail="Only ADMIN or CLIENT_ADMIN may control recordings")
|
||||
|
||||
@app.post("/recordings/start/{mmsi}")
|
||||
async def manual_start_recording(mmsi: str, aid_id: str = "MANUAL"):
|
||||
async def manual_start_recording(
|
||||
mmsi: str, aid_id: str = "MANUAL",
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
_require_recording_permission(current_user)
|
||||
from services.alert_engine import active_recordings
|
||||
from models.vessel import RecordingEvent
|
||||
import uuid as _uuid
|
||||
@@ -368,10 +520,14 @@ async def manual_start_recording(mmsi: str, aid_id: str = "MANUAL"):
|
||||
return {"ok": True}
|
||||
|
||||
@app.post("/recordings/stop/{mmsi}")
|
||||
async def manual_stop_recording(mmsi: str, aid_id: str = "MANUAL"):
|
||||
async def manual_stop_recording(
|
||||
mmsi: str, aid_id: str = "MANUAL",
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
from services.alert_engine import active_recordings
|
||||
from models.vessel import RecordingEvent
|
||||
from sqlalchemy import and_
|
||||
_require_recording_permission(current_user)
|
||||
key = f"{mmsi}_{aid_id}"
|
||||
rec_mem = active_recordings.pop(key, None)
|
||||
min_dist = round(rec_mem["min_dist"], 1) if rec_mem else None
|
||||
@@ -562,10 +718,63 @@ async def ais_stop():
|
||||
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def websocket_endpoint(ws: WebSocket):
|
||||
async def websocket_endpoint(
|
||||
ws: WebSocket,
|
||||
token: str | None = _Query(default=None),
|
||||
):
|
||||
await ws.accept()
|
||||
connected_clients.append(ws)
|
||||
await ws.send_text(json.dumps({"type": "init", "aids": list(aids_state.values()), "vessels": list(vessels_state.values()), "atons": list(aton_state.values())}))
|
||||
|
||||
# ── Resolve company_id from optional JWT token ────────────────────────────
|
||||
company_id = None
|
||||
if token:
|
||||
try:
|
||||
from jose import jwt as _jwt, JWTError as _JWTError
|
||||
from routers.auth import SECRET_KEY, ALGORITHM
|
||||
payload = _jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
username = payload.get("sub")
|
||||
if username:
|
||||
_db = SessionLocal()
|
||||
try:
|
||||
_u = _db.query(User).filter(User.username == username,
|
||||
User.activo == True).first()
|
||||
if _u:
|
||||
company_id = getattr(_u, "company_id", None)
|
||||
finally:
|
||||
_db.close()
|
||||
except Exception:
|
||||
pass # invalid/expired token → treat as anonymous (admin-level view)
|
||||
|
||||
client = {"ws": ws, "company_id": company_id}
|
||||
connected_clients.append(client)
|
||||
|
||||
# ── Build filtered init snapshot ──────────────────────────────────────────
|
||||
# company users see ONLY their company's aids/vessels/atons
|
||||
# admins (company_id=None) see everything
|
||||
owned_mmsis = _ownership_cache.get(company_id) if company_id else None
|
||||
owned_aid_ids= _aid_ownership_cache.get(company_id) if company_id else None
|
||||
|
||||
# aids = official positions → visible to ALL users (public nav info)
|
||||
# vessels= ship AIS traffic → visible to ALL users
|
||||
# atons = live AIS AtoN msgs → filtered by company MMSI ownership
|
||||
init_aids = list(aids_state.values())
|
||||
init_vessels = list(vessels_state.values())
|
||||
|
||||
if company_id is None:
|
||||
# Admin / superadmin → all AtoNs
|
||||
init_atons = list(aton_state.values())
|
||||
else:
|
||||
# Client role: get their owned MMSIs (empty set = no buoys assigned = sees nothing)
|
||||
client_mmsis = _ownership_cache.get(company_id, set())
|
||||
init_atons = [a for a in aton_state.values()
|
||||
if a.get("mmsi") in client_mmsis]
|
||||
|
||||
await ws.send_text(json.dumps({
|
||||
"type": "init",
|
||||
"aids": init_aids,
|
||||
"vessels": init_vessels,
|
||||
"atons": init_atons,
|
||||
}))
|
||||
|
||||
# Re-emit any active aid alerts so new clients see current state
|
||||
from datetime import datetime as _dt
|
||||
for aid_id, state in aid_alert_state.items():
|
||||
@@ -584,7 +793,7 @@ async def websocket_endpoint(ws: WebSocket):
|
||||
while True:
|
||||
await ws.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
if ws in connected_clients:
|
||||
connected_clients.remove(ws)
|
||||
if client in connected_clients:
|
||||
connected_clients.remove(client)
|
||||
|
||||
app.mount("/", StaticFiles(directory=os.path.join(os.path.dirname(__file__), '..', 'frontend'), html=True), name="frontend")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Organization models: Port, Company, BuoyOwnership
|
||||
Used for multi-client access control and per-port default views.
|
||||
"""
|
||||
from sqlalchemy import Column, String, Float, Boolean, DateTime, Text
|
||||
from sqlalchemy.sql import func
|
||||
from database import Base
|
||||
|
||||
|
||||
class Port(Base):
|
||||
"""Geographic port / region served by this deployment."""
|
||||
__tablename__ = "ports"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
name = Column(String, nullable=False)
|
||||
country = Column(String, default="Colombia")
|
||||
center_lat = Column(Float, nullable=True)
|
||||
center_lon = Column(Float, nullable=True)
|
||||
default_zoom = Column(Float, default=12.0)
|
||||
chart_name = Column(String, nullable=True) # folder under charts/, e.g. "BAHÍA_DE_CARTAGENA"
|
||||
activo = Column(Boolean, default=True)
|
||||
creado_en = Column(DateTime, server_default=func.now())
|
||||
|
||||
|
||||
class Company(Base):
|
||||
"""Buoy-owner company (client). Belongs to a home port."""
|
||||
__tablename__ = "companies"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
name = Column(String, nullable=False)
|
||||
port_id = Column(String, nullable=True) # FK → ports.id
|
||||
contact_email = Column(String, nullable=True)
|
||||
contact_phone = Column(String, nullable=True)
|
||||
activa = Column(Boolean, default=True)
|
||||
notas = Column(Text, nullable=True)
|
||||
creado_en = Column(DateTime, server_default=func.now())
|
||||
|
||||
|
||||
class BuoyOwnership(Base):
|
||||
"""
|
||||
Which company owns (and monitors) a given Aid/MMSI.
|
||||
A company user can see AIS/ATON real-time data only for buoys
|
||||
listed in this table under their company_id.
|
||||
"""
|
||||
__tablename__ = "buoy_ownership"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
company_id = Column(String, nullable=False) # FK → companies.id
|
||||
aid_id = Column(String, nullable=True) # FK → aids.id (nullable if aid not yet in DB)
|
||||
mmsi = Column(String, nullable=True) # direct MMSI reference
|
||||
notas = Column(Text, nullable=True)
|
||||
creado_en = Column(DateTime, server_default=func.now())
|
||||
@@ -4,9 +4,10 @@ from database import Base
|
||||
import enum
|
||||
|
||||
class Role(str, enum.Enum):
|
||||
SUPERADMIN = "SUPERADMIN"
|
||||
ADMIN = "ADMIN"
|
||||
USER = "USER"
|
||||
SUPERADMIN = "SUPERADMIN"
|
||||
ADMIN = "ADMIN"
|
||||
CLIENT_ADMIN = "CLIENT_ADMIN" # company-scoped: can start/stop recordings for own aids
|
||||
USER = "USER" # company-scoped: read-only
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
@@ -17,6 +18,7 @@ class User(Base):
|
||||
email = Column(String, unique=True, nullable=True)
|
||||
hashed_pw = Column(String, nullable=False)
|
||||
role = Column(String, default="USER")
|
||||
company_id = Column(String, nullable=True)
|
||||
activo = Column(Boolean, default=True)
|
||||
creado_en = Column(DateTime, server_default=func.now())
|
||||
ultimo_login = Column(DateTime, nullable=True)
|
||||
|
||||
@@ -2,6 +2,7 @@ from sqlalchemy import Column, String, Float, DateTime, Boolean, Integer
|
||||
from sqlalchemy.sql import func
|
||||
from database import Base
|
||||
|
||||
|
||||
class Vessel(Base):
|
||||
__tablename__ = "vessels"
|
||||
|
||||
@@ -44,5 +45,18 @@ class RecordingEvent(Base):
|
||||
inicio = Column(DateTime, nullable=False)
|
||||
fin = Column(DateTime, nullable=True)
|
||||
distancia_min_m = Column(Float, nullable=True)
|
||||
trigger = Column(String) # PROXIMIDAD | PROYECCION
|
||||
trigger = Column(String) # PROXIMIDAD | PROYECCION | MANUAL
|
||||
cerrado = Column(Boolean, default=False)
|
||||
|
||||
|
||||
class AtonTrack(Base):
|
||||
"""Continuous position history for AIS AtoN (Type 21) devices."""
|
||||
__tablename__ = "aton_tracks"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
mmsi = Column(String, nullable=False, index=True)
|
||||
timestamp = Column(DateTime, server_default=func.now(), index=True)
|
||||
lat = Column(Float, nullable=False)
|
||||
lon = Column(Float, nullable=False)
|
||||
voltage_v = Column(Float, nullable=True)
|
||||
off_position = Column(Boolean, nullable=True)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+27
-8
@@ -100,11 +100,12 @@ class TokenResponse(BaseModel):
|
||||
role: str
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
username: str
|
||||
nombre: str
|
||||
email: Optional[str] = None
|
||||
password: str
|
||||
role: str = "USER"
|
||||
username: str
|
||||
nombre: str
|
||||
email: Optional[str] = None
|
||||
password: str
|
||||
role: str = "USER"
|
||||
company_id: Optional[str] = None
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
||||
@@ -121,11 +122,25 @@ def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get
|
||||
|
||||
@router.get("/me")
|
||||
def me(user: User = Depends(get_current_user)):
|
||||
return {"username": user.username, "nombre": user.nombre, "role": user.role}
|
||||
return {
|
||||
"username": user.username,
|
||||
"nombre": user.nombre,
|
||||
"role": user.role,
|
||||
"company_id": getattr(user, "company_id", None),
|
||||
}
|
||||
|
||||
@router.get("/users", dependencies=[Depends(require_superadmin)])
|
||||
def list_users(db: Session = Depends(get_db)):
|
||||
return db.query(User).all()
|
||||
users = db.query(User).all()
|
||||
return [
|
||||
{
|
||||
"id": u.id, "username": u.username, "nombre": u.nombre,
|
||||
"email": u.email, "role": u.role, "activo": u.activo,
|
||||
"company_id": getattr(u, "company_id", None),
|
||||
"ultimo_login": u.ultimo_login.isoformat() if u.ultimo_login else None,
|
||||
}
|
||||
for u in users
|
||||
]
|
||||
|
||||
@router.post("/users", dependencies=[Depends(require_superadmin)])
|
||||
def create_user(data: UserCreate, db: Session = Depends(get_db)):
|
||||
@@ -138,10 +153,12 @@ def create_user(data: UserCreate, db: Session = Depends(get_db)):
|
||||
email=data.email,
|
||||
hashed_pw=hash_password(data.password),
|
||||
role=data.role,
|
||||
company_id=data.company_id,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
return {"ok": True, "username": user.username, "role": user.role}
|
||||
return {"ok": True, "username": user.username, "role": user.role,
|
||||
"company_id": user.company_id}
|
||||
|
||||
@router.put("/users/{username}", dependencies=[Depends(require_superadmin)])
|
||||
def update_user(username: str, data: dict, db: Session = Depends(get_db)):
|
||||
@@ -158,6 +175,8 @@ def update_user(username: str, data: dict, db: Session = Depends(get_db)):
|
||||
user.activo = data["activo"]
|
||||
if "password" in data and data["password"]:
|
||||
user.hashed_pw = hash_password(data["password"])
|
||||
if "company_id" in data:
|
||||
user.company_id = data["company_id"] or None
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
+110
-3
@@ -17,6 +17,7 @@ from services.chart_manager import (
|
||||
delete_cell, get_all_features, get_all_depths,
|
||||
get_all_land, get_all_hazards, get_all_zones,
|
||||
CHARTS_DIR, set_meta, get_region,
|
||||
install_from_csv_zip, install_from_csv_dir,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/charts", tags=["charts"])
|
||||
@@ -210,9 +211,28 @@ async def download_noaa(cell_id: str):
|
||||
return {"installed": installed}
|
||||
|
||||
|
||||
def _zip_contains_csvs(zip_path: Path) -> bool:
|
||||
"""Return True if the ZIP has *.csv files but no *.000 ENC files."""
|
||||
import zipfile as _zf
|
||||
with _zf.ZipFile(zip_path) as z:
|
||||
names = z.namelist()
|
||||
has_csv = any(n.lower().endswith(".csv") for n in names)
|
||||
has_enc = any(n.upper().endswith(".000") for n in names)
|
||||
return has_csv and not has_enc
|
||||
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_chart(file: UploadFile = File(...)):
|
||||
suffix = Path(file.filename).suffix.lower()
|
||||
"""
|
||||
Universal chart upload.
|
||||
|
||||
Accepts:
|
||||
• .000 — single S-57 ENC cell
|
||||
• .zip — either a NOAA ENC zip (contains .000) OR a CSV-based custom
|
||||
chart zip (contains *.csv, no .000). The ZIP auto-detection
|
||||
determines which parser is used.
|
||||
"""
|
||||
suffix = Path(file.filename or "").suffix.lower()
|
||||
if suffix not in (".zip", ".000"):
|
||||
raise HTTPException(400, "Only .zip or .000 files accepted")
|
||||
|
||||
@@ -223,8 +243,13 @@ async def upload_chart(file: UploadFile = File(...)):
|
||||
|
||||
try:
|
||||
if suffix == ".zip":
|
||||
installed = await asyncio.get_event_loop().run_in_executor(
|
||||
None, install_from_zip, tmp_path)
|
||||
# Auto-detect: CSV zip vs ENC zip
|
||||
if _zip_contains_csvs(tmp_path):
|
||||
installed = await asyncio.get_event_loop().run_in_executor(
|
||||
None, install_from_csv_zip, tmp_path)
|
||||
else:
|
||||
installed = await asyncio.get_event_loop().run_in_executor(
|
||||
None, install_from_zip, tmp_path)
|
||||
else:
|
||||
orig_name = Path(file.filename).stem.upper() if file.filename else None
|
||||
cell_id = await asyncio.get_event_loop().run_in_executor(
|
||||
@@ -238,6 +263,58 @@ async def upload_chart(file: UploadFile = File(...)):
|
||||
return {"installed": installed}
|
||||
|
||||
|
||||
@router.post("/upload-csv")
|
||||
async def upload_csv_chart(file: UploadFile = File(...),
|
||||
cell_id: str | None = None):
|
||||
"""
|
||||
Upload a ZIP archive containing CSV navigation-aid files to create a
|
||||
custom chart cell. Use this when your source data is in DIMAR / custom
|
||||
CSV format rather than S-57 .000.
|
||||
|
||||
The cell_id query parameter overrides the inferred name from the ZIP.
|
||||
|
||||
Workflow:
|
||||
1. Edit BOYLAT.csv, BOYCAR.csv, BOYSPEC.csv, etc. in your local
|
||||
capas_ctg/ folder.
|
||||
2. Zip the entire folder.
|
||||
3. POST the zip here (optionally with ?cell_id=BAHIA_DE_CARTAGENA).
|
||||
4. AidsMonitoring reads the CSVs directly, preserving all light
|
||||
attributes (LITCHR, SIGPER, VALNMR …) without GDAL round-trip loss.
|
||||
"""
|
||||
if not (file.filename or "").lower().endswith(".zip"):
|
||||
raise HTTPException(400, "Only .zip files accepted for CSV upload")
|
||||
|
||||
data = await file.read()
|
||||
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
|
||||
tmp.write(data)
|
||||
tmp_path = Path(tmp.name)
|
||||
|
||||
try:
|
||||
if cell_id:
|
||||
# Extract to temp dir then install with explicit cell_id
|
||||
import zipfile as _zf
|
||||
import tempfile as _tf
|
||||
with _tf.TemporaryDirectory() as td:
|
||||
td_p = Path(td)
|
||||
with _zf.ZipFile(tmp_path) as z:
|
||||
for member in z.namelist():
|
||||
if member.lower().endswith(".csv"):
|
||||
data_bytes = z.read(member)
|
||||
(td_p / Path(member).name).write_bytes(data_bytes)
|
||||
installed_id = await asyncio.get_event_loop().run_in_executor(
|
||||
None, install_from_csv_dir, td_p, cell_id)
|
||||
installed = [installed_id]
|
||||
else:
|
||||
installed = await asyncio.get_event_loop().run_in_executor(
|
||||
None, install_from_csv_zip, tmp_path)
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
finally:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
|
||||
return {"installed": installed}
|
||||
|
||||
|
||||
@router.delete("/cells/{cell_id}")
|
||||
def remove_cell(cell_id: str):
|
||||
delete_cell(cell_id)
|
||||
@@ -305,6 +382,36 @@ async def rebuild_cache():
|
||||
return {"rebuilt": rebuilt}
|
||||
|
||||
|
||||
@router.post("/cells/{cell_id}/rebuild-from-csv")
|
||||
async def rebuild_cell_from_csv(cell_id: str, file: UploadFile = File(...)):
|
||||
"""
|
||||
Update an existing cell's features.geojson by re-uploading its CSV zip.
|
||||
Equivalent to DELETE + upload-csv but preserves meta.json settings
|
||||
(e.g. region override).
|
||||
"""
|
||||
if not (file.filename or "").lower().endswith(".zip"):
|
||||
raise HTTPException(400, "Only .zip files accepted")
|
||||
data = await file.read()
|
||||
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
|
||||
tmp.write(data)
|
||||
tmp_path = Path(tmp.name)
|
||||
try:
|
||||
import zipfile as _zf, tempfile as _tf
|
||||
with _tf.TemporaryDirectory() as td:
|
||||
td_p = Path(td)
|
||||
with _zf.ZipFile(tmp_path) as z:
|
||||
for member in z.namelist():
|
||||
if member.lower().endswith(".csv"):
|
||||
(td_p / Path(member).name).write_bytes(z.read(member))
|
||||
installed_id = await asyncio.get_event_loop().run_in_executor(
|
||||
None, install_from_csv_dir, td_p, cell_id)
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
finally:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
return {"rebuilt": installed_id}
|
||||
|
||||
|
||||
@router.post("/cells/{cell_id}/rebuild")
|
||||
async def rebuild_cell(cell_id: str):
|
||||
"""Re-parse a single ENC cell and regenerate its feature cache."""
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
Organization CRUD: Ports, Companies, BuoyOwnership.
|
||||
|
||||
GET /org/ports → list ports
|
||||
POST /org/ports → create port (admin)
|
||||
PUT /org/ports/{id} → update port (admin)
|
||||
|
||||
GET /org/companies → list companies
|
||||
POST /org/companies → create company (admin)
|
||||
PUT /org/companies/{id} → update company (admin)
|
||||
|
||||
GET /org/companies/{company_id}/buoys → list owned buoys
|
||||
POST /org/companies/{company_id}/buoys → assign buoy to company (admin)
|
||||
DELETE /org/companies/{company_id}/buoys/{id} → remove ownership (admin)
|
||||
|
||||
GET /org/me/company → current user's company + port (for homepage)
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
import uuid
|
||||
import os
|
||||
import json
|
||||
|
||||
from database import get_db
|
||||
from models.org import Port, Company, BuoyOwnership
|
||||
from models.user import User
|
||||
from models.aid import Aid
|
||||
from routers.auth import get_current_user, require_admin
|
||||
|
||||
# Charts directory — one level above the backend package
|
||||
_CHARTS_DIR = os.path.normpath(
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'charts')
|
||||
)
|
||||
|
||||
def _read_chart_bbox(chart_name: str) -> list | None:
|
||||
"""Return [west, south, east, north] from the chart's meta.json, or None."""
|
||||
if not chart_name:
|
||||
return None
|
||||
meta = os.path.join(_CHARTS_DIR, chart_name, 'meta.json')
|
||||
try:
|
||||
with open(meta, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
bbox = data.get('bbox')
|
||||
if bbox and len(bbox) == 4:
|
||||
return bbox # [minLon, minLat, maxLon, maxLat]
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
router = APIRouter(prefix="/org", tags=["org"])
|
||||
|
||||
|
||||
# ── Ports ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/ports")
|
||||
def list_ports(db: Session = Depends(get_db)):
|
||||
return [_port_dict(p) for p in db.query(Port).filter(Port.activo == True).all()]
|
||||
|
||||
|
||||
@router.post("/ports", dependencies=[Depends(require_admin)])
|
||||
def create_port(data: dict, db: Session = Depends(get_db)):
|
||||
if not data.get("name"):
|
||||
raise HTTPException(400, "name is required")
|
||||
port = Port(
|
||||
id=str(uuid.uuid4()),
|
||||
name=data["name"],
|
||||
country=data.get("country", "Colombia"),
|
||||
center_lat=data.get("center_lat"),
|
||||
center_lon=data.get("center_lon"),
|
||||
default_zoom=data.get("default_zoom", 12.0),
|
||||
chart_name=data.get("chart_name"),
|
||||
)
|
||||
db.add(port); db.commit()
|
||||
return _port_dict(port)
|
||||
|
||||
|
||||
@router.put("/ports/{port_id}", dependencies=[Depends(require_admin)])
|
||||
def update_port(port_id: str, data: dict, db: Session = Depends(get_db)):
|
||||
port = db.query(Port).filter(Port.id == port_id).first()
|
||||
if not port:
|
||||
raise HTTPException(404, "Port not found")
|
||||
for field in ("name", "country", "center_lat", "center_lon",
|
||||
"default_zoom", "chart_name", "activo"):
|
||||
if field in data:
|
||||
setattr(port, field, data[field])
|
||||
db.commit()
|
||||
return _port_dict(port)
|
||||
|
||||
|
||||
def _port_dict(p: Port) -> dict:
|
||||
return {
|
||||
"id": p.id, "name": p.name, "country": p.country,
|
||||
"center_lat": p.center_lat, "center_lon": p.center_lon,
|
||||
"default_zoom": p.default_zoom, "chart_name": p.chart_name,
|
||||
"chart_bbox": _read_chart_bbox(p.chart_name), # [W,S,E,N] or null
|
||||
"activo": p.activo,
|
||||
}
|
||||
|
||||
|
||||
# ── Companies ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/companies")
|
||||
def list_companies(db: Session = Depends(get_db)):
|
||||
return [_company_dict(c) for c in db.query(Company).all()]
|
||||
|
||||
|
||||
@router.post("/companies", dependencies=[Depends(require_admin)])
|
||||
def create_company(data: dict, db: Session = Depends(get_db)):
|
||||
if not data.get("name"):
|
||||
raise HTTPException(400, "name is required")
|
||||
company = Company(
|
||||
id=str(uuid.uuid4()),
|
||||
name=data["name"],
|
||||
port_id=data.get("port_id"),
|
||||
contact_email=data.get("contact_email"),
|
||||
contact_phone=data.get("contact_phone"),
|
||||
notas=data.get("notas"),
|
||||
)
|
||||
db.add(company); db.commit()
|
||||
return _company_dict(company)
|
||||
|
||||
|
||||
@router.put("/companies/{company_id}", dependencies=[Depends(require_admin)])
|
||||
def update_company(company_id: str, data: dict, db: Session = Depends(get_db)):
|
||||
company = db.query(Company).filter(Company.id == company_id).first()
|
||||
if not company:
|
||||
raise HTTPException(404, "Company not found")
|
||||
for field in ("name", "port_id", "contact_email", "contact_phone", "notas", "activa"):
|
||||
if field in data:
|
||||
setattr(company, field, data[field])
|
||||
db.commit()
|
||||
return _company_dict(company)
|
||||
|
||||
|
||||
def _company_dict(c: Company) -> dict:
|
||||
return {
|
||||
"id": c.id, "name": c.name, "port_id": c.port_id,
|
||||
"contact_email": c.contact_email, "contact_phone": c.contact_phone,
|
||||
"activa": c.activa, "notas": c.notas,
|
||||
}
|
||||
|
||||
|
||||
# ── Buoy Ownership ────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/companies/{company_id}/buoys")
|
||||
def list_company_buoys(company_id: str, db: Session = Depends(get_db)):
|
||||
rows = db.query(BuoyOwnership).filter(BuoyOwnership.company_id == company_id).all()
|
||||
result = []
|
||||
for r in rows:
|
||||
entry = {"id": r.id, "company_id": r.company_id,
|
||||
"aid_id": r.aid_id, "mmsi": r.mmsi, "notas": r.notas}
|
||||
# Enrich with aid name if available
|
||||
if r.aid_id:
|
||||
aid = db.query(Aid).filter(Aid.id == r.aid_id).first()
|
||||
if aid:
|
||||
entry["aid_nombre"] = aid.nombre
|
||||
entry["mmsi"] = entry["mmsi"] or aid.mmsi
|
||||
result.append(entry)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/companies/{company_id}/buoys", dependencies=[Depends(require_admin)])
|
||||
def assign_buoy(company_id: str, data: dict, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Assign a buoy to a company. Provide either aid_id or mmsi (or both).
|
||||
If aid_id is given, mmsi is auto-filled from the Aid row.
|
||||
"""
|
||||
company = db.query(Company).filter(Company.id == company_id).first()
|
||||
if not company:
|
||||
raise HTTPException(404, "Company not found")
|
||||
|
||||
aid_id = data.get("aid_id")
|
||||
mmsi = data.get("mmsi")
|
||||
|
||||
if not aid_id and not mmsi:
|
||||
raise HTTPException(400, "Provide aid_id or mmsi")
|
||||
|
||||
if aid_id:
|
||||
aid = db.query(Aid).filter(Aid.id == aid_id).first()
|
||||
if not aid:
|
||||
raise HTTPException(404, "Aid not found")
|
||||
mmsi = mmsi or aid.mmsi # fill from aid if not explicitly given
|
||||
|
||||
# Prevent duplicate
|
||||
existing = db.query(BuoyOwnership).filter(
|
||||
BuoyOwnership.company_id == company_id,
|
||||
BuoyOwnership.aid_id == aid_id,
|
||||
).first()
|
||||
if existing:
|
||||
raise HTTPException(409, "Already assigned")
|
||||
|
||||
row = BuoyOwnership(
|
||||
id=str(uuid.uuid4()),
|
||||
company_id=company_id,
|
||||
aid_id=aid_id,
|
||||
mmsi=mmsi,
|
||||
notas=data.get("notas"),
|
||||
)
|
||||
db.add(row); db.commit()
|
||||
return {"ok": True, "id": row.id, "mmsi": mmsi}
|
||||
|
||||
|
||||
@router.delete("/companies/{company_id}/buoys/{ownership_id}",
|
||||
dependencies=[Depends(require_admin)])
|
||||
def remove_ownership(company_id: str, ownership_id: str, db: Session = Depends(get_db)):
|
||||
row = db.query(BuoyOwnership).filter(
|
||||
BuoyOwnership.id == ownership_id,
|
||||
BuoyOwnership.company_id == company_id,
|
||||
).first()
|
||||
if not row:
|
||||
raise HTTPException(404, "Ownership record not found")
|
||||
db.delete(row); db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── Current user's company / home port ───────────────────────────────────────
|
||||
|
||||
@router.get("/me/company")
|
||||
def my_company(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Returns the company and port associated with the logged-in user.
|
||||
Used by the frontend to set the default map view on login.
|
||||
"""
|
||||
company_id = getattr(current_user, "company_id", None)
|
||||
if not company_id:
|
||||
return {"company": None, "port": None}
|
||||
|
||||
company = db.query(Company).filter(Company.id == company_id).first()
|
||||
port = db.query(Port).filter(Port.id == company.port_id).first() \
|
||||
if company and company.port_id else None
|
||||
|
||||
return {
|
||||
"company": _company_dict(company) if company else None,
|
||||
"port": _port_dict(port) if port else None,
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
"""
|
||||
Track history endpoints — DVR replay for vessels and AtoNs.
|
||||
|
||||
GET /tracks/vessels → list MMSIs that have track data
|
||||
GET /tracks/vessels/{mmsi} → track points for one vessel (?from=&to=&limit=)
|
||||
GET /tracks/atons → list AtoN MMSIs with track data
|
||||
GET /tracks/atons/{mmsi} → track points for one AtoN (?from=&to=&limit=)
|
||||
GET /recordings → list RecordingEvents (?mmsi=&open=)
|
||||
GET /recordings/{event_id}/track → vessel track for that event window
|
||||
|
||||
Access control:
|
||||
- ADMIN / SUPERADMIN : see ALL recordings
|
||||
- USER with company : see ONLY recordings where aid_id belongs to their company's buoys
|
||||
- Anonymous : see ALL (legacy / unauthenticated clients)
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException, Header
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import distinct
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
from database import get_db
|
||||
from models.vessel import VesselTrack, AtonTrack, RecordingEvent
|
||||
from models.org import BuoyOwnership
|
||||
from models.user import User, Role
|
||||
|
||||
router = APIRouter(tags=["tracks"])
|
||||
|
||||
|
||||
def _optional_user(
|
||||
authorization: Optional[str] = Header(default=None),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Optional[User]:
|
||||
"""Resolve JWT token from Authorization header if present. Returns None if absent/invalid."""
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
return None
|
||||
token = authorization.split(" ", 1)[1]
|
||||
try:
|
||||
from jose import jwt as _jwt, JWTError
|
||||
from routers.auth import SECRET_KEY, ALGORITHM
|
||||
payload = _jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
username = payload.get("sub")
|
||||
if not username:
|
||||
return None
|
||||
return db.query(User).filter(User.username == username, User.activo == True).first()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _company_aid_ids(company_id: str, db: Session) -> Optional[set]:
|
||||
"""Return the set of aid_ids owned by this company, or None if unrestricted."""
|
||||
rows = db.query(BuoyOwnership).filter(
|
||||
BuoyOwnership.company_id == company_id,
|
||||
BuoyOwnership.aid_id.isnot(None),
|
||||
).all()
|
||||
return {r.aid_id for r in rows} if rows is not None else set()
|
||||
|
||||
|
||||
# ── Vessel tracks ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/tracks/vessels")
|
||||
def list_tracked_vessels(db: Session = Depends(get_db)):
|
||||
"""Return distinct MMSIs that have at least one VesselTrack row."""
|
||||
rows = db.query(distinct(VesselTrack.mmsi)).all()
|
||||
return [r[0] for r in rows]
|
||||
|
||||
|
||||
@router.get("/tracks/vessels/{mmsi}")
|
||||
def get_vessel_track(
|
||||
mmsi: str,
|
||||
from_dt: Optional[str] = Query(None, alias="from"),
|
||||
to_dt: Optional[str] = Query(None, alias="to"),
|
||||
limit: int = Query(10_000, le=100_000),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Return time-ordered track points for *mmsi*.
|
||||
Optionally filter by ISO-8601 timestamps: ?from=2026-05-01T00:00:00&to=2026-05-02T00:00:00
|
||||
"""
|
||||
q = db.query(VesselTrack).filter(VesselTrack.mmsi == mmsi)
|
||||
if from_dt:
|
||||
try:
|
||||
q = q.filter(VesselTrack.timestamp >= datetime.fromisoformat(from_dt))
|
||||
except ValueError:
|
||||
raise HTTPException(400, "Invalid 'from' datetime (use ISO-8601)")
|
||||
if to_dt:
|
||||
try:
|
||||
q = q.filter(VesselTrack.timestamp <= datetime.fromisoformat(to_dt))
|
||||
except ValueError:
|
||||
raise HTTPException(400, "Invalid 'to' datetime (use ISO-8601)")
|
||||
rows = q.order_by(VesselTrack.timestamp.asc()).limit(limit).all()
|
||||
return [
|
||||
{
|
||||
"mmsi": r.mmsi,
|
||||
"ts": r.timestamp.isoformat() if r.timestamp else None,
|
||||
"lat": r.lat,
|
||||
"lon": r.lon,
|
||||
"sog": r.sog,
|
||||
"cog": r.cog,
|
||||
"heading": r.heading,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
# ── AtoN tracks ───────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/tracks/atons")
|
||||
def list_tracked_atons(db: Session = Depends(get_db)):
|
||||
"""Return distinct MMSIs that have at least one AtonTrack row."""
|
||||
rows = db.query(distinct(AtonTrack.mmsi)).all()
|
||||
return [r[0] for r in rows]
|
||||
|
||||
|
||||
@router.get("/tracks/atons/{mmsi}")
|
||||
def get_aton_track(
|
||||
mmsi: str,
|
||||
from_dt: Optional[str] = Query(None, alias="from"),
|
||||
to_dt: Optional[str] = Query(None, alias="to"),
|
||||
limit: int = Query(10_000, le=100_000),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Time-ordered position history for an AIS AtoN (Type 21)."""
|
||||
q = db.query(AtonTrack).filter(AtonTrack.mmsi == mmsi)
|
||||
if from_dt:
|
||||
try:
|
||||
q = q.filter(AtonTrack.timestamp >= datetime.fromisoformat(from_dt))
|
||||
except ValueError:
|
||||
raise HTTPException(400, "Invalid 'from' datetime")
|
||||
if to_dt:
|
||||
try:
|
||||
q = q.filter(AtonTrack.timestamp <= datetime.fromisoformat(to_dt))
|
||||
except ValueError:
|
||||
raise HTTPException(400, "Invalid 'to' datetime")
|
||||
rows = q.order_by(AtonTrack.timestamp.asc()).limit(limit).all()
|
||||
return [
|
||||
{
|
||||
"mmsi": r.mmsi,
|
||||
"ts": r.timestamp.isoformat() if r.timestamp else None,
|
||||
"lat": r.lat,
|
||||
"lon": r.lon,
|
||||
"voltage_v": r.voltage_v,
|
||||
"off_position": r.off_position,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
# ── Recording events ──────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/recordings")
|
||||
def list_recordings(
|
||||
mmsi: Optional[str] = Query(None),
|
||||
open_: Optional[bool] = Query(None, alias="open"),
|
||||
from_dt: Optional[str] = Query(None, alias="from"),
|
||||
to_dt: Optional[str] = Query(None, alias="to"),
|
||||
limit: int = Query(200, le=2000),
|
||||
db: Session = Depends(get_db),
|
||||
caller: Optional[User] = Depends(_optional_user),
|
||||
):
|
||||
"""
|
||||
List RecordingEvents (most-recent first).
|
||||
Supports ?mmsi=, ?open=true, ?from=YYYY-MM-DD, ?to=YYYY-MM-DD
|
||||
|
||||
Access control:
|
||||
ADMIN/SUPERADMIN → all recordings
|
||||
USER with company → only recordings for their company's aids
|
||||
No token → all recordings (legacy)
|
||||
"""
|
||||
from models.vessel import Vessel
|
||||
from models.aid import Aid
|
||||
|
||||
# ── Company filter for USER role ─────────────────────────────────────────
|
||||
allowed_aid_ids: Optional[set] = None
|
||||
if caller and caller.role == Role.USER:
|
||||
cid = getattr(caller, "company_id", None)
|
||||
if cid:
|
||||
allowed_aid_ids = _company_aid_ids(cid, db)
|
||||
|
||||
q = db.query(RecordingEvent)
|
||||
|
||||
# Restrict to owned aids if caller is a company user
|
||||
if allowed_aid_ids is not None:
|
||||
q = q.filter(RecordingEvent.aid_id.in_(allowed_aid_ids))
|
||||
|
||||
if mmsi:
|
||||
q = q.filter(RecordingEvent.mmsi == mmsi)
|
||||
if open_ is not None:
|
||||
q = q.filter(RecordingEvent.cerrado == (not open_))
|
||||
if from_dt:
|
||||
try:
|
||||
q = q.filter(RecordingEvent.inicio >= datetime.fromisoformat(from_dt))
|
||||
except ValueError:
|
||||
pass
|
||||
if to_dt:
|
||||
try:
|
||||
# Include the full end date day
|
||||
to_end = datetime.fromisoformat(to_dt).replace(hour=23, minute=59, second=59)
|
||||
q = q.filter(RecordingEvent.inicio <= to_end)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
events = q.order_by(RecordingEvent.inicio.desc()).limit(limit).all()
|
||||
|
||||
# Enrich with vessel / aid names in one pass
|
||||
mmsis = {e.mmsi for e in events}
|
||||
aid_ids = {e.aid_id for e in events}
|
||||
vessels = {v.mmsi: v.nombre for v in db.query(Vessel).filter(Vessel.mmsi.in_(mmsis)).all()}
|
||||
aids = {a.id: a.nombre for a in db.query(Aid).filter(Aid.id.in_(aid_ids)).all()}
|
||||
|
||||
return [
|
||||
{
|
||||
"id": e.id,
|
||||
"mmsi": e.mmsi,
|
||||
"vessel_nombre": vessels.get(e.mmsi),
|
||||
"aid_id": e.aid_id,
|
||||
"aid_nombre": aids.get(e.aid_id),
|
||||
"inicio_utc": e.inicio.isoformat() if e.inicio else None,
|
||||
"fin_utc": e.fin.isoformat() if e.fin else None,
|
||||
"distancia_min_m": e.distancia_min_m,
|
||||
"trigger": e.trigger,
|
||||
"cerrado": e.cerrado,
|
||||
}
|
||||
for e in events
|
||||
]
|
||||
|
||||
|
||||
@router.get("/recordings/{event_id}/track")
|
||||
def get_recording_track(event_id: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Return the VesselTrack points that fall within the time window of a
|
||||
specific RecordingEvent — ready for DVR replay.
|
||||
"""
|
||||
event = db.query(RecordingEvent).filter(RecordingEvent.id == event_id).first()
|
||||
if not event:
|
||||
raise HTTPException(404, "Recording event not found")
|
||||
|
||||
q = (
|
||||
db.query(VesselTrack)
|
||||
.filter(VesselTrack.mmsi == event.mmsi)
|
||||
.filter(VesselTrack.timestamp >= event.inicio)
|
||||
)
|
||||
if event.fin:
|
||||
q = q.filter(VesselTrack.timestamp <= event.fin)
|
||||
|
||||
rows = q.order_by(VesselTrack.timestamp.asc()).all()
|
||||
return {
|
||||
"event": {
|
||||
"id": event.id,
|
||||
"mmsi": event.mmsi,
|
||||
"aid_id": event.aid_id,
|
||||
"inicio": event.inicio.isoformat(),
|
||||
"fin": event.fin.isoformat() if event.fin else None,
|
||||
"trigger": event.trigger,
|
||||
},
|
||||
"track": [
|
||||
{
|
||||
"ts": r.timestamp.isoformat() if r.timestamp else None,
|
||||
"lat": r.lat,
|
||||
"lon": r.lon,
|
||||
"sog": r.sog,
|
||||
"cog": r.cog,
|
||||
"heading": r.heading,
|
||||
}
|
||||
for r in rows
|
||||
],
|
||||
}
|
||||
Binary file not shown.
@@ -78,6 +78,269 @@ def auto_region(cell_id: str) -> str:
|
||||
if prefix in REGION_B_PREFIXES: return "B"
|
||||
return "B"
|
||||
|
||||
|
||||
# ── CSV → features (DIMAR / custom CSV charts) ────────────────────────────────
|
||||
# Mapping from DIMAR CSV feat_type to canonical S-57 layer name.
|
||||
_CSV_FEAT_TYPE_MAP = {
|
||||
"BOYSPEC": "BOYSPP", # DIMAR uses BOYSPEC; IHO S-57 is BOYSPP
|
||||
}
|
||||
|
||||
# Feature categories per layer
|
||||
_CSV_CATEGORY_MAP = {
|
||||
"BOYLAT": "buoy", "BOYCAR": "buoy", "BOYISD": "buoy",
|
||||
"BOYSPP": "buoy", "BOYSAW": "buoy",
|
||||
"BCNLAT": "beacon","BCNCAR": "beacon",
|
||||
"LIGHTS": "light", "LNDMRK": "landmark",
|
||||
}
|
||||
|
||||
|
||||
def _csv_num(v):
|
||||
if v is None or str(v).strip() == "":
|
||||
return None
|
||||
try:
|
||||
return float(v)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _csv_int(v):
|
||||
n = _csv_num(v)
|
||||
return int(n) if n is not None else None
|
||||
|
||||
|
||||
def _csv_fmt(v):
|
||||
"""Format float: drop trailing .0 for whole numbers."""
|
||||
try:
|
||||
f = float(v)
|
||||
return str(int(f)) if f == int(f) else str(f)
|
||||
except Exception:
|
||||
return str(v)
|
||||
|
||||
|
||||
def _csv_light_desc(litchr, siggrp, colour, sigper, valnmr) -> str:
|
||||
"""Build compact light description from CSV fields (e.g. 'Fl G 3s 3M')."""
|
||||
parts = []
|
||||
lc_int = _csv_int(litchr)
|
||||
lc = LITCHR.get(lc_int, str(lc_int)) if lc_int is not None else ""
|
||||
sg = _csv_int(siggrp)
|
||||
if sg is not None:
|
||||
lc = f"{lc}({sg})"
|
||||
col_int = _csv_int(colour)
|
||||
col_str = COLOUR.get(col_int, "") if col_int is not None else ""
|
||||
if lc:
|
||||
parts.append(f"{lc} {col_str}".strip())
|
||||
sp = _csv_num(sigper)
|
||||
if sp is not None:
|
||||
parts.append(f"{_csv_fmt(sp)}s")
|
||||
rng = _csv_num(valnmr)
|
||||
if rng is not None:
|
||||
parts.append(f"{_csv_fmt(rng)}M")
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _csv_infer_catcam(siggrp: str, name: str) -> str | None:
|
||||
"""
|
||||
Cardinal buoy quadrant from SIGGRP (reliable) or DIMAR naming convention.
|
||||
Q(9)+LFl=W Q(6)+LFl=S Q(3)=E Q=N
|
||||
DIMAR name suffixes: SS/VS→S SN/VN→N SE→E SO→W
|
||||
"""
|
||||
sg = _csv_int(siggrp)
|
||||
if sg is not None:
|
||||
if sg == 9: return "W"
|
||||
if sg == 6: return "S"
|
||||
if sg == 3: return "E"
|
||||
return "N"
|
||||
n = (name or "").upper()
|
||||
if any(x in n for x in (" SS", "(SS)", "VS")): return "S"
|
||||
if any(x in n for x in (" SE", "(SE)")): return "E"
|
||||
if any(x in n for x in (" SO", "(SO)")): return "W"
|
||||
return "N"
|
||||
|
||||
|
||||
def _csv_infer_catlam(colour: str) -> int | None:
|
||||
"""IALA B (Americas): green(4)=port=1, red(3)=stbd=2."""
|
||||
c = _csv_int(colour)
|
||||
if c == 4: return 1
|
||||
if c == 3: return 2
|
||||
return None
|
||||
|
||||
|
||||
def _parse_csvs_to_features(csv_dir: Path) -> list[dict]:
|
||||
"""
|
||||
Read navigation-aid CSV files from csv_dir and return GeoJSON features.
|
||||
|
||||
Each CSV must have columns: OBJNAM, lon, lat, feat_type, LITCHR, SIGGRP,
|
||||
COLOUR, SIGPER, VALNMR, HEIGHT, ORIENT, INFORM.
|
||||
feat_type values: BOYLAT, BOYCAR, BOYISD, BOYSPEC/BOYSPP, BCNLAT, LIGHTS,
|
||||
LNDMRK.
|
||||
|
||||
This function is the primary path for DIMAR/custom charts created with
|
||||
QGISS57Converter; it preserves all light attributes (LITCHR, SIGPER, etc.)
|
||||
that the GDAL S-57 driver may drop during the .000 round-trip.
|
||||
"""
|
||||
import csv as _csv_mod
|
||||
features: list[dict] = []
|
||||
for csv_file in sorted(csv_dir.glob("*.csv")):
|
||||
with open(csv_file, newline="", encoding="utf-8") as fh:
|
||||
reader = _csv_mod.DictReader(fh)
|
||||
for row in reader:
|
||||
feat_type = (row.get("feat_type") or csv_file.stem).strip()
|
||||
layer = _CSV_FEAT_TYPE_MAP.get(feat_type, feat_type)
|
||||
if not layer:
|
||||
continue
|
||||
lon = _csv_num(row.get("lon"))
|
||||
lat = _csv_num(row.get("lat"))
|
||||
if lon is None or lat is None:
|
||||
continue
|
||||
|
||||
category = _CSV_CATEGORY_MAP.get(layer, "buoy")
|
||||
litchr = row.get("LITCHR", "").strip()
|
||||
siggrp = row.get("SIGGRP", "").strip()
|
||||
colour = row.get("COLOUR", "").strip()
|
||||
sigper = row.get("SIGPER", "").strip()
|
||||
valnmr = row.get("VALNMR", "").strip()
|
||||
height = row.get("HEIGHT", "").strip()
|
||||
orient_r = row.get("ORIENT", "").strip()
|
||||
name = row.get("OBJNAM", "").strip()
|
||||
inform = row.get("INFORM", "").strip()
|
||||
|
||||
col_int = _csv_int(colour)
|
||||
colours = [col_int] if col_int is not None else []
|
||||
light_d = _csv_light_desc(litchr, siggrp, colour, sigper, valnmr)
|
||||
|
||||
props: dict = {
|
||||
"layer": layer,
|
||||
"category": category,
|
||||
"name": name or None,
|
||||
"info": inform or None,
|
||||
"light_desc": light_d or None,
|
||||
"range_nm": _csv_num(valnmr),
|
||||
"height_m": _csv_num(height),
|
||||
"colours": colours,
|
||||
"colour_code": col_int,
|
||||
}
|
||||
if layer in ("BOYCAR", "BCNCAR"):
|
||||
props["catcam"] = _csv_infer_catcam(siggrp, name)
|
||||
if layer in ("BOYLAT", "BCNLAT"):
|
||||
props["catlam"] = _csv_infer_catlam(colour)
|
||||
orient_val = _csv_num(orient_r)
|
||||
if orient_val is not None:
|
||||
props["orient"] = orient_val
|
||||
props["aid_type"] = classify(layer, props)
|
||||
# Remove None values
|
||||
props = {k: v for k, v in props.items() if v is not None}
|
||||
features.append({
|
||||
"type": "Feature",
|
||||
"geometry": {"type": "Point", "coordinates": [lon, lat]},
|
||||
"properties": props,
|
||||
})
|
||||
return features
|
||||
|
||||
|
||||
def install_from_csv_dir(csv_dir: Path, cell_id: str) -> str:
|
||||
"""
|
||||
Create or update an AidsMonitoring chart cell from a directory of CSV files.
|
||||
|
||||
This is the preferred install pathway for custom (DIMAR) charts because it
|
||||
preserves all S-57 attribute codes (LITCHR, SIGGRP, etc.) without loss.
|
||||
|
||||
csv_dir — directory containing *.csv files (BOYLAT.csv, BOYCAR.csv, etc.)
|
||||
cell_id — chart cell identifier (e.g. 'BAHIA_DE_CARTAGENA')
|
||||
"""
|
||||
cell_id = cell_id.upper()
|
||||
cell_dir = CHARTS_DIR / cell_id
|
||||
cell_dir.mkdir(exist_ok=True)
|
||||
|
||||
features = _parse_csvs_to_features(csv_dir)
|
||||
with open(cell_dir / "features.geojson", "w", encoding="utf-8") as f:
|
||||
json.dump({"type": "FeatureCollection", "features": features}, f,
|
||||
ensure_ascii=False)
|
||||
log.info("CSV chart %s → %d features", cell_id, len(features))
|
||||
|
||||
# Empty auxiliary caches so the frontend doesn't show stale data
|
||||
for fname in ("depths.geojson", "land.geojson", "hazards.geojson", "zones.geojson"):
|
||||
p = cell_dir / fname
|
||||
if not p.exists():
|
||||
with open(p, "w") as f:
|
||||
json.dump({"type": "FeatureCollection", "features": []}, f)
|
||||
|
||||
# Update meta.json
|
||||
bbox = None
|
||||
if features:
|
||||
lons = [ft["geometry"]["coordinates"][0] for ft in features]
|
||||
lats = [ft["geometry"]["coordinates"][1] for ft in features]
|
||||
bbox = [min(lons), min(lats), max(lons), max(lats)]
|
||||
meta = get_meta(cell_id)
|
||||
meta["feature_count"] = len(features)
|
||||
meta["bbox"] = bbox
|
||||
meta.setdefault("region", auto_region(cell_id))
|
||||
_meta_path(cell_dir).write_text(json.dumps(meta))
|
||||
return cell_id
|
||||
|
||||
|
||||
def install_from_csv_zip(zip_path: Path) -> list[str]:
|
||||
"""
|
||||
Install one or more CSV chart cells from a ZIP archive.
|
||||
|
||||
Expected ZIP layout (any of these is accepted):
|
||||
Option A — single cell (cell_id inferred from folder name or ZIP name):
|
||||
BOYLAT.csv
|
||||
BOYCAR.csv
|
||||
...
|
||||
Option B — cell_id in subfolder:
|
||||
BAHIA_DE_CARTAGENA/BOYLAT.csv
|
||||
BAHIA_DE_CARTAGENA/BOYCAR.csv
|
||||
Option C — meta.json declares cell_id:
|
||||
meta.json → {"cell_id": "BAHIA_DE_CARTAGENA"}
|
||||
BOYLAT.csv
|
||||
...
|
||||
"""
|
||||
import csv as _csv_mod
|
||||
installed: list[str] = []
|
||||
with zipfile.ZipFile(zip_path) as z:
|
||||
namelist = z.namelist()
|
||||
|
||||
# Collect all CSV files grouped by subfolder
|
||||
csv_files = [n for n in namelist if n.lower().endswith(".csv")]
|
||||
if not csv_files:
|
||||
raise ValueError("No CSV files found in ZIP")
|
||||
|
||||
# Check for meta.json
|
||||
meta_cell_id: str | None = None
|
||||
if "meta.json" in namelist:
|
||||
try:
|
||||
meta_cell_id = json.loads(z.read("meta.json")).get("cell_id")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Group by directory prefix
|
||||
import collections
|
||||
groups: dict[str, list[str]] = collections.defaultdict(list)
|
||||
for name in csv_files:
|
||||
prefix = str(Path(name).parent)
|
||||
groups[prefix].append(name)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_root:
|
||||
tmp_root_p = Path(tmp_root)
|
||||
for prefix, members in groups.items():
|
||||
# Determine cell_id
|
||||
if meta_cell_id:
|
||||
cid = meta_cell_id
|
||||
elif prefix and prefix != ".":
|
||||
cid = Path(prefix).name
|
||||
else:
|
||||
cid = Path(zip_path).stem # e.g. BAHIA_DE_CARTAGENA
|
||||
# Extract CSVs to temp dir
|
||||
tmp_csv = tmp_root_p / cid
|
||||
tmp_csv.mkdir(exist_ok=True)
|
||||
for member in members:
|
||||
data = z.read(member)
|
||||
(tmp_csv / Path(member).name).write_bytes(data)
|
||||
# Install
|
||||
result_id = install_from_csv_dir(tmp_csv, cid)
|
||||
installed.append(result_id)
|
||||
return installed
|
||||
|
||||
def _meta_path(cell_dir: Path) -> Path:
|
||||
return cell_dir / "meta.json"
|
||||
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{"type": "FeatureCollection", "features": []}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"feature_count": 101, "bbox": [-75.992167, 9.591333, -75.0175, 10.963333], "region": "B"}
|
||||
@@ -0,0 +1 @@
|
||||
{"type": "FeatureCollection", "features": []}
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1 +1,10 @@
|
||||
{"feature_count": 75, "bbox": null, "region": "B"}
|
||||
{
|
||||
"region": "B",
|
||||
"feature_count": 124,
|
||||
"bbox": [
|
||||
-75.452898,
|
||||
10.627093,
|
||||
-74.629835,
|
||||
11.134
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1,9 @@
|
||||
{"feature_count": 549, "bbox": null}
|
||||
{
|
||||
"feature_count": 549,
|
||||
"bbox": [
|
||||
-98.116667,
|
||||
17.791497,
|
||||
-76.1,
|
||||
33.6085
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1,9 @@
|
||||
{"feature_count": 317, "bbox": null}
|
||||
{
|
||||
"feature_count": 317,
|
||||
"bbox": [
|
||||
-85.326389,
|
||||
19.143375,
|
||||
-73.286111,
|
||||
26.366669
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-87.6,
|
||||
26.4,
|
||||
-86.4,
|
||||
27.6
|
||||
]
|
||||
}
|
||||
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-86.4,
|
||||
26.4,
|
||||
-85.2,
|
||||
27.6
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-81.6,
|
||||
26.4,
|
||||
-80.4,
|
||||
27.6
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-87.6,
|
||||
27.6,
|
||||
-86.4,
|
||||
28.8
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.781684, 28.565406]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 4.3, "colours": [1], "colour_code": 1, "light_desc": "Iso((1)) W 6.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.779889, 28.505449]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 4.3, "colours": [1], "colour_code": 1, "light_desc": "Oc((1)) W 4.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7710187, 28.2855142]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 4.9, "range_nm": 5.0, "colours": [4], "colour_code": 4, "light_desc": "Fl((1)) G 6.0s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.690928, 27.9069481]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1, 4], "colour_code": 1, "light_desc": "19(()) W", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.760678, 27.600794]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 25.9, "range_nm": 13.0, "colours": [1], "colour_code": 1, "light_desc": "Fl((1)) W 15.0s 13.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7710187, 28.2855142]}, "properties": {"layer": "BCNLAT", "category": "beacon", "name": "Pithlachascotee River Light 1", "status": "1", "colours": [4], "colour_code": 4, "catlam": 1, "aid_type": "LATERAL_PORT", "light_desc": "Fl((1)) G 6.0s 5.0M", "range_nm": 5.0, "height_m": 4.9}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.690928, 27.9069481]}, "properties": {"layer": "LNDMRK", "category": "landmark", "name": "Saint Petersburg Airport Aero Light", "status": "1", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.5460057, 27.8612386]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "1", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.760678, 27.600794]}, "properties": {"layer": "LNDMRK", "category": "landmark", "name": "Egmont Key Light", "height_m": 25.9, "status": "1", "colours": [1], "colour_code": 1, "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7010796, 28.3304827]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.6977117, 28.3201107]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7677622, 28.182613]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.6965789, 28.3638625]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7885313, 28.1844825]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7229607, 28.2319711]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7118743, 28.283035]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "aid_type": "LANDMARK"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.781684, 28.565406]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 4.3, "colours": [1], "colour_code": 1, "light_desc": "Iso((1)) W 6.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.779889, 28.505449]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 4.3, "colours": [1], "colour_code": 1, "light_desc": "Oc((1)) W 4.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7710187, 28.2855142]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 4.9, "range_nm": 5.0, "colours": [4], "colour_code": 4, "light_desc": "Fl((1)) G 6.0s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.690928, 27.9069481]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1, 4], "colour_code": 1, "light_desc": "19(()) W", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.760678, 27.600794]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 25.9, "range_nm": 13.0, "colours": [1], "colour_code": 1, "light_desc": "Fl((1)) W 15.0s 13.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7710187, 28.2855142]}, "properties": {"layer": "BCNLAT", "category": "beacon", "name": "Pithlachascotee River Light 1", "status": "1", "colours": [4], "colour_code": 4, "light_desc": "Fl((1)) G 6.0s 5.0M", "catlam": 1, "aid_type": "LATERAL_PORT", "range_nm": 5.0, "height_m": 4.9}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.690928, 27.9069481]}, "properties": {"layer": "LNDMRK", "category": "landmark", "name": "Saint Petersburg Airport Aero Light", "status": "1", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.5460057, 27.8612386]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "1", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.760678, 27.600794]}, "properties": {"layer": "LNDMRK", "category": "landmark", "name": "Egmont Key Light", "height_m": 25.9, "status": "1", "colours": [1], "colour_code": 1, "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7010796, 28.3304827]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.6977117, 28.3201107]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7677622, 28.182613]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.6965789, 28.3638625]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7885313, 28.1844825]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7229607, 28.2319711]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7118743, 28.283035]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}]}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-79.2,
|
||||
27.66666,
|
||||
-78.0,
|
||||
28.8
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.3287, 29.6240833]}, "properties": {"layer": "BOYLAT", "category": "buoy", "name": "Cape San Blas Shoal Inner Buoy 1", "status": "1", "colours": [4], "colour_code": 4, "light_desc": "", "boyshp": 2, "shape": "Can(C)", "catlam": 1, "aid_type": "LATERAL_PORT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.3765836, 29.6390983]}, "properties": {"layer": "BOYLAT", "category": "buoy", "name": "Cape San Blas Shoal Inner Buoy 3", "status": "1", "colours": [4], "colour_code": 4, "light_desc": "", "boyshp": 2, "shape": "Can(C)", "catlam": 1, "aid_type": "LATERAL_PORT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.4861708, 29.9887006]}, "properties": {"layer": "LNDMRK", "category": "landmark", "height_m": 0.0, "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.3473009, 29.6799147]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.4938139, 29.9964746]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "aid_type": "LANDMARK"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.3287, 29.6240833]}, "properties": {"layer": "BOYLAT", "category": "buoy", "name": "Cape San Blas Shoal Inner Buoy 1", "status": "1", "colours": [4], "colour_code": 4, "light_desc": "", "boyshp": 2, "shape": "Can(C)", "catlam": 1, "aid_type": "LATERAL_PORT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.3765836, 29.6390983]}, "properties": {"layer": "BOYLAT", "category": "buoy", "name": "Cape San Blas Shoal Inner Buoy 3", "status": "1", "colours": [4], "colour_code": 4, "light_desc": "", "boyshp": 2, "shape": "Can(C)", "catlam": 1, "aid_type": "LATERAL_PORT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.4861708, 29.9887006]}, "properties": {"layer": "LNDMRK", "category": "landmark", "height_m": 0.0, "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.3473009, 29.6799147]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.4938139, 29.9964746]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}]}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.6302612, 29.6637761]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 6.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.1536833, 28.9753308]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 9.4, "range_nm": 7.0, "colours": [1], "colour_code": 1, "light_desc": "Fl((1)) W 6.0s 7.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.1308858, 29.1413733]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 4.9, "range_nm": 5.0, "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 2.5s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.049074, 29.1363891]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1, 4], "colour_code": 1, "light_desc": "19 W", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.6302612, 29.6637761]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fishermans Rest Fish Haven Lighted Buoy A", "status": "8", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 6.0s", "boyshp": 4, "shape": "Pillar", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.1308858, 29.1413733]}, "properties": {"layer": "BCNLAT", "category": "beacon", "name": "Cedar Keys Northwest Channel Approach Light 2", "status": "1", "colours": [3], "colour_code": 3, "catlam": 2, "aid_type": "LATERAL_STBD", "light_desc": "Fl((1)) R 2.5s 5.0M", "range_nm": 5.0, "height_m": 4.9}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.049074, 29.1363891]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "1", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.0656384, 29.0971088]}, "properties": {"layer": "LNDMRK", "category": "landmark", "name": "Old Tower", "info": "House", "status": "4", "colours": [], "aid_type": "LANDMARK"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.6302612, 29.6637761]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 6.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.1536833, 28.9753308]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 9.4, "range_nm": 7.0, "colours": [1], "colour_code": 1, "light_desc": "Fl((1)) W 6.0s 7.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.1308858, 29.1413733]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 4.9, "range_nm": 5.0, "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 2.5s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.049074, 29.1363891]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1, 4], "colour_code": 1, "light_desc": "19 W", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.6302612, 29.6637761]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fishermans Rest Fish Haven Lighted Buoy A", "status": "8", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 6.0s", "boyshp": 4, "shape": "Pillar", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.1308858, 29.1413733]}, "properties": {"layer": "BCNLAT", "category": "beacon", "name": "Cedar Keys Northwest Channel Approach Light 2", "status": "1", "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 2.5s 5.0M", "catlam": 2, "aid_type": "LATERAL_STBD", "range_nm": 5.0, "height_m": 4.9}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.049074, 29.1363891]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "1", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-83.0656384, 29.0971088]}, "properties": {"layer": "LNDMRK", "category": "landmark", "name": "Old Tower", "info": "House", "status": "4", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}]}
|
||||
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7843333, 28.8701667]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 3.7, "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 4.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7001159, 28.9570279]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [3], "colour_code": 3, "light_desc": "F R", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7007413, 28.9570244]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [3], "colour_code": 3, "light_desc": "F R", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.6967797, 28.9675486]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1], "colour_code": 1, "light_desc": "Q((1)) W 1.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.6967787, 28.9667224]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1], "colour_code": 1, "light_desc": "Q((1)) W 1.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.6967797, 28.9675486]}, "properties": {"layer": "LNDMRK", "category": "landmark", "height_m": 186.2, "status": "1", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.6967787, 28.9667224]}, "properties": {"layer": "LNDMRK", "category": "landmark", "height_m": 186.2, "status": "1", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7007413, 28.9570244]}, "properties": {"layer": "LNDMRK", "category": "landmark", "height_m": 155.4, "status": "1", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7001159, 28.9570279]}, "properties": {"layer": "LNDMRK", "category": "landmark", "height_m": 155.4, "status": "1", "colours": [], "aid_type": "LANDMARK"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7843333, 28.8701667]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 3.7, "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 4.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7001159, 28.9570279]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [3], "colour_code": 3, "light_desc": "F R", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7007413, 28.9570244]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [3], "colour_code": 3, "light_desc": "F R", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.6967797, 28.9675486]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1], "colour_code": 1, "light_desc": "Q((1)) W 1.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.6967787, 28.9667224]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1], "colour_code": 1, "light_desc": "Q((1)) W 1.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.6967797, 28.9675486]}, "properties": {"layer": "LNDMRK", "category": "landmark", "height_m": 186.2, "status": "1", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.6967787, 28.9667224]}, "properties": {"layer": "LNDMRK", "category": "landmark", "height_m": 186.2, "status": "1", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7007413, 28.9570244]}, "properties": {"layer": "LNDMRK", "category": "landmark", "height_m": 155.4, "status": "1", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.7001159, 28.9570279]}, "properties": {"layer": "LNDMRK", "category": "landmark", "height_m": 155.4, "status": "1", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}]}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-80.4,
|
||||
28.8,
|
||||
-79.2,
|
||||
30.0
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.1923323, 30.4770443]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1, 4], "colour_code": 1, "light_desc": "19(()) W", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.5483333, 30.06]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [6], "colour_code": 6, "light_desc": "Fl((4)) Y 20.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.5936819, 30.0485231]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1], "colour_code": 1, "light_desc": "Fl((1)) W 4.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.1794704, 30.4382338]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1], "colour_code": 1, "light_desc": "F W", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.5155486, 30.3707192]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1], "colour_code": 1, "light_desc": "Mo((A)) W 8.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.5483333, 30.06]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "NOAA Lighted Buoy 42012", "info": "Aid maintained by National Oceanic and Atmospheric Administration.", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((4)) Y 20.0s", "boyshp": 7, "shape": "Super-buoy", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.5936819, 30.0485231]}, "properties": {"layer": "BOYSPP", "category": "buoy", "info": "Navy maintained", "status": "8", "colours": [1], "light_desc": "Fl((1)) W 4.0s", "boyshp": 4, "shape": "Pillar", "aid_type": "SPECIAL", "colour_code": 1}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.805695, 30.21262]}, "properties": {"layer": "BOYSPP", "category": "buoy", "status": "8", "colours": [1, 11], "colour_code": 1, "light_desc": "", "boyshp": 4, "shape": "Pillar", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.5155486, 30.3707192]}, "properties": {"layer": "BOYSAW", "category": "buoy", "name": "Choctawhatchee Bay Entrance Buoy CB, MMSI 993682106", "info": "Automatic Identification System (AIS) aid to navigation.", "colours": [3, 1], "colour_code": 3, "light_desc": "Mo((A)) W 8.0s", "boyshp": 4, "shape": "Pillar", "aid_type": "SAFE_WATER"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.1923323, 30.4770443]}, "properties": {"layer": "LNDMRK", "category": "landmark", "name": "Pensacola Regional Airport Aero Light", "status": "1", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.6276875, 30.4138589]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "1", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.1794704, 30.4382338]}, "properties": {"layer": "LNDMRK", "category": "landmark", "info": "Aero", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.14995296984341, 30.333726532657202]}, "properties": {"layer": "LAKARE", "category": "area", "colours": [], "aid_type": "UNKNOWN"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.1923323, 30.4770443]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1, 4], "colour_code": 1, "light_desc": "19(()) W", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.5483333, 30.06]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [6], "colour_code": 6, "light_desc": "Fl((4)) Y 20.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.5936819, 30.0485231]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1], "colour_code": 1, "light_desc": "Fl((1)) W 4.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.1794704, 30.4382338]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1], "colour_code": 1, "light_desc": "F W", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.5155486, 30.3707192]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1], "colour_code": 1, "light_desc": "Mo((A)) W 8.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.5483333, 30.06]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "NOAA Lighted Buoy 42012", "info": "Aid maintained by National Oceanic and Atmospheric Administration.", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((4)) Y 20.0s", "boyshp": 7, "shape": "Super-buoy", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.5936819, 30.0485231]}, "properties": {"layer": "BOYSPP", "category": "buoy", "info": "Navy maintained", "status": "8", "colours": [1], "light_desc": "Fl((1)) W 4.0s", "boyshp": 4, "shape": "Pillar", "aid_type": "SPECIAL", "colour_code": 1}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.805695, 30.21262]}, "properties": {"layer": "BOYSPP", "category": "buoy", "status": "8", "colours": [1, 11], "colour_code": 1, "light_desc": "", "boyshp": 4, "shape": "Pillar", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.5155486, 30.3707192]}, "properties": {"layer": "BOYSAW", "category": "buoy", "name": "Choctawhatchee Bay Entrance Buoy CB, MMSI 993682106", "info": "Automatic Identification System (AIS) aid to navigation.", "colours": [3, 1], "colour_code": 3, "light_desc": "Mo((A)) W 8.0s", "boyshp": 4, "shape": "Pillar", "aid_type": "SAFE_WATER"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.1923323, 30.4770443]}, "properties": {"layer": "LNDMRK", "category": "landmark", "name": "Pensacola Regional Airport Aero Light", "status": "1", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.6276875, 30.4138589]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "1", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.1794704, 30.4382338]}, "properties": {"layer": "LNDMRK", "category": "landmark", "info": "Aero", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-87.14995296984341, 30.333726532657202]}, "properties": {"layer": "LAKARE", "category": "area", "colours": [], "aid_type": "UNKNOWN"}}]}
|
||||
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.77382, 30.0915931]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1], "colour_code": 1, "light_desc": "Mo((A)) W 8.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.8307833, 30.0796]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [6], "colour_code": 6, "light_desc": "Fl((5)) Y 20.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.8307833, 30.0796]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "U.S. Navy ODAS Lighted Buoy A", "info": "Buoy maintained by U.S. Navy", "status": "8", "colours": [6], "colour_code": 6, "light_desc": "Fl((5)) Y 20.0s", "boyshp": 7, "shape": "Super-buoy", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.77382, 30.0915931]}, "properties": {"layer": "BOYSAW", "category": "buoy", "name": "Saint Andrew Bay Entrance Lighted Buoy SA", "info": "Automatic Identification System (AIS) aid to navigation.", "status": "1", "colours": [3, 1], "colour_code": 3, "light_desc": "Mo((A)) W 8.0s", "boyshp": 4, "shape": "Pillar", "aid_type": "SAFE_WATER"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.3077728, 30.3766086]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.7010957, 30.1683154]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.620107, 30.1419212]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "aid_type": "LANDMARK"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.77382, 30.0915931]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [1], "colour_code": 1, "light_desc": "Mo((A)) W 8.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.8307833, 30.0796]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [6], "colour_code": 6, "light_desc": "Fl((5)) Y 20.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.8307833, 30.0796]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "U.S. Navy ODAS Lighted Buoy A", "info": "Buoy maintained by U.S. Navy", "status": "8", "colours": [6], "colour_code": 6, "light_desc": "Fl((5)) Y 20.0s", "boyshp": 7, "shape": "Super-buoy", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.77382, 30.0915931]}, "properties": {"layer": "BOYSAW", "category": "buoy", "name": "Saint Andrew Bay Entrance Lighted Buoy SA", "info": "Automatic Identification System (AIS) aid to navigation.", "status": "1", "colours": [3, 1], "colour_code": 3, "light_desc": "Mo((A)) W 8.0s", "boyshp": 4, "shape": "Pillar", "aid_type": "SAFE_WATER"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-86.3077728, 30.3766086]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.7010957, 30.1683154]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-85.620107, 30.1419212]}, "properties": {"layer": "LNDMRK", "category": "landmark", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}]}
|
||||
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-84.3591561, 30.3236642]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "1", "colours": [], "aid_type": "LANDMARK"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-84.3591561, 30.3236642]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "1", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}]}
|
||||
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-84.0,
|
||||
30.0,
|
||||
-82.8,
|
||||
31.2
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-80.3165198, 30.7999068]}, "properties": {"layer": "LNDMRK", "category": "landmark", "name": "Navy Tower R4", "status": "1", "colours": [], "aid_type": "LANDMARK"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-80.3165198, 30.7999068]}, "properties": {"layer": "LNDMRK", "category": "landmark", "name": "Navy Tower R4", "status": "1", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}]}
|
||||
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-79.2,
|
||||
30.0,
|
||||
-78.0,
|
||||
31.2
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-83.249821,
|
||||
28.165554,
|
||||
-83.1,
|
||||
28.8
|
||||
]
|
||||
}
|
||||
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-83.566505,
|
||||
28.900245,
|
||||
-83.4,
|
||||
29.1
|
||||
]
|
||||
}
|
||||
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-83.916666,
|
||||
29.299999,
|
||||
-83.7,
|
||||
29.7
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.873742, 24.593639]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 5.8, "range_nm": 5.0, "colours": [4], "colour_code": 4, "light_desc": "Fl((1)) G 2.5s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9002833, 24.5665833]}, "properties": {"layer": "LIGHTS", "category": "light", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 2.5s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9668833, 24.5666833]}, "properties": {"layer": "LIGHTS", "category": "light", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 4.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.83355, 24.59975]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Buoy P", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.8667167, 24.5834]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Buoy Q", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9668833, 24.5666833]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Lighted Buoy C", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 4.0s", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9333833, 24.5666167]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Buoy B", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9002833, 24.5665833]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Lighted Buoy A", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 2.5s", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.873742, 24.593639]}, "properties": {"layer": "BCNLAT", "category": "beacon", "name": "Dry Tortugas Southeast Channel Light 1", "status": "1", "colours": [4], "colour_code": 4, "catlam": 1, "aid_type": "LATERAL_PORT", "light_desc": "Fl((1)) G 2.5s 5.0M", "range_nm": 5.0, "height_m": 5.8}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.873742, 24.593639]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 5.8, "range_nm": 5.0, "colours": [4], "colour_code": 4, "light_desc": "Fl((1)) G 2.5s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9002833, 24.5665833]}, "properties": {"layer": "LIGHTS", "category": "light", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 2.5s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9668833, 24.5666833]}, "properties": {"layer": "LIGHTS", "category": "light", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 4.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.83355, 24.59975]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Buoy P", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.8667167, 24.5834]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Buoy Q", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9668833, 24.5666833]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Lighted Buoy C", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 4.0s", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9333833, 24.5666167]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Buoy B", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9002833, 24.5665833]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Lighted Buoy A", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 2.5s", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.873742, 24.593639]}, "properties": {"layer": "BCNLAT", "category": "beacon", "name": "Dry Tortugas Southeast Channel Light 1", "status": "1", "colours": [4], "colour_code": 4, "light_desc": "Fl((1)) G 2.5s 5.0M", "catlam": 1, "aid_type": "LATERAL_PORT", "range_nm": 5.0, "height_m": 5.8}}]}
|
||||
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.4737972, 24.5582722]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 5.8, "range_nm": 5.0, "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 6.0s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.42545, 24.4300667]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 4.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.42545, 24.4300667]}, "properties": {"layer": "BOYLAT", "category": "buoy", "name": "Twenty Eight Foot Shoal Lighted Buoy 2", "status": "1", "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 4.0s", "boyshp": 4, "shape": "Pillar", "catlam": 2, "aid_type": "LATERAL_STBD"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.4737972, 24.5582722]}, "properties": {"layer": "BCNLAT", "category": "beacon", "name": "Halfmoon Shoal Light WR2", "status": "1", "colours": [3], "colour_code": 3, "catlam": 2, "aid_type": "LATERAL_STBD", "light_desc": "Fl((1)) R 6.0s 5.0M", "range_nm": 5.0, "height_m": 5.8}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.3333882, 24.5568912]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "12", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.4383872, 24.5434524]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "12", "colours": [], "aid_type": "LANDMARK"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.4737972, 24.5582722]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 5.8, "range_nm": 5.0, "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 6.0s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.42545, 24.4300667]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 4.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.42545, 24.4300667]}, "properties": {"layer": "BOYLAT", "category": "buoy", "name": "Twenty Eight Foot Shoal Lighted Buoy 2", "status": "1", "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 4.0s", "boyshp": 4, "shape": "Pillar", "catlam": 2, "aid_type": "LATERAL_STBD"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.4737972, 24.5582722]}, "properties": {"layer": "BCNLAT", "category": "beacon", "name": "Halfmoon Shoal Light WR2", "status": "1", "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 6.0s 5.0M", "catlam": 2, "aid_type": "LATERAL_STBD", "range_nm": 5.0, "height_m": 5.8}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.3333882, 24.5568912]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "12", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.4383872, 24.5434524]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "12", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}]}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-81.0,
|
||||
24.3,
|
||||
-80.7,
|
||||
24.6
|
||||
]
|
||||
}
|
||||
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-80.7,
|
||||
24.3,
|
||||
-80.4,
|
||||
24.6
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.82945, 24.6232]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 4.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.861341, 24.638735]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 4.9, "range_nm": 5.0, "colours": [4], "colour_code": 4, "light_desc": "Fl((1)) G 4.0s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.8003333, 24.6164667]}, "properties": {"layer": "LIGHTS", "category": "light", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 6.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.82945, 24.6232]}, "properties": {"layer": "BOYLAT", "category": "buoy", "name": "Dry Tortugas Southeast Channel Lighted Buoy 2", "status": "1", "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 4.0s", "boyshp": 4, "shape": "Pillar", "catlam": 2, "aid_type": "LATERAL_STBD"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.8003333, 24.6164667]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Lighted Buoy O", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 6.0s", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9667333, 24.6082833]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Buoy D", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.861341, 24.638735]}, "properties": {"layer": "BCNLAT", "category": "beacon", "name": "Dry Tortugas Southeast Channel Light 3", "status": "1", "colours": [4], "colour_code": 4, "catlam": 1, "aid_type": "LATERAL_PORT", "light_desc": "Fl((1)) G 4.0s 5.0M", "range_nm": 5.0, "height_m": 4.9}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9205413, 24.6333357]}, "properties": {"layer": "LNDMRK", "category": "landmark", "info": "Aband LT House", "colours": [], "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.872267, 24.628172]}, "properties": {"layer": "LNDMRK", "category": "landmark", "info": "House", "status": "4", "colours": [], "aid_type": "LANDMARK"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.82945, 24.6232]}, "properties": {"layer": "LIGHTS", "category": "light", "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 4.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.861341, 24.638735]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 4.9, "range_nm": 5.0, "colours": [4], "colour_code": 4, "light_desc": "Fl((1)) G 4.0s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.8003333, 24.6164667]}, "properties": {"layer": "LIGHTS", "category": "light", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 6.0s", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.82945, 24.6232]}, "properties": {"layer": "BOYLAT", "category": "buoy", "name": "Dry Tortugas Southeast Channel Lighted Buoy 2", "status": "1", "colours": [3], "colour_code": 3, "light_desc": "Fl((1)) R 4.0s", "boyshp": 4, "shape": "Pillar", "catlam": 2, "aid_type": "LATERAL_STBD"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.8003333, 24.6164667]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Lighted Buoy O", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "Fl((1)) Y 6.0s", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9667333, 24.6082833]}, "properties": {"layer": "BOYSPP", "category": "buoy", "name": "Fort Jefferson Buoy D", "status": "1", "colours": [6], "colour_code": 6, "light_desc": "", "boyshp": 2, "shape": "Can(C)", "aid_type": "SPECIAL"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.861341, 24.638735]}, "properties": {"layer": "BCNLAT", "category": "beacon", "name": "Dry Tortugas Southeast Channel Light 3", "status": "1", "colours": [4], "colour_code": 4, "light_desc": "Fl((1)) G 4.0s 5.0M", "catlam": 1, "aid_type": "LATERAL_PORT", "range_nm": 5.0, "height_m": 4.9}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.9205413, 24.6333357]}, "properties": {"layer": "LNDMRK", "category": "landmark", "info": "Aband LT House", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.872267, 24.628172]}, "properties": {"layer": "LNDMRK", "category": "landmark", "info": "House", "status": "4", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}]}
|
||||
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.4441597, 24.6666069]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 5.8, "range_nm": 5.0, "colours": [1], "colour_code": 1, "light_desc": "Fl((1)) W 4.0s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.2864332, 24.6797895]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "12", "colours": [], "aid_type": "LANDMARK"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.4441597, 24.6666069]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 5.8, "range_nm": 5.0, "colours": [1], "colour_code": 1, "light_desc": "Fl((1)) W 4.0s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.2864332, 24.6797895]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "12", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}]}
|
||||
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.184248, 24.6490071]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 4.9, "range_nm": 5.0, "colours": [1], "colour_code": 1, "light_desc": "Fl((1)) W 2.5s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-81.921725, 24.7182208]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 16.5, "range_nm": 9.0, "colours": [1], "colour_code": 1, "light_desc": "Fl((1)) W 6.0s 9.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.0731764, 24.6214309]}, "properties": {"layer": "BCNLAT", "category": "beacon", "name": "Boca Grande Channel Daybeacon 1", "status": "1", "colours": [4], "colour_code": 4, "catlam": 1, "aid_type": "LATERAL_PORT"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.184248, 24.6490071]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 4.9, "range_nm": 5.0, "colours": [1], "colour_code": 1, "light_desc": "Fl((1)) W 2.5s 5.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-81.921725, 24.7182208]}, "properties": {"layer": "LIGHTS", "category": "light", "height_m": 16.5, "range_nm": 9.0, "colours": [1], "colour_code": 1, "light_desc": "Fl((1)) W 6.0s 9.0M", "aid_type": "LIGHT_POINT"}}, {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.0731764, 24.6214309]}, "properties": {"layer": "BCNLAT", "category": "beacon", "name": "Boca Grande Channel Daybeacon 1", "status": "1", "colours": [4], "colour_code": 4, "light_desc": "", "catlam": 1, "aid_type": "LATERAL_PORT"}}]}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-80.4,
|
||||
24.6,
|
||||
-80.1,
|
||||
24.9
|
||||
]
|
||||
}
|
||||
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-80.1,
|
||||
24.6,
|
||||
-79.8,
|
||||
24.9
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.0013157, 25.1181091]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "12", "colours": [], "aid_type": "LANDMARK"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.0013157, 25.1181091]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "12", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}]}
|
||||
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-81.9,
|
||||
24.9,
|
||||
-81.6,
|
||||
25.2
|
||||
]
|
||||
}
|
||||
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-81.6,
|
||||
24.9,
|
||||
-81.3,
|
||||
25.2
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-80.1,
|
||||
24.9,
|
||||
-79.8,
|
||||
25.2
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.1017157, 25.4684687]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "12", "colours": [], "aid_type": "LANDMARK"}}]}
|
||||
{"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-82.1017157, 25.4684687]}, "properties": {"layer": "LNDMRK", "category": "landmark", "status": "12", "colours": [], "light_desc": "", "aid_type": "LANDMARK"}}]}
|
||||
@@ -1 +1,9 @@
|
||||
{"feature_count": 0, "bbox": null}
|
||||
{
|
||||
"feature_count": 0,
|
||||
"bbox": [
|
||||
-81.9,
|
||||
25.2,
|
||||
-81.6,
|
||||
25.5
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user