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.
187 lines
7.5 KiB
Plaintext
187 lines
7.5 KiB
Plaintext
= Development guide
|
|
:toc:
|
|
:toclevels: 2
|
|
|
|
== Web app only
|
|
|
|
[source,shell]
|
|
----
|
|
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:
|
|
|
|
[source,shell]
|
|
----
|
|
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
|
|
https://app.protomaps.com[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
|
|
https://github.com/protomaps/go-pmtiles[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 https://github.com/tilezen/joerd[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
|
|
https://github.com/mapbox/rio-rgbify[rio-rgbify] (GeoTIFF ->
|
|
Terrarium-encoded PNG tiles in an `.mbtiles`), then
|
|
`pmtiles convert out.mbtiles data/terrain.pmtiles` with
|
|
https://github.com/protomaps/go-pmtiles[go-pmtiles], same as
|
|
`data/offline.pmtiles`.
|
|
|
|
== Camper-relevant POIs (Overpass)
|
|
|
|
`rake overpass:pois[south,west,north,east,out.geojson]` queries
|
|
https://overpass-api.de[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 https://gpsd.io[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:
|
|
|
|
[source,shell]
|
|
----
|
|
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:
|
|
|
|
[source,shell]
|
|
----
|
|
# 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
|
|
https://docs.renovatebot.com[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.
|