{"name": "AIS Watchkeeper", "package_name": "ais_nmea_source", "version": "0.43.0", "experimental": false, "qgis_min": "3.22.0", "qgis_max": "3.99.0", "downloads": 20, "uploaded_by": "CaptainAhab", "upload_datetime": "2026-07-05T09:27:55.500360", "changelog": "0.43.0 - Strip & settings polish. (1) On the route-leg strip, the ship\nSYMBOLS now declutter like the labels do: where triangles crowd along the\naxis they fan out perpendicular on the same side, ordered by speed - the\nslowest stays nearest the route line, the fastest goes furthest out - and\neach drops back to the line as soon as it no longer overlaps a neighbour.\nNames follow their (possibly pushed-out) triangle. (2) The AIS stream\nSettings box is reorganised into two tight columns with the toggles side by\nside, removing the dead space.\n0.42.0 - Custom routes (for users outside the bundled network). The\nwhole RWS FIS-VNDS route network is bundled in data/routes.gpkg and ships in\nthe plugin. NEW: an editable QGIS \"Custom routes\" LineString layer - draw a\nline near where ships sail, name it, and it becomes a fairway you can bind to.\nThe drawn direction (first vertex to last) is treated as INBOUND, the reverse\nas outbound (used for the start/end-based in/out counts). Custom routes show\nin the Routes tab in BLUE (marked \"(custom)\") and take binding PRIORITY: when\na vessel is within tolerance of both a custom and a native route, it binds to\nthe custom one (native binding is unchanged when no custom route is in range).\nCustom routes persist in custom_routes.gpkg in the profile dir (user data) and\nreload next start. Mainly to let people outside the EU define their own routes.\n0.41.0 - Per-route bind width. Tidal fairways are wide (the\nWesterschelde mouth is nearly 1 km across) and ships legally sail well off\nthe centreline, so a single 150 m bind tolerance dropped them as off-fairway.\nThe Routes table now has an editable \"Bind width (m)\" box per route: widen it\n(e.g. 650 m) for a broad tidal fairway and vessels sailing off-axis bind\nthere, while narrow canals/parallel routes keep the tight default so they\ndon't mis-bind. The route a vessel lands on is still chosen by nearest +\ncourse alignment, so a wider tolerance never steals a vessel that is closer\nto another route; an off-centre bind just reads as lower confidence. Values\npersist per route in route_tol.json (profile dir) and reload next start.\n0.40.0 - User reference markers on the route. A long fairway like the\nWesterschelde (~105 km, no locks or bridges) had nothing on its strip to map\nkm to the real world. Now there's an editable QGIS point layer \"Route\nreference markers\": drop a point near a fairway, name it (a buoy, a VTS sector\nboundary, a berth) with QGIS's native editing, and the route manager portrays\nit on the strip exactly like a bridge or lock - snapped to the nearest route\nand route-km (teal -). Markers persist in markers.gpkg in the profile dir\n(user data, survives plugin upgrades) and reload next start. They are also\nwired into route-CPA: pick a vessel then a marker to get the vessel's ETA /\ndistance-to-go to that marker, just like bridges and locks.\n0.39.0 - Two fairway datasets, each for what it is best at. BINDING +\nROUTE MANAGER now run on the Rijkswaterstaat FIS-VNDS \"route\" layer: full-\nstretch, properly-named routes (e.g. id 20940 = \"Westerschelde\", ~105 km as\nONE line) with linear-referencing km. The fine-grained \"vaarwegen\" passages\nare now a DISPLAY-only chart layer - each individual stretch labelled with its\nown name (Honte, Pas van Terneuzen, Vlissingen Rede, ...), thin, drawn only\nwhen zoomed in past 1:12500. We do NOT trust the service's own \"routejunctie\"\nlayer; instead route_ingest.py passes over the route geometries and flags a\nconnection only where >=2 routes actually share a vertex (~2 m) or a dangling\nendpoint lands on another route's interior (<=10 m T-branch). The result\n(2056 routes, ~2240 derived junctions) is baked into a bundled resource\ndata/routes.gpkg so binding runs fully offline; re-run route_ingest to refresh\nfrom the live service. This supersedes the 0.38.1 per-route name override. The\nold on-canvas route polyline layer (\"Routes in coverage\") and its click-picker\nare removed - the route UX is the Routes table. Also: incoming sentences with\na leading NMEA 4.0 / IEC 61162-1 TAG block (\\\\s:...,c:...*HH\\\\, added by many\naggregators & multiplexers) are now stripped before classify/validate/decode -\nsuch feeds previously decoded almost nothing; live-tested at >4000 vessels.\n0.38.1 - Routelines tidy-up: the on-canvas reference lines are half as thick\n(0.45 mm) and only draw when zoomed in past 1:12500.\n0.38.0 - Route binding now uses the vessel's track, not a single\nfix. A vessel is bound only after at least 3 position hits, and the route it\nlands on is ranked by the COURSE MADE GOOD across those hits (the leg\nbearings between them) rather than one reported COG - so when several routes\nrun nearby, it binds to the one sharing the stretch the ship is actually\ntravelling, not a closer crossing/parallel fairway. A vessel under 2 kn for\nmore than 3 consecutive hits is DETACHED from its route (moored/anchored\nvessels stop being glued to whatever fairway they sit beside); it re-binds\nonce it is moving again with a fresh 3-hit track. Reported COG is kept only\nas a fallback before a track course exists. Tunable via MapMatcher\n(min_track_hits, slow_speed_kn, slow_detach_hits, min_move_m).\n0.37.1 - Fix a Windows crash (access violation) when disconnecting /\nunplugging a serial AIS receiver. The reader thread was blocked in pyserial's\noverlapped read (GetOverlappedResult) while the port handle was closed/\nfinalised underneath it. SerialReader.stop() now aborts the in-flight read\nthe thread-safe way via cancel_read() (CancelIoEx) and waits for the thread to\nfinish; the run loop clears self._serial in finally and stays quiet on a\nstop-induced abort (reporting a clear \"device disconnected?\" only on a real\nfault). The dialog keeps a reference to a retiring reader until its thread\nends, so it can't be garbage-collected (and close its port) mid-read. The\nexact unplug-during-read fault lives inside pyserial's C call and can't be\nfully caught in Python, but the close-under-read / GC races (the common crash)\nare gone and ordinary removals unwind cleanly. Proven 12/12 (clean stop via\ncancel_read, device-removed graceful unwind, stop-before-start safe).\n0.37.0 - Routes UX: anti-collision strip names + a Routes table; map\nroutes are now quiet & non-interactive. (1) Strip ship-names no longer\noverlap - they behave like objects that can't collide, pushed out\nperpendicular to the route line by the FULL length of the name before them\n(greedy packing on each side via font metrics; the scene grows to fit dense\nclusters). Replaces the too-small fixed stagger. (2) On the chart the\nin-coverage routes are now drawn as a quiet THIN DASHED reference layer\n(0.9 px) and are NON-interactive - the map-click tool/filter is removed\n(it was awkward). (3) New \"Routes\" tab after Vessels: a table (Fairway |\nInbound | Outbound | Idle) of routes with vessels in coverage, sorted by\nmoving vessels (in+out) desc; clicking a row opens that route's leg in the\nroute manager. New engine RouteMapModel.route_table(). Route-CPA chart\nportrayal is unchanged - it still draws when you run a CPA inside a leg.\nProven headless 15/15 (route_table counts/sort/idle; anti-collision stacking;\nRoutes tab populate/sort/row-open; dashed thinner layer); full suite 129/129.\n0.36.0 - TCP login: credentials via QGIS's native encrypted auth store.\nThe security dialog now uses QgsAuthConfigSelect to pick or create a stored\ncredential; the secret lives ENCRYPTED in QGIS's auth database (master-\npassword protected) and is decrypted only at connect to render the login -\nthe plugin never holds it beyond the session and never writes it to its own\nfiles. A session-only manual fallback remains for a quick test. Per host:port\nthe dialog remembers (in QgsSettings) the non-secret bits - the auth config\nID, TLS flags, login template and method - so the next connect pre-fills;\nthe secret stays in the auth DB, only the ID is remembered. Verified headless\n9/9 in an isolated temp auth DB: master pw set, credential stored, decrypt\nround-trip, dialog renders the login from the stored config, the id (not the\nsecret) is remembered, and a scan of the on-disk DB confirms the secret is\nNOT in plaintext. Full suite 114/114. (QgsAuthConfigSelect's add/edit editor\n+ the live authenticated connect still want an in-QGIS run.)\n0.35.0 - TCP source: scan the endpoint for security + login dialog.\nWhen you Connect to a Network (TCP) source, the plugin now PROBES host:port\nfirst (new endpoint_probe.py, pure-stdlib, off the wire it only listens):\n(1) reachable? (2) TLS? - it attempts a TLS handshake; a ServerHello means\nthe endpoint is TLS, otherwise it re-probes as plaintext; (3) auth likely? -\nif no NMEA streams on connect (and/or a text banner/login prompt arrives).\nIf the endpoint looks secured (TLS or auth-likely) a login dialog opens,\npre-filled from the scan: a TLS toggle (+ allow self-signed), an auth method\n(none / username+password / token), and a server-specific login-line template\nwith {user}/{pass}/{token} placeholders (there is no standard AIS login, so\nthe exact line(s) are configurable). TcpReader gained use_tls / tls_insecure\n/ login_lines: it wraps the socket in TLS and sends the rendered login on\nconnect. The probe is best-effort and gated on 'reachable AND secured', so a\nnormal plaintext feed connects exactly as before. Credentials are used for\nthe session only (not written to disk; encrypted-store persistence is a\nlater refinement). Addresses the 2026-06-25 audit's 'TCP feed is cleartext /\nno auth' note. Proven headless: probe 14/14 (reachable / TLS-fallback /\nstreaming-NMEA / silent / banner / unreachable; TLS-positive path complete,\nskipped live for lack of a test cert), dialog+reader 8/8 (TLS prefill, method\nswitching, login rendering, login sent CRLF on connect). Suite 105/105 green.\nLive test wants a real TLS / authenticated AIS server. 0.34.0 - Route-leg window: wheel-zoom + drag-pan restored. The strip\nstill opens at the panel-fit 1:1 layout (the start point), but the user can\nnow wheel-zoom about the cursor and drag-pan from there. The zoom/pan is\nPRESERVED across the 2 s refresh and CPA/ETA redraws; only an explicit fit\n(open a route / Refresh / resize / Rotate) snaps back to the panel-fit. Done\nby resetting the view transform only on fit=True (the tick keeps it) and\nre-enabling wheel-zoom in the strip. Constant-size labels stay readable at\nany zoom. Proven 6/6 (zoom enabled, fit start 1:1, preserved across\ntick/redraw, reset on explicit fit); full suite 77/77.\n0.33.1 - Fix: could not start a route-CPA (regression from 0.33.0).\nThe second pick was never recognised - _on_click called _exit_select()\n(which clears _cpa_pick) BEFORE _resolve_cpa_picks() read the picks, so it\nalways fell through to \"pick two vessels...\". Now the two picks are snapshotted\nbefore _exit_select(); _resolve_cpa_picks takes them as an argument. Covered by\na new two-click regression test (vessel+vessel meeting and vessel+bridge ETA,\nwith a 2 s tick between picks). 4/4 + suite 73/73.\n0.33.0 - Routes-on-map UX refinements + route-CPA to bridges/locks.\n(1) The in-coverage routes layer now AUTO-REFRESHES every ~10 s (and the\nleg-window detail redraws live), so it tracks coverage without any button.\n(2) The \"Routes on map\" button is REMOVED - routes show automatically; this\nis gated on the fairway matcher already existing (built by binding), so it\nnever forces a build or hangs the GUI at startup. (3) Clicking a route now\nuses a canvas event filter (RouteClickFilter) instead of a custom map tool:\nit never consumes the event, so the user's normal pan/zoom/identify keeps\nworking and routes stay clickable regardless of the active tool (and it\nsurvives tool switches). (4) NEW route-CPA to fixed objects: in the leg's\nRoute-CPA mode you can pick a vessel + a bridge/lock and get that vessel's\nETA + distance-to-go to the structure (engine RouteMapModel.object_eta);\ndrawn as a label at the object and in the header. Picking two vessels still\ngives the meeting CPA. Also fixed: the strip header is now context-aware\n(route/km, or the active CPA/ETA) so it no longer gets clobbered on redraw.\nProven headless 73/73 across 7 suites (object_eta in/out/stationary; routes\nlayer; window open-to-detail; orientation; CPA; vars; rule). The on-canvas\nclick + auto-refresh wiring wants an in-QGIS click-through.\n0.32.0 - Routes drawn on the QGIS map; click a route to open its leg.\nThe in-window octolinear schematic overview is RETIRED. The \"Routes on map\"\nbutton (was \"Route map\") now draws the in-coverage routes straight on the\nQGIS canvas at their real lon/lat (\"Routes in coverage\" layer - coloured per\nroute, named, carrying routeid). New engine RouteMapModel.coverage_polylines()\nreturns each in-coverage route's segments. Clicking a route on the map opens\nthat route's leg in the route-leg window (the strip) via RouteClickTool - an\nalways-on map tool that SUBCLASSES QgsMapToolPan so panning still works (drag\npans; a no-drag click on a route opens it, hit-tested via QgsMapToolIdentify).\nThe route-leg window is now detail-strip only (open_route(rid)). Proven\nheadless 17/17 (coverage polylines in/out of range; layer build, attrs,\ngeometry, idempotent rebuild; window opens to detail; tool subclasses pan);\nregression 46/46. The map-tool click flow (mouse events, identify tolerance,\nCRS) is compile-checked and wants an in-QGIS click-through.\n0.31.0 - Route monitor: portrait/landscape re-layout + panel-bound\nsizing. (1) Portrait is now a REAL re-layout (the km-axis runs vertically),\nnot a view rotation - so in portrait ALL labels read normally (horizontal),\nfixing the unreadable rotated labels. Landscape keeps vertical labels along\nthe horizontal axis. The whole strip is drawn in (along, perp) coordinates\nmapped by orientation, so one code path serves both. (2) The route axis now\nfills the panel's long side (width in landscape, height in portrait) at 1:1,\nfloored at 500 px; a smaller panel scrolls (scrollbars) instead of shrinking.\nWheel-zoom is disabled in the strip (it scrolls instead); the view re-fits on\npanel resize. The \"Rotate\" button toggles portrait/landscape. Proven headless\n8/8 (axis orientation, label orientation per mode, opposite-side placement,\nobject line on the axis, 500 px floor); regression 38/38. 0.30.0 - Route monitor (strip) visual overhaul. (1) The vertical layout\nis now laid out in SCREEN pixels (offsets divided by the live zoom) and\nredraws on zoom, so spacing looks the same at any zoom and labels no longer\ncollide/overlap when zoomed out. (2) More vertical breathing room overall.\n(3) The symbol-key legend moved OUT of the canvas into a real label under the\nview - it can no longer collide with vessel/object labels and is never\ntruncated. (4) All ship-name labels now read the same way as the bridge/lock\nlabels (rotated -90, upward). (5) Declutter stagger is now zoom-aware (screen-\ngap threshold, up to 5 levels) so crowded names step apart even when zoomed\nout. (6) NEW: a \"Rotate\" button in the toolbar rotates the whole view 90 deg to\ntoggle landscape <-> portrait. Bridge/lock markers keep the touch-axis +\ndot + name-at-top behaviour, now positioned in the same screen-space scheme.\nProven headless: route-map polish 11/11 + object marker 7/7 + regression\n38/38. 0.29.4 - Renamed the visible UI from \"AIS NMEA Source\" to \"AIS\nWatchkeeper\": the docked panel title (the QGIS tab), the Plugins-menu entry\nand the toolbar/menu action all now read \"AIS Watchkeeper\". Internal package\nname (ais_nmea_source) and the dock objectName are unchanged (load-bearing).\n0.29.3 - Completed the route-CPA vessel highlight: the blue dot is now\na rule ON THE LIVE AIS LAYER. vessel_layer._style, after loading the QML,\nconverts the renderer to rule-based (which preserves all authored ship-type\nsymbology exactly) and appends ONE rule that draws a blue circle on any\ntarget whose vessel_stationID matches @routeCPAVesselOne / @routeCPAVesselTwo\n(filter uses array_contains+coalesce so empty/unset variables = no dots).\nIdempotent (never double-adds) and best-effort. So the highlight now rides\nthe real moving target. Proven 8/8 (rule present + base symbology preserved +\nmatches/excludes/cleared); route-map side unchanged (37/37). 0.29.2 - Route-CPA vessel highlight reworked to a project-variable +\nsymbology-rule scheme. When a route-CPA is active the two vessels' MMSIs are\nwritten to project variables @routeCPAVesselOne / @routeCPAVesselTwo (blanked\nwhen the CPA is cleared); the old blue-ring \"Route CPA vessels\" overlay layer\nis retired. The blue dot is now drawn by a rule on the LIVE AIS layer's own\nsymbology (matching vessel_stationID against the two variables) - so it rides\nthe real moving target, not a static snapped point. (This window writes the\nvariables; the symbology rule lives on the liveFeedAIS layer / its QML.)\nVariable set/clear + old-layer retirement proven 4/4. 0.29.1 - Bridge/lock marker tweak: the dotted line is now SHORT - it\ntouches the route axis and rises only to just below the name, where it ends\nin a small dot, then a gap, then the object name (instead of running far up\npast it). Verified (7/7). 0.29.0 - Bridges & locks on the route monitor (live from the RWS\nFIS-VNDS WFS). Opening a route's strip now fetches that route's bridges\n(layer 'brug') and locks ('sluiskolk') from the FIS-VNDS WFS, server-side\nfiltered by routeid (so only the few on that route are fetched), and draws\neach as a dotted line that touches the route axis and runs far upward, with\nthe object name at the top - above the ship labels - read along the line.\nPurple = bridge, orange = lock. The objects carry routeid + route-km in the\nsame scheme as the bundled fairway network, so they place by km directly (no\ngeometry snapping). This is the plugin's first network call: it is per-route,\ncached, time-limited and fully best-effort - offline or a WFS hiccup just\nleaves the strip without the markers and never blocks the live AIS feed.\nProven: pure GeoJSON parser, a live fetch (route 31596 -> 24 bridges), and\nthe strip rendering (12/12); the earlier route-viewer/CPA work still passes\n(36/36). 0.28.1 - Route viewer / route-CPA interaction tweaks. (1) The two\nvessels in an active route-CPA are now highlighted on the main QGIS map with\na blue ring (\"Route CPA vessels\" layer). (2) In the strip view, vessel names\nare rotated 180 degrees (still outbound-above / inbound-below, placed via\ntheir bounding rect so each stays fully on its own side). (3) Click a ship in\nthe strip -> the main QGIS map recentres + zooms to that ship (snapped\nposition) and flashes it. (4) Click the route-CPA bar in the strip -> the\nmain map zooms to the CPA meeting point. (5) The strip legend moved lower\n(clear of the inbound names) in a larger, constant-size font. All five\nverified headless (10/10) with no regression to 0.28.0 (26/26 still green).\n0.28.0 - Route viewer + route-CPA display upgrades. Route viewer:\n(1) vessel names now sit on the SAME side as the vessel's lane - outbound\nabove the axis, inbound below - instead of all on one side, which declutters\nthe strip; near-neighbour names additionally stack outward. (2) The marker\ncolour is now a speed heat ramp (green at 2 kn -> red at 20 kn, clamped red\nabove 20). (3) Vessels slower than 2 kn are dropped from the strip\n(unknown-speed targets are always kept). Route CPA: now also predicts the\nOVERTAKING case - two vessels on the same course, where the faster catches\nthe slower - drawn GREEN (head-on stays blue). On the chart, the two ship\nnames sit on opposite sides of the fairway centre-line (from the fairway\nlinestring bearing), each with 'DTG : x.x km' (distance to go to the CPA\npoint) beneath the name; line + labels are green for overtaking, blue for\nhead-on. Engine maths in route_map_model (route_strip sog+filter, route_cpa\novertaking+DTG) proven offline; route-viewer + chart rendering verified\nheadless (26/26 checks). 0.27.4 - Audit hardening + full annotation pass. Removed a dead\nshadowed _update_qgis_cpa stub in the route-map window; HTML-escape untrusted\nAIS vessel names before they go into Qt rich text; tidied cosmetic nits\n(duplicate import, uninitialised Binding.sog, unused imports). Added a\ndocstring to every function in the plugin (annotation coverage 36 to 100 percent).\nNo behavioural change. See documents/AUDIT_2026-06-25.md.\n0.27.3 - Route-CPA chart labels simplified to the sketch: the\ninbound and outbound ship names now sit on the blue CPA line itself, one\non each side of the fairway, reading perpendicular to the line; 'routeCPA'\nand the 24h meeting time sit at the top of the line. (Names are along the\nfairway axis via parallel line-labelling.)", "external_deps": "", "download_url": "https://plugins.qgis.org/plugins/ais_nmea_source/version/0.43.0/download/"}