Catalog npm
Back to packageRELEASE DETAILS

0.0.0-stage

npm

@0k/geo-map

Temporary package placeholder for staged publishing

geojsongpxkmlkmzleafletmapopenstreetmap
Published18 Oct 2026
Direct dependencies0
Transitive dependenciesNot calculated

README

Current package documentation

@0k/geo-map

Purpose

A framework-free TypeScript map widget that shows the tracks and places of a KMZ, KML, GPX or GeoJSON file over free, OpenStreetMap-based basemaps — or, from a small YAML descriptor, several such files plus geotagged photos and hand-written places on one map. Its runtime libraries — Leaflet 1.9.4, @tmcw/togeojson 7.1.3, fflate 0.8.3 and yaml 2.9.1 — are dependencies pinned EXACTLY, resolved by the host's bundler. The package's ES2020 output and those libraries run on Chromium 91 (old Android WebViews).

The widget knows no file path, URL, or transport for the data. A host supplies the file bytes; this package owns parsing, rendering, layout, popups, the item list, and teardown. The only network traffic it causes is basemap tiles: it never fetches the icons a KML file references. MIT licensed.

Install

npm install @0k/geo-map
import { createGeoMap, detectFormat, type GeoMapHost } from '@0k/geo-map';

const host: GeoMapHost = {
  // Raw bytes.  The host owns the endpoint and its authorisation.
  loadFile: async (signal) => (await fetch('/api/file?path=…', { signal })).arrayBuffer(),
  renderDescription: (text) => mySanitisedHtml(text),  // optional, MUST be safe HTML
  initialBasemap: localStorage.mapBasemap,             // optional
  onBasemapChange: (id) => { localStorage.mapBasemap = id; },
  onClose: () => collapseMyShell(),                    // optional: ✕ pressed
};

const handle = createGeoMap(rootElement, host, { format: detectFormat(name)!, label: 'Ridge loop' });
await handle.open();      // load bytes → parse → mount Leaflet
handle.destroy();         // release everything (idempotent)

The element handed to createGeoMap receives the gm-root class; the widget owns its content between open() and destroy() (or close()).

Importing the package also imports its stylesheets (Leaflet's and the widget's), so the host's bundler must handle CSS imports. The widget's own rules are also exported as @0k/geo-map/style.css.

Host contract

CallbackRequiredPurpose
loadFileyesResolve the file's bytes, or the descriptor's (abortable).
loadRefdescr.Bytes of a file a descriptor names; for photos, {maxBytes} asks for a prefix.
photoUrlnoURL of a full-size photo (popup image, marker fallback).
openPhotonoShow photo index of the map's photos (e.g. a lightbox); default: new tab.
mediaUrlnoURL of a medium a feature links to, resolved against its source file.
renderMedianoRender a medium yourself (e.g. your audio player); null = default.
renderDescriptionnoSAFE HTML for an item description; default is plain text.
basemapsnoBasemaps offered in the menu, first is the default (DEFAULT_BASEMAPS).
initialBasemapnoId to start on; unknown ids are ignored.
onBasemapChangenoThe user picked another basemap.
fullscreenPortalnoContainer for the full-screen overlay (default document.body).
wideHostnoScrolling ancestor the wide layout spans (default: root's parent).
onLayoutChangenoinline / wide / full changed (e.g. to own a history entry).
onCloseno✕ pressed; the widget has already destroyed itself.

The handle: open, retry, close, destroy, setLayout / getLayout, isPortalled, setBasemap / basemap, focusItem(id), fit(), and the parsed document and Leaflet leaflet map while mounted.

Formats

detectFormat(name) maps .kmz, .kml, .gpx and .geojson (any case) to a format; parseGeoFile(bytes, format) returns a GeoDocument:

  • name / description: the KML Document (or top Folder), the GPX metadata, or a GeoJSON top-level name.
  • items: one per feature with a geometry, in file order. kind is track (lines, including Google My Tracks gx:Track / gx:MultiTrack), place (points) or area (polygons). Each item has its name, raw description, lengthMeters (tracks), start / end timestamps when the file records time, and the colour the file asks for (KML LineStyle). Placemarks without geometry are dropped.
  • bounds: south / west / north / east of everything.

A KMZ is a zip: its doc.kml is read, else the shallowest .kml. The entry's inflated size is checked against maxUncompressedBytes (64 MiB by default) before inflating it. Every failure throws GeoParseError with a message naming the problem (not a zip, no .kml inside, not well-formed XML, wrong root element, not GeoJSON, no tracks or places); the widget shows it as Cannot show this map: <reason>. A failed loadFile shows Failed to load map: <reason> with a Retry button.

Linked media

A GPX waypoint's <link href> (OSMTracker voice notes, pictures) and the local <a href> / <img src> of a KML description become GeoItem.media ({href, text, kind}, kind audio / image by extension, or web for an http(s) GPX link; mediaKind(href) is exported). Remote media in a KML description are never collected: they would be fetched. The popup shows each one:

  • the URL comes from host.mediaUrl(href, source, kind), where source is the descriptor layer the feature comes from (null: the map's own file), so relative references resolve against the right folder;
  • audio: host.renderMedia(media, url) if it returns an element, else a native <audio controls preload="none">; image: a thumbnail that opens through host.openPhoto; web: a new-tab link;
  • without mediaUrl a local medium reads <name>: not available here; a medium that fails to load reads cannot be played / cannot be shown.

List entries carrying media end with ♪ / 🖼.

Descriptor maps

createGeoMap(root, host, { format: 'descriptor' }) (detectMapKind maps .yml / .yaml to it) reads a YAML descriptor through loadFile and every file it names through loadRef. References are opaque to the package: the host resolves them (for example: relative to the descriptor's folder, absolute, or ~/).

title: Lake walk, May 2020         # map title (a host label wins)
basemap: opentopomap               # optional; overrides the host's choice
layers:                            # KMZ / KML / GPX / GeoJSON files
  - file: tracks/lake-loop.kmz
  - file: ride.gpx
    color: '#2f7fe0'               # optional, overrides the file's colours
    name: Morning ride             # optional, renames the file's single track
photos:
  - path: photos/IMG_1234.jpg      # placed from its EXIF GPS position
    caption: Lakeside steps        # optional; default: the file name
  - path: photos/IMG_1300.jpg
    at: [45.01, 10.02]             # [lat, lon]; wins over the EXIF position
places:                            # hand-written pins
  - name: The shelter
    at: [45.02, 10.01]
    note: Picnic stop, **windy**.  # optional; rendered by renderDescription
  • Keys are strict: an unknown key, a missing file / path / name, a bad colour or position fails the whole map with a message naming the key (layers[0]: unknown key "path" (expected file, color, name)).
  • A part that cannot be shown does not: the rest of the map shows, and a note under it names the part and why (ride.gpx: File not found, IMG_1.jpg: no GPS position in the photo (add `at:`)). Only a descriptor where nothing can be shown fails as a whole.
  • Photos: only the first 256 KiB of each is requested (PHOTO_HEADER_BYTES), enough for its EXIF block: GPS position, time taken, orientation, and the small embedded thumbnail, which becomes the round marker (turned upright by its orientation). Without an embedded thumbnail the marker uses photoUrl. The popup shows the photo in a fixed 260×195 box; clicking it calls openPhoto with every photo of the map. readPhotoMeta(bytes) is exported and never throws.

Display

  • Toolbar: title (host label, else the file's name), summary (1 track · 2 places), basemap menu (hidden with a single basemap), ⌖ show everything, ⇔ wide, ⛶ full screen, ✕ close.
  • The view is fitted to all items (never closer than zoom 16). Tracks are lines in the file's colour or the theme's; their first and last points get small green / red dots. Places are SVG circle markers.
  • A collapsed list under the map names every item with its length, duration and start time; clicking one zooms to it and opens its popup.
  • Popups show the name, the same details, and the description.
  • Full screen portals the root into host.fullscreenPortal(), Escape or ⛶ brings it back in place.
  • A grip in the inline map's bottom-right corner drags its height between 200 px and 90 % of the viewport (mouse, pen or finger); double-click resets it. The height is not remembered.

Basemaps

BASEMAPS holds the presets; DEFAULT_BASEMAPS offers all four, OSM first. All are built from OpenStreetMap data (ODbL):

IdServiceMax zoom
osmtile.openstreetmap.org (OSMF)19
opentopomaptile.opentopomap.org (CC-BY-SA)17
cyclosmtile-cyclosm.openstreetmap.fr (OSM France)20
osm-hottile.openstreetmap.fr/hot (HOT, OSM France)19

The widget follows the services' tile usage policies by construction: the attribution is always visible, it never prefetches or bulk-downloads, never asks beyond a service's maxZoom, and lets the browser send its normal User-Agent and Referer (strict-origin-when-cross-origin). Showing a file reveals the tile coordinates around it to the chosen service; a host that must avoid that can supply its own basemaps pointing at a caching proxy.

Theme

Every rule hangs from .gm-root; override the --gm-* variables there (--gm-surface, --gm-text, --gm-track, --gm-place, --gm-area, --gm-start, --gm-end, --gm-map-height, buttons, popup colours, and --gm-inset-* for full-screen safe areas).

Other exports

Besides createGeoMap and the parsers above, the package exports the helpers and constants the widget itself uses:

ExportWhat it is
ROOT_CLASS'gm-root', the class every widget style hangs from
GEO_FORMATS / MAP_KINDSThe formats parseGeoFile reads; the same plus 'descriptor'
DEFAULT_MAX_UNCOMPRESSED_BYTESDefault KMZ inflation limit (64 MiB)
parseMapDescriptor(text)Parses and validates a YAML descriptor; throws MapDescriptorError
summaryText(document)The header summary, e.g. 1 track · 2 places
itemMeta(item)The list / popup line: length, duration and start time
haversine(a, b)Great-circle distance between two [lon, lat] positions, metres
formatLength(m)850 m, 1.9 km, 42 km
formatDuration(ms)45 min, 1 h 05

Development

npm install
npm run check   # unit tests, typecheck, build, Chrome 91 audit, demo build,
                # real-Chrome acceptance (test/browser.py, tiles stubbed)

Tests use only the synthetic fixtures in test/fixtures/ (the KMZ is zipped at test time).

To try an unreleased change in a host, run npm install in the checkout first (its prepare script builds dist/ with the development dependencies), then install the checkout in the host without saving it: npm install --no-save <path-to-geo-map>.

Changelog

0.4.0 (2026-10-18)

New

  • Play the recordings and show the images a map's waypoints link to. [Valentin Lab]

    A GPX waypoint's <link href> (OSMTracker voice notes, pictures) and the local <a href> / <img src> of a KML description now appear in the feature's popup: a recording as an audio player that loads nothing until played, an image as a thumbnail opening in the host viewer, an http(s) GPX link as a new-tab link. List entries carrying media are marked with a note or picture sign. Remote media in a KML description are never collected, since showing them would fetch them.

    The host resolves each reference against the file the waypoint comes from (new mediaUrl hook; a descriptor layer, not the descriptor) and may render a medium itself (renderMedia). A medium that cannot be resolved or loaded says so in the popup.

    Contract:

    • Parsed items carry media (GeoMedia: href, text, kind): a GPX <link href> and the local <a href> or <img src> of a description, typed audio or image by extension (mediaKind, exported) or web for an http(s) GPX link. A remote medium in a description is never collected.
    • Items also carry source: the descriptor layer file they come from, null for the map's own file. mediaUrl(href, source, kind) resolves a local medium; without it, or when it returns null, the popup reads <name>: not available here.
    • renderMedia(media, url) may supply the element for a local medium; otherwise a recording gets a native <audio controls preload="none"> and an image a thumbnail that opens through openPhoto (default: a new tab). A web link opens in a new tab; a medium that fails to load says so.
    • List entries carrying media end with a note or picture sign.
    • Tests: test/formats.spec.ts, test/widget.spec.ts (linked media), test/descriptor-widget.spec.ts, test/browser.py.
  • Let users resize the inline map from a corner grip. [Valentin Lab]

    The inline map had a fixed height (--gm-map-height, 360 px by default) that a reader could not change. A grip in its bottom-right corner now drags the height with a mouse, pen or finger; a double-click resets it.

    Contract:

    • Outside full screen, every map has a .gm-resize grip (role separator, aria-label "Resize map") in its bottom-right corner; .gm-map now sits in a .gm-map-wrap element.
    • A drag sets the map height between 200 px and 90 % of the viewport, and Leaflet follows the new size; a double-click returns to the CSS height. The height is not remembered.
    • In full screen the grip is hidden and inert and a dragged height does not apply; the attribution is kept clear of the grip.
    • Teardown ends a drag in progress: no listener outlives the map.
    • Tests: test/widget.spec.ts (resize grip), test/browser.py.
  • Combine files, geotagged photos and places on one map from a YAML descriptor. [Valentin Lab]

    createGeoMap accepts a descriptor format: a YAML file listing layer files (with an optional colour or name override), photos and hand-written places. The host loads every file it names through a new loadRef callback, so references stay opaque to the package.

    Photos are placed from their EXIF GPS position (or an explicit at), read from the first 256 KiB only; the embedded EXIF thumbnail becomes a round marker turned upright by the photo's orientation. The popup shows the photo, and clicking it hands the map's photos to the host (openPhoto). A part that cannot be shown is listed under the map with its reason instead of failing the whole map; an invalid descriptor fails with a message naming the key.

    Contract:

    • format: 'descriptor' builds one map from a YAML descriptor (title, basemap, layers, photos, places); detectMapKind maps .yml and .yaml names to it. A descriptor basemap wins over initialBasemap; one the host does not offer becomes a note.
    • Every byte still comes from the host: the descriptor through loadFile, each file it names through loadRef, a photo with { maxBytes: PHOTO_HEADER_BYTES } (256 KiB, enough for its EXIF block). References are opaque strings the package never resolves.
    • A photo sits at its explicit at, else at its EXIF GPS position. Its marker is the embedded thumbnail turned upright, else photoUrl. Clicking the popup photo calls openPhoto with every photo of the map and the clicked index (default: a new tab). readPhotoMeta never throws; thumbnail object URLs are revoked on teardown.
    • An invalid descriptor fails as a whole with a MapDescriptorError naming the key. A part that cannot be shown becomes a line of GeoDocument.notes, listed under the map; only a descriptor with nothing showable fails.
    • New exports: parseMapDescriptor, MapDescriptorError, readPhotoMeta, PHOTO_HEADER_BYTES, detectMapKind, MAP_KINDS and their types. GeoDocument.notes is always set (empty for a single file) and GeoItemKind gains photo. yaml 2.9.1 is pinned exactly.
    • Tests: test/descriptor.spec.ts, test/exif.spec.ts, test/descriptor-widget.spec.ts, test/browser.py.
  • Add a reusable map widget for KMZ, KML, GPX and GeoJSON files. [Valentin Lab]

    A framework-free TypeScript widget (createGeoMap) that shows the tracks, places and areas of a file over free OpenStreetMap-based basemaps (OpenStreetMap, OpenTopoMap, CyclOSM, Humanitarian), with the attribution always visible. The host supplies the file bytes; the widget parses them (KMZ unzipped with a bounded inflate, Google My Tracks gx:Track included), fits the view, draws SVG markers instead of fetching the icons a KML references, and offers popups, an item list, wide and full-screen layouts, and idempotent teardown.

    Leaflet 1.9.4, @tmcw/togeojson and fflate are pinned exactly; the code runs on Chromium 91, and npm run check audits that, runs the unit tests on synthetic fixtures, and drives the demo in a real Chrome with tiles stubbed.

Changes

  • Document the exported helpers and how to try an unreleased build. [Valentin Lab]

    README.org gains an "Other exports" table (ROOT_CLASS, GEO_FORMATS, MAP_KINDS, DEFAULT_MAX_UNCOMPRESSED_BYTES, parseMapDescriptor, summaryText, itemMeta, haversine, formatLength, formatDuration), which the first npm release makes public API. Trying an unreleased checkout in a host needs npm install in the checkout first: its prepare script builds dist/ with the development dependencies. A destroy() / close() mention no longer renders with stray tildes on npm.

  • Ship ESM only, with the runtime libraries as exact-pinned dependencies. [Valentin Lab]

    Leaflet, toGeoJSON, fflate and yaml install as dependencies pinned exactly instead of being embedded in the package. A host that also uses Leaflet shares one copy, so the map behind handle.leaflet works with its plugins; the host's npm audit sees every library; and the published declarations resolve the leaflet and geojson types. tsc emits the package's own ES2020 modules and declarations.

    Contract:

    • leaflet 1.9.4, @tmcw/togeojson 7.1.3, fflate 0.8.3, yaml 2.9.1, @types/leaflet and @types/geojson are dependencies pinned exactly; nothing is bundled.
    • ESM only: exports has types and default for ., plus ./style.css; the CommonJS build is gone.
    • Importing @0k/geo-map imports Leaflet's stylesheet and the widget's; sideEffects marks the entry module so bundlers keep them.
    • npm run check:chrome91 audits dist/ and the browser entry of each pinned library.
    • Tests: npm run check (unit, typecheck, build, Chrome 91 audit, demo build, real-Chrome acceptance).

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Declared dependencies 0

Native requirements
0 direct targetsNot calculated transitive count
No declared dependencies in this collected response

Declarations describe requirements; they are not a resolved dependency graph.

Distribution files

1 collected
Distribution 1

Source distribution

Source file
Checksums from source
{
  "shasum": "f72ff3fca5bf1dd56d1e592b17edada3d9a3eafa",
  "tarball": "https://registry.npmjs.org/@0k/geo-map/-/geo-map-0.0.0-stage.tgz",
  "fileCount": 2,
  "integrity": "sha512-PiULEycq5MpMn1fxfk4XDhlLsBJuIg/XOBU6Rsy+iRt54a8HsZF5I9v995vz+vdjAULqYANfOzm+W3y5hFAPnQ==",
  "signatures": [
    {
      "sig": "MEUCIQCXgSX6aDJXZntepifsu457eZ0PSVaf2F/HI2fkZxgq7AIgcuE6pCXxkHZZwSFk4as+RM5qp09fHMyBy99WsBIIri8=",
      "keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"
    }
  ],
  "unpackedSize": 333
}

Repository

PENDING
0k/geo-map

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 22:12 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package_version:49ce0bf725ef4eed2aaad3a3e5c152cc1c74ead4753bb3d4bcdaeb7303fea7df

Other collected versions

0.4.018 OctView all collected versions →