[general]
name=AIS Watchkeeper
qgisMinimumVersion=3.22
description=Receive, decode and plot live AIS vessel traffic from serial or TCP sources, with guard-zone alarms and coverage mapping.
about=AIS Watchkeeper turns QGIS into a live maritime traffic console. It
    connects to an AIS receiver over USB/serial or a raw TCP stream, decodes
    !AIVDM/!AIVDO sentences, and plots vessels live on the map with trails,
    speed vectors and a learned vessel registry. The built-in AIS Guardian
    raises alarms on guard-zone entry/exit and navigation-status changes, maps
    Class A/B reception coverage, and lists nearby vessels with CPA/TCPA.
    Requires pyserial and pyais (bundled).
version=0.43.0
changelog=0.43.0 - Strip & settings polish. (1) On the route-leg strip, the ship
    SYMBOLS now declutter like the labels do: where triangles crowd along the
    axis they fan out perpendicular on the same side, ordered by speed - the
    slowest stays nearest the route line, the fastest goes furthest out - and
    each drops back to the line as soon as it no longer overlaps a neighbour.
    Names follow their (possibly pushed-out) triangle. (2) The AIS stream
    Settings box is reorganised into two tight columns with the toggles side by
    side, removing the dead space.
    0.42.0 - Custom routes (for users outside the bundled network). The
    whole RWS FIS-VNDS route network is bundled in data/routes.gpkg and ships in
    the plugin. NEW: an editable QGIS "Custom routes" LineString layer - draw a
    line near where ships sail, name it, and it becomes a fairway you can bind to.
    The drawn direction (first vertex to last) is treated as INBOUND, the reverse
    as outbound (used for the start/end-based in/out counts). Custom routes show
    in the Routes tab in BLUE (marked "(custom)") and take binding PRIORITY: when
    a vessel is within tolerance of both a custom and a native route, it binds to
    the custom one (native binding is unchanged when no custom route is in range).
    Custom routes persist in custom_routes.gpkg in the profile dir (user data) and
    reload next start. Mainly to let people outside the EU define their own routes.
    0.41.0 - Per-route bind width. Tidal fairways are wide (the
    Westerschelde mouth is nearly 1 km across) and ships legally sail well off
    the centreline, so a single 150 m bind tolerance dropped them as off-fairway.
    The Routes table now has an editable "Bind width (m)" box per route: widen it
    (e.g. 650 m) for a broad tidal fairway and vessels sailing off-axis bind
    there, while narrow canals/parallel routes keep the tight default so they
    don't mis-bind. The route a vessel lands on is still chosen by nearest +
    course alignment, so a wider tolerance never steals a vessel that is closer
    to another route; an off-centre bind just reads as lower confidence. Values
    persist per route in route_tol.json (profile dir) and reload next start.
    0.40.0 - User reference markers on the route. A long fairway like the
    Westerschelde (~105 km, no locks or bridges) had nothing on its strip to map
    km to the real world. Now there's an editable QGIS point layer "Route
    reference markers": drop a point near a fairway, name it (a buoy, a VTS sector
    boundary, a berth) with QGIS's native editing, and the route manager portrays
    it on the strip exactly like a bridge or lock - snapped to the nearest route
    and route-km (teal -). Markers persist in markers.gpkg in the profile dir
    (user data, survives plugin upgrades) and reload next start. They are also
    wired into route-CPA: pick a vessel then a marker to get the vessel's ETA /
    distance-to-go to that marker, just like bridges and locks.
    0.39.0 - Two fairway datasets, each for what it is best at. BINDING +
    ROUTE MANAGER now run on the Rijkswaterstaat FIS-VNDS "route" layer: full-
    stretch, properly-named routes (e.g. id 20940 = "Westerschelde", ~105 km as
    ONE line) with linear-referencing km. The fine-grained "vaarwegen" passages
    are now a DISPLAY-only chart layer - each individual stretch labelled with its
    own name (Honte, Pas van Terneuzen, Vlissingen Rede, ...), thin, drawn only
    when zoomed in past 1:12500. We do NOT trust the service's own "routejunctie"
    layer; instead route_ingest.py passes over the route geometries and flags a
    connection only where >=2 routes actually share a vertex (~2 m) or a dangling
    endpoint lands on another route's interior (<=10 m T-branch). The result
    (2056 routes, ~2240 derived junctions) is baked into a bundled resource
    data/routes.gpkg so binding runs fully offline; re-run route_ingest to refresh
    from the live service. This supersedes the 0.38.1 per-route name override. The
    old on-canvas route polyline layer ("Routes in coverage") and its click-picker
    are removed - the route UX is the Routes table. Also: incoming sentences with
    a leading NMEA 4.0 / IEC 61162-1 TAG block (\\s:...,c:...*HH\\, added by many
    aggregators & multiplexers) are now stripped before classify/validate/decode -
    such feeds previously decoded almost nothing; live-tested at >4000 vessels.
    0.38.1 - Routelines tidy-up: the on-canvas reference lines are half as thick
    (0.45 mm) and only draw when zoomed in past 1:12500.
    0.38.0 - Route binding now uses the vessel's track, not a single
    fix. A vessel is bound only after at least 3 position hits, and the route it
    lands on is ranked by the COURSE MADE GOOD across those hits (the leg
    bearings between them) rather than one reported COG - so when several routes
    run nearby, it binds to the one sharing the stretch the ship is actually
    travelling, not a closer crossing/parallel fairway. A vessel under 2 kn for
    more than 3 consecutive hits is DETACHED from its route (moored/anchored
    vessels stop being glued to whatever fairway they sit beside); it re-binds
    once it is moving again with a fresh 3-hit track. Reported COG is kept only
    as a fallback before a track course exists. Tunable via MapMatcher
    (min_track_hits, slow_speed_kn, slow_detach_hits, min_move_m).
    0.37.1 - Fix a Windows crash (access violation) when disconnecting /
    unplugging a serial AIS receiver. The reader thread was blocked in pyserial's
    overlapped read (GetOverlappedResult) while the port handle was closed/
    finalised underneath it. SerialReader.stop() now aborts the in-flight read
    the thread-safe way via cancel_read() (CancelIoEx) and waits for the thread to
    finish; the run loop clears self._serial in finally and stays quiet on a
    stop-induced abort (reporting a clear "device disconnected?" only on a real
    fault). The dialog keeps a reference to a retiring reader until its thread
    ends, so it can't be garbage-collected (and close its port) mid-read. The
    exact unplug-during-read fault lives inside pyserial's C call and can't be
    fully caught in Python, but the close-under-read / GC races (the common crash)
    are gone and ordinary removals unwind cleanly. Proven 12/12 (clean stop via
    cancel_read, device-removed graceful unwind, stop-before-start safe).
    0.37.0 - Routes UX: anti-collision strip names + a Routes table; map
    routes are now quiet & non-interactive. (1) Strip ship-names no longer
    overlap - they behave like objects that can't collide, pushed out
    perpendicular to the route line by the FULL length of the name before them
    (greedy packing on each side via font metrics; the scene grows to fit dense
    clusters). Replaces the too-small fixed stagger. (2) On the chart the
    in-coverage routes are now drawn as a quiet THIN DASHED reference layer
    (0.9 px) and are NON-interactive - the map-click tool/filter is removed
    (it was awkward). (3) New "Routes" tab after Vessels: a table (Fairway |
    Inbound | Outbound | Idle) of routes with vessels in coverage, sorted by
    moving vessels (in+out) desc; clicking a row opens that route's leg in the
    route manager. New engine RouteMapModel.route_table(). Route-CPA chart
    portrayal is unchanged - it still draws when you run a CPA inside a leg.
    Proven headless 15/15 (route_table counts/sort/idle; anti-collision stacking;
    Routes tab populate/sort/row-open; dashed thinner layer); full suite 129/129.
    0.36.0 - TCP login: credentials via QGIS's native encrypted auth store.
    The security dialog now uses QgsAuthConfigSelect to pick or create a stored
    credential; the secret lives ENCRYPTED in QGIS's auth database (master-
    password protected) and is decrypted only at connect to render the login -
    the plugin never holds it beyond the session and never writes it to its own
    files. A session-only manual fallback remains for a quick test. Per host:port
    the dialog remembers (in QgsSettings) the non-secret bits - the auth config
    ID, TLS flags, login template and method - so the next connect pre-fills;
    the secret stays in the auth DB, only the ID is remembered. Verified headless
    9/9 in an isolated temp auth DB: master pw set, credential stored, decrypt
    round-trip, dialog renders the login from the stored config, the id (not the
    secret) is remembered, and a scan of the on-disk DB confirms the secret is
    NOT in plaintext. Full suite 114/114. (QgsAuthConfigSelect's add/edit editor
    + the live authenticated connect still want an in-QGIS run.)
    0.35.0 - TCP source: scan the endpoint for security + login dialog.
    When you Connect to a Network (TCP) source, the plugin now PROBES host:port
    first (new endpoint_probe.py, pure-stdlib, off the wire it only listens):
    (1) reachable? (2) TLS? - it attempts a TLS handshake; a ServerHello means
    the endpoint is TLS, otherwise it re-probes as plaintext; (3) auth likely? -
    if no NMEA streams on connect (and/or a text banner/login prompt arrives).
    If the endpoint looks secured (TLS or auth-likely) a login dialog opens,
    pre-filled from the scan: a TLS toggle (+ allow self-signed), an auth method
    (none / username+password / token), and a server-specific login-line template
    with {user}/{pass}/{token} placeholders (there is no standard AIS login, so
    the exact line(s) are configurable). TcpReader gained use_tls / tls_insecure
    / login_lines: it wraps the socket in TLS and sends the rendered login on
    connect. The probe is best-effort and gated on 'reachable AND secured', so a
    normal plaintext feed connects exactly as before. Credentials are used for
    the session only (not written to disk; encrypted-store persistence is a
    later refinement). Addresses the 2026-06-25 audit's 'TCP feed is cleartext /
    no auth' note. Proven headless: probe 14/14 (reachable / TLS-fallback /
    streaming-NMEA / silent / banner / unreachable; TLS-positive path complete,
    skipped live for lack of a test cert), dialog+reader 8/8 (TLS prefill, method
    switching, login rendering, login sent CRLF on connect). Suite 105/105 green.
    Live test wants a real TLS / authenticated AIS server. 0.34.0 - Route-leg window: wheel-zoom + drag-pan restored. The strip
    still opens at the panel-fit 1:1 layout (the start point), but the user can
    now wheel-zoom about the cursor and drag-pan from there. The zoom/pan is
    PRESERVED across the 2 s refresh and CPA/ETA redraws; only an explicit fit
    (open a route / Refresh / resize / Rotate) snaps back to the panel-fit. Done
    by resetting the view transform only on fit=True (the tick keeps it) and
    re-enabling wheel-zoom in the strip. Constant-size labels stay readable at
    any zoom. Proven 6/6 (zoom enabled, fit start 1:1, preserved across
    tick/redraw, reset on explicit fit); full suite 77/77.
    0.33.1 - Fix: could not start a route-CPA (regression from 0.33.0).
    The second pick was never recognised - _on_click called _exit_select()
    (which clears _cpa_pick) BEFORE _resolve_cpa_picks() read the picks, so it
    always fell through to "pick two vessels...". Now the two picks are snapshotted
    before _exit_select(); _resolve_cpa_picks takes them as an argument. Covered by
    a new two-click regression test (vessel+vessel meeting and vessel+bridge ETA,
    with a 2 s tick between picks). 4/4 + suite 73/73.
    0.33.0 - Routes-on-map UX refinements + route-CPA to bridges/locks.
    (1) The in-coverage routes layer now AUTO-REFRESHES every ~10 s (and the
    leg-window detail redraws live), so it tracks coverage without any button.
    (2) The "Routes on map" button is REMOVED - routes show automatically; this
    is gated on the fairway matcher already existing (built by binding), so it
    never forces a build or hangs the GUI at startup. (3) Clicking a route now
    uses a canvas event filter (RouteClickFilter) instead of a custom map tool:
    it never consumes the event, so the user's normal pan/zoom/identify keeps
    working and routes stay clickable regardless of the active tool (and it
    survives tool switches). (4) NEW route-CPA to fixed objects: in the leg's
    Route-CPA mode you can pick a vessel + a bridge/lock and get that vessel's
    ETA + distance-to-go to the structure (engine RouteMapModel.object_eta);
    drawn as a label at the object and in the header. Picking two vessels still
    gives the meeting CPA. Also fixed: the strip header is now context-aware
    (route/km, or the active CPA/ETA) so it no longer gets clobbered on redraw.
    Proven headless 73/73 across 7 suites (object_eta in/out/stationary; routes
    layer; window open-to-detail; orientation; CPA; vars; rule). The on-canvas
    click + auto-refresh wiring wants an in-QGIS click-through.
    0.32.0 - Routes drawn on the QGIS map; click a route to open its leg.
    The in-window octolinear schematic overview is RETIRED. The "Routes on map"
    button (was "Route map") now draws the in-coverage routes straight on the
    QGIS canvas at their real lon/lat ("Routes in coverage" layer - coloured per
    route, named, carrying routeid). New engine RouteMapModel.coverage_polylines()
    returns each in-coverage route's segments. Clicking a route on the map opens
    that route's leg in the route-leg window (the strip) via RouteClickTool - an
    always-on map tool that SUBCLASSES QgsMapToolPan so panning still works (drag
    pans; a no-drag click on a route opens it, hit-tested via QgsMapToolIdentify).
    The route-leg window is now detail-strip only (open_route(rid)). Proven
    headless 17/17 (coverage polylines in/out of range; layer build, attrs,
    geometry, idempotent rebuild; window opens to detail; tool subclasses pan);
    regression 46/46. The map-tool click flow (mouse events, identify tolerance,
    CRS) is compile-checked and wants an in-QGIS click-through.
    0.31.0 - Route monitor: portrait/landscape re-layout + panel-bound
    sizing. (1) Portrait is now a REAL re-layout (the km-axis runs vertically),
    not a view rotation - so in portrait ALL labels read normally (horizontal),
    fixing the unreadable rotated labels. Landscape keeps vertical labels along
    the horizontal axis. The whole strip is drawn in (along, perp) coordinates
    mapped by orientation, so one code path serves both. (2) The route axis now
    fills the panel's long side (width in landscape, height in portrait) at 1:1,
    floored at 500 px; a smaller panel scrolls (scrollbars) instead of shrinking.
    Wheel-zoom is disabled in the strip (it scrolls instead); the view re-fits on
    panel resize. The "Rotate" button toggles portrait/landscape. Proven headless
    8/8 (axis orientation, label orientation per mode, opposite-side placement,
    object line on the axis, 500 px floor); regression 38/38. 0.30.0 - Route monitor (strip) visual overhaul. (1) The vertical layout
    is now laid out in SCREEN pixels (offsets divided by the live zoom) and
    redraws on zoom, so spacing looks the same at any zoom and labels no longer
    collide/overlap when zoomed out. (2) More vertical breathing room overall.
    (3) The symbol-key legend moved OUT of the canvas into a real label under the
    view - it can no longer collide with vessel/object labels and is never
    truncated. (4) All ship-name labels now read the same way as the bridge/lock
    labels (rotated -90, upward). (5) Declutter stagger is now zoom-aware (screen-
    gap threshold, up to 5 levels) so crowded names step apart even when zoomed
    out. (6) NEW: a "Rotate" button in the toolbar rotates the whole view 90 deg to
    toggle landscape <-> portrait. Bridge/lock markers keep the touch-axis +
    dot + name-at-top behaviour, now positioned in the same screen-space scheme.
    Proven headless: route-map polish 11/11 + object marker 7/7 + regression
    38/38. 0.29.4 - Renamed the visible UI from "AIS NMEA Source" to "AIS
    Watchkeeper": the docked panel title (the QGIS tab), the Plugins-menu entry
    and the toolbar/menu action all now read "AIS Watchkeeper". Internal package
    name (ais_nmea_source) and the dock objectName are unchanged (load-bearing).
    0.29.3 - Completed the route-CPA vessel highlight: the blue dot is now
    a rule ON THE LIVE AIS LAYER. vessel_layer._style, after loading the QML,
    converts the renderer to rule-based (which preserves all authored ship-type
    symbology exactly) and appends ONE rule that draws a blue circle on any
    target whose vessel_stationID matches @routeCPAVesselOne / @routeCPAVesselTwo
    (filter uses array_contains+coalesce so empty/unset variables = no dots).
    Idempotent (never double-adds) and best-effort. So the highlight now rides
    the real moving target. Proven 8/8 (rule present + base symbology preserved +
    matches/excludes/cleared); route-map side unchanged (37/37). 0.29.2 - Route-CPA vessel highlight reworked to a project-variable +
    symbology-rule scheme. When a route-CPA is active the two vessels' MMSIs are
    written to project variables @routeCPAVesselOne / @routeCPAVesselTwo (blanked
    when the CPA is cleared); the old blue-ring "Route CPA vessels" overlay layer
    is retired. The blue dot is now drawn by a rule on the LIVE AIS layer's own
    symbology (matching vessel_stationID against the two variables) - so it rides
    the real moving target, not a static snapped point. (This window writes the
    variables; the symbology rule lives on the liveFeedAIS layer / its QML.)
    Variable set/clear + old-layer retirement proven 4/4. 0.29.1 - Bridge/lock marker tweak: the dotted line is now SHORT - it
    touches the route axis and rises only to just below the name, where it ends
    in a small dot, then a gap, then the object name (instead of running far up
    past it). Verified (7/7). 0.29.0 - Bridges & locks on the route monitor (live from the RWS
    FIS-VNDS WFS). Opening a route's strip now fetches that route's bridges
    (layer 'brug') and locks ('sluiskolk') from the FIS-VNDS WFS, server-side
    filtered by routeid (so only the few on that route are fetched), and draws
    each as a dotted line that touches the route axis and runs far upward, with
    the object name at the top - above the ship labels - read along the line.
    Purple = bridge, orange = lock. The objects carry routeid + route-km in the
    same scheme as the bundled fairway network, so they place by km directly (no
    geometry snapping). This is the plugin's first network call: it is per-route,
    cached, time-limited and fully best-effort - offline or a WFS hiccup just
    leaves the strip without the markers and never blocks the live AIS feed.
    Proven: pure GeoJSON parser, a live fetch (route 31596 -> 24 bridges), and
    the strip rendering (12/12); the earlier route-viewer/CPA work still passes
    (36/36). 0.28.1 - Route viewer / route-CPA interaction tweaks. (1) The two
    vessels in an active route-CPA are now highlighted on the main QGIS map with
    a blue ring ("Route CPA vessels" layer). (2) In the strip view, vessel names
    are rotated 180 degrees (still outbound-above / inbound-below, placed via
    their bounding rect so each stays fully on its own side). (3) Click a ship in
    the strip -> the main QGIS map recentres + zooms to that ship (snapped
    position) and flashes it. (4) Click the route-CPA bar in the strip -> the
    main map zooms to the CPA meeting point. (5) The strip legend moved lower
    (clear of the inbound names) in a larger, constant-size font. All five
    verified headless (10/10) with no regression to 0.28.0 (26/26 still green).
    0.28.0 - Route viewer + route-CPA display upgrades. Route viewer:
    (1) vessel names now sit on the SAME side as the vessel's lane - outbound
    above the axis, inbound below - instead of all on one side, which declutters
    the strip; near-neighbour names additionally stack outward. (2) The marker
    colour is now a speed heat ramp (green at 2 kn -> red at 20 kn, clamped red
    above 20). (3) Vessels slower than 2 kn are dropped from the strip
    (unknown-speed targets are always kept). Route CPA: now also predicts the
    OVERTAKING case - two vessels on the same course, where the faster catches
    the slower - drawn GREEN (head-on stays blue). On the chart, the two ship
    names sit on opposite sides of the fairway centre-line (from the fairway
    linestring bearing), each with 'DTG : x.x km' (distance to go to the CPA
    point) beneath the name; line + labels are green for overtaking, blue for
    head-on. Engine maths in route_map_model (route_strip sog+filter, route_cpa
    overtaking+DTG) proven offline; route-viewer + chart rendering verified
    headless (26/26 checks). 0.27.4 - Audit hardening + full annotation pass. Removed a dead
    shadowed _update_qgis_cpa stub in the route-map window; HTML-escape untrusted
    AIS vessel names before they go into Qt rich text; tidied cosmetic nits
    (duplicate import, uninitialised Binding.sog, unused imports). Added a
    docstring to every function in the plugin (annotation coverage 36 to 100 percent).
    No behavioural change. See documents/AUDIT_2026-06-25.md.
    0.27.3 - Route-CPA chart labels simplified to the sketch: the
    inbound and outbound ship names now sit on the blue CPA line itself, one
    on each side of the fairway, reading perpendicular to the line; 'routeCPA'
    and the 24h meeting time sit at the top of the line. (Names are along the
    fairway axis via parallel line-labelling.)
author=CaptainAhab and Cosmo
email=fabian@maraton.consulting
license=GPL-3.0-or-later

# Qt6 / QGIS 4 compatibility - code uses version-neutral qgis.PyQt imports.
supportsQt6=True

tracker=https://gitlab.com/foss4maritime/aiswatchkeeper
repository=https://gitlab.com/foss4maritime/aiswatchkeeper
homepage=https://gitlab.com/foss4maritime/aiswatchkeeper
tags=ais,nmea,vessel,maritime,ship,marine,realtime,serial,tcp,navigation

category=Plugins
icon=icon.png
experimental=False
deprecated=False
hasProcessingProvider=no
