Observed since 20 Oct, 22:12 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
@0k/geo-map
Temporary package placeholder for staged publishing
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.
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.
| Callback | Required | Purpose |
|---|---|---|
loadFile | yes | Resolve the file's bytes, or the descriptor's (abortable). |
loadRef | descr. | Bytes of a file a descriptor names; for photos, {maxBytes} asks for a prefix. |
photoUrl | no | URL of a full-size photo (popup image, marker fallback). |
openPhoto | no | Show photo index of the map's photos (e.g. a lightbox); default: new tab. |
mediaUrl | no | URL of a medium a feature links to, resolved against its source file. |
renderMedia | no | Render a medium yourself (e.g. your audio player); null = default. |
renderDescription | no | SAFE HTML for an item description; default is plain text. |
basemaps | no | Basemaps offered in the menu, first is the default (DEFAULT_BASEMAPS). |
initialBasemap | no | Id to start on; unknown ids are ignored. |
onBasemapChange | no | The user picked another basemap. |
fullscreenPortal | no | Container for the full-screen overlay (default document.body). |
wideHost | no | Scrolling ancestor the wide layout spans (default: root's parent). |
onLayoutChange | no | inline / wide / full changed (e.g. to own a history entry). |
onClose | no | ✕ 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.
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.
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:
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;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;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 ♪ / 🖼.
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
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)).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.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.1 track · 2 places), basemap menu (hidden with a single basemap), ⌖
show everything, ⇔ wide, ⛶ full screen, ✕ close.host.fullscreenPortal(), Escape or
⛶ brings it back in place.BASEMAPS holds the presets; DEFAULT_BASEMAPS offers all four, OSM
first. All are built from OpenStreetMap data (ODbL):
| Id | Service | Max zoom |
|---|---|---|
osm | tile.openstreetmap.org (OSMF) | 19 |
opentopomap | tile.opentopomap.org (CC-BY-SA) | 17 |
cyclosm | tile-cyclosm.openstreetmap.fr (OSM France) | 20 |
osm-hot | tile.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.
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).
Besides createGeoMap and the parsers above, the package exports the
helpers and constants the widget itself uses:
| Export | What it is |
|---|---|
ROOT_CLASS | 'gm-root', the class every widget style hangs from |
GEO_FORMATS / MAP_KINDS | The formats parseGeoFile reads; the same plus 'descriptor' |
DEFAULT_MAX_UNCOMPRESSED_BYTES | Default 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 |
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>.
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:
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.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.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:
.gm-resize grip (role
separator, aria-label "Resize map") in its bottom-right corner;
.gm-map now sits in a .gm-map-wrap element.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.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.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.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.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.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.
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.exports has types and default for ., plus
./style.css; the CommonJS build is gone.@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.npm run check (unit, typecheck, build, Chrome 91 audit,
demo build, real-Chrome acceptance).Download counts and popularity will appear here as the collector receives them.
Declarations describe requirements; they are not a resolved dependency graph.
Source distribution
{
"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
}Observed since 20 Oct, 22:12 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:49ce0bf725ef4eed2aaad3a3e5c152cc1c74ead4753bb3d4bcdaeb7303fea7df