Files
maptool/docs/development.adoc
T
randy f53c74e06f
CI / check (push) Successful in 40s
CI / deploy (push) Successful in 12s
Add rake overpass:pois to fetch camper POIs from Overpass
lib/maptool/overpass_pois.rb: queries Overpass for drinking water,
dump stations, LPG refill, toilets, laundromats, supermarkets/
convenience stores, and springs within a bbox, writes GeoJSON. A
dev-time tool (like the offline.pmtiles/terrain.pmtiles generation
steps already documented) — not called by the running app, drop the
output into data/gpx/ or upload it through the Tracks panel like any
other .geojson layer.

Verified against the real Overpass API (88 POIs for central
Bratislava) in addition to the new unit tests (pure to_geojson/
build_query logic, no network in the suite itself). Full suite: 103
runs, 0 failures.
2026-09-09 10:21:41 +02:00

7.5 KiB
Raw Blame History

Development guide

Web app only

bundle install
bundle exec puma -C config/puma.rb

Open http://127.0.0.1:4567. For auto-reload during development, run via the rerun gem instead:

bundle exec rerun -- bundle exec puma -C config/puma.rb
Tip
Browsers apply heuristic caching to static files even without explicit cache headers, so an edit to public/ (or views/) can appear not to have taken effect on the next reload. Force a real reload (e.g. Ctrl/Cmd+Shift+R) if that happens.

Offline map data

To enable the offline style, place a PMTiles file at data/offline.pmtiles (see docs/architecture.adoc for how it is served and styled). It is not part of the repository — obtain or build one locally:

  • The simplest source is a pre-built extract from protomaps.com’s extract builder — pick a region and download it as .pmtiles. Its schema is what protomaps-themes-base (the styling library used for the offline style) expects.

  • To convert some other vector tile source (e.g. an .mbtiles file) into PMTiles, use the go-pmtiles CLI: pmtiles convert in.mbtiles data/offline.pmtiles.

Once the file is in place, GET /api/offline-tiles reports available: true and the map defaults to the offline style.

Offline terrain data (hillshade)

The sidebar’s "Terrain shading" toggle works online out of the box (public Terrarium elevation tiles, no API key). For it to work offline too, place a PMTiles archive of Terrarium-encoded raster-dem tiles at data/terrain.pmtiles (GET /api/terrain-tiles reports available: true once it’s there, same pattern as offline map data above). It is not part of the repository — build one locally from a DEM source (e.g. Copernicus GLO-30 or SRTM) with a tool like rio-rgbify (GeoTIFF → Terrarium-encoded PNG tiles in an .mbtiles), then pmtiles convert out.mbtiles data/terrain.pmtiles with go-pmtiles, same as data/offline.pmtiles.

Camper-relevant POIs (Overpass)

rake oversouth,west,north,east,out.geojson queries Overpass for drinking water, dump stations, LPG refill, public toilets, laundromats, supermarkets/convenience stores, and springs within a bbox, and writes the result as a .geojson file (lib/maptool/overpass_pois.rb). A dev-time tool, not something the running app calls itself — drop the output into data/gpx/ (or upload it through the Tracks panel) like any other .geojson layer once it’s generated. Nodes only (nothing that’s mapped as a way/relation, e.g. a supermarket traced as a building outline rather than a single point).

GNSS position (gpsd)

The "GNSS" control needs gpsd running and pointed at a receiver (gpsd /dev/ttyUSB0, or whatever device the receiver shows up as). On openSUSE, gpsd is a package with systemd units (gpsd.service / gpsd.socket); on other distributions, check your package manager. No app-side configuration is needed — it just connects to 127.0.0.1:2947, gpsd’s default port.

To test without hardware or gpsd itself installed/running, run bin/gnss_simulator.rb — it speaks gpsd’s own JSON wire protocol directly on gpsd’s default port (127.0.0.1:2947), so Maptool::GpsdClient connects to it exactly as it would to real gpsd, no app-side changes needed. Drives a loop around a center point (Bratislava by default) at a driving-ish pace instead of sitting at a fixed position, since a stationary "GNSS position" isn’t much to look at on the map:

bin/gnss_simulator.rb                       # loop around Bratislava, 300m radius
bin/gnss_simulator.rb 48.85 2.35 500         # custom center/radius (Paris, 500m)

GET /api/gnss/position should report a fix within a couple of seconds, moving on every poll.

To test against gpsd itself instead (e.g. to exercise the actual NMEA-parsing path, not just gpsd’s JSON output), feed it a fake NMEA stream over TCP instead of a real serial device — gpsd accepts a tcp://host:port source:

# terminal 1: a TCP server repeating one NMEA GGA/RMC sentence pair
python3 - <<'PY' 5001
import socket, sys, time

def checksum(s):
    cs = 0
    for ch in s:
        cs ^= ord(ch)
    return f"{cs:02X}"

def sentence(body):
    return f"${body}*{checksum(body)}\r\n"

port = int(sys.argv[1])
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", port))
srv.listen(1)
conn, _ = srv.accept()
gga = "GPGGA,123519,4808.916,N,01706.462,E,1,08,0.9,140.4,M,46.9,M,,"
rmc = "GPRMC,123519,A,4808.916,N,01706.462,E,000.5,084.4,230824,003.1,W"
while True:
    conn.sendall((sentence(gga) + sentence(rmc)).encode())
    time.sleep(1)
PY

# terminal 2: point gpsd at it instead of a real device
gpsd -N -n tcp://127.0.0.1:5001

GET /api/gnss/position should then report the fake fix (Bratislava Castle, in the sentences above) within a couple of seconds.

Tests

bundle exec rake test runs the whole suite (test/test_*.rb, Minitest — a default gem, but pinned in the Gemfile’s `:test group for reproducibility, same reasoning as rake itself). test_app.rb drives app.rb’s routes end to end through `Rack::MockRequest rather than a real server; everything else tests one lib/maptool/ module in isolation. MAPTOOL_DATA_DIR (see app.rb) is pointed at a throwaway Dir.mktmpdir by test/test_helper.rb before app.rb loads, so running the suite never touches real data/ — the app-level tests also wipe that directory between each test (see AppTestHelper#teardown), since GPX_STORE/LIVETRACK_STORE are process-wide constants shared by every test in the run.

LiveTrackClient’s own tests use a synthetic fixture (no network) — its parsing was checked against real Garmin sessions by hand during development, not by the suite, since that needs a live, unexpired session token to fetch. `Fit’s tests likewise build a minimal FIT file by hand rather than shipping a real device fixture; correctness against real files (position/altitude fields, definition-message framing, compressed-timestamp headers) was checked by hand against `python-fitparse’s test fixtures during development — see `docs/architecture.adoc.

Continuous integration

.gitea/workflows/ci.yml runs on every push/PR against a self-hosted Gitea Actions runner: ruby -c on every .rb file, a require_relative "app" smoke check that the app loads without error, bundle exec rake test (see above), and node --check on every file under public/js/ — no JS test suite, just a syntax check.

.gitea/workflows/renovate.yml runs Renovate (the renovate/renovate image) weekly (Mondays, also triggerable manually), opening PRs for outdated dependencies (Bundler Gemfile/Gemfile.lock, auto-detected — no renovate.json needed) against this Gitea instance (RENOVATE_PLATFORM=gitea). It runs on the native deploy runner (see docs/deployment.adoc) as a one-shot docker run, not a persistent service. Needs a RENOVATE_TOKEN repo secret — a Gitea personal access token (Settings → Applications → Generate New Token, with repository read/write scope) from whichever account should author the PRs; RENOVATE_ONBOARDING=false skips Renovate’s usual first-PR "configure me" step and goes straight to dependency update PRs using its defaults.