diff --git a/README.md b/README.md index 3ff3648..55939e9 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ A file named `guide.md` produces `guide.html`. A folder named `docs` produces `d Relative links and images remain unchanged, so they resolve from the generated HTML file. The site needs internet access to load its pinned Markdown-it dependency. +`geomap` code fences render GeoJSON layers as interactive maps with searchable feature tables and Google Maps links for points. Maps load Leaflet and OpenStreetMap tiles online; the table remains usable if the map fails to load. See the [GeoMap guide](https://github.com/asafmor/pageleaf/blob/main/docs/geomap.md) and [example](https://github.com/asafmor/pageleaf/blob/main/examples/geomap.md). + For uploads, install [Git](https://git-scm.com/downloads) and [GitHub CLI](https://cli.github.com/), then run `gh auth login --hostname github.com`. Use `--upload my-docs` for the signed-in user's personal account, or `--upload my-org/my-docs` for an explicit owner. The repository must already exist and permit direct pushes; an empty repository is supported. Uploads keep the generated filename (`docs.html` in the example), replace that remote file automatically, and leave other files unchanged. Identical content creates no commit. Commits use the signed-in account's GitHub noreply identity. `--force` controls local output replacement only. If an upload fails, the local HTML remains available; fix the reported problem and rerun with `--force`. Uploading stores the file in GitHub; it does not configure GitHub Pages. diff --git a/docs/geomap.md b/docs/geomap.md new file mode 100644 index 0000000..2d0f9a0 --- /dev/null +++ b/docs/geomap.md @@ -0,0 +1,33 @@ +# GeoMap fences + +Put a JSON object with a `layers` array inside a `geomap` Markdown code fence. Generate the page as usual: + +```bash +npx pageleaf examples/geomap.md --open +``` + +See the [complete example](../examples/geomap.md) for points, routes, a boundary, and multiple maps. + +Each layer requires nonempty `id`, `name`, and `role` strings and a `geojson` object. IDs must be unique within a map. GeoJSON may be a FeatureCollection, Feature, or raw geometry; all standard geometry types are supported. Optional layer `source` metadata accepts string fields `url`, `publisher`, `retrievedAt`, and `originalFormat`. GPX conversion must happen before rendering; source files are never downloaded or converted by Pageleaf. + +The map uses [Leaflet 1.9.4](https://leafletjs.com/download.html) and OpenStreetMap tiles. It needs internet access for those assets. Pageleaf embeds its own map component in the HTML, so the generated file needs no adjacent scripts or backend. If Leaflet cannot load, the feature table and its links remain available. The document itself still needs the existing Markdown-it CDN dependency. + +All layers start visible and can be toggled under **Layers**. Routes use different colors and line patterns. Point categories select a marker symbol where recognized; unknown categories use a generic marker. The initial view includes every visible geometry, with a maximum initial zoom of 15 for isolated points. + +The table contains one row per Feature, including a named fallback for raw geometries. It opens by default for up to eight rows; larger tables start collapsed and offer a text filter. Optional columns appear when the data supplies them: + +| Column | Property | +| --- | --- | +| Name | `name`, or layer name and feature number | +| Category / Type | `category`, then `type`, then layer role | +| Layer | Layer name; hidden layers retain their rows with a visible state label | +| Route section | `routeSection` or `route_section` | +| Details | `note`, `description`, or `desc` | +| π | Google Maps link for Point coordinates only; other geometry cells stay empty | +| Source | Feature `source.url`, `source` URL string, `sourceUrl`, or `source_url`; falls back to layer `source.url` | + +Select a name with the keyboard, or click a row, to reveal its layer, scroll to the map, focus the geometry, and open its popup. Selecting a map feature highlights it and its row without scrolling the page, expanding the table, or changing its filter. Hover highlights do not move the map. Google Maps and source links open a separate tab without selecting the row. Printed pages include expanded feature tables. + +Invalid JSON displays a local error. Invalid layers are omitted while valid siblings remain visible. Empty collections and unlocated features are accepted. Coordinates must be finite longitude/latitude values; polygon rings must be closed. Technical details go to the browser console. Feature text is rendered as text, and source URLs accept only HTTP or HTTPS. + +For development, `npm test` builds the template and tests normalization, the table, actual Leaflet interactions, failure handling, and the generated HTML. GeoMap modules live in `src/geomap/`; the build inlines their bundle and CSS into `dist/template.html`. Run `npm run build:template` after editing them to refresh the editable root `index.html` page's bundle. diff --git a/examples/geomap.md b/examples/geomap.md new file mode 100644 index 0000000..d1c5398 --- /dev/null +++ b/examples/geomap.md @@ -0,0 +1,78 @@ +# GeoMap example + +This illustrative dataset demonstrates places, routes, and a boundary. The route lines are examples, not navigation tracks. + +Select a name in the table to focus the map. Map clicks stay on the map. Point rows also have a small Google Maps link. + +```geomap +{ + "layers": [ + { + "id": "places", + "name": "Points of interest", + "role": "poi", + "source": { "url": "https://www.openstreetmap.org/" }, + "geojson": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { "name": "Valldemossa", "category": "settlement", "routeSection": "Core", "note": "Village reference point." }, + "geometry": { "type": "Point", "coordinates": [2.62300, 39.71080] } + }, + { + "type": "Feature", + "properties": { "name": "Illustrative viewpoint", "category": "viewpoint", "routeSection": "Detour" }, + "geometry": { "type": "Point", "coordinates": [2.641, 39.721] } + } + ] + } + }, + { + "id": "route", + "name": "Example route", + "role": "route", + "geojson": { + "type": "Feature", + "properties": { "name": "Illustrative route", "description": "One table entry for the entire line." }, + "geometry": { "type": "LineString", "coordinates": [[2.623, 39.7108], [2.632, 39.716], [2.647, 39.719]] } + } + }, + { + "id": "detour", + "name": "Example detour", + "role": "route", + "geojson": { + "type": "Feature", + "properties": { "name": "Illustrative detour", "route_section": "Detour" }, + "geometry": { "type": "MultiLineString", "coordinates": [[[2.632, 39.716], [2.641, 39.721]], [[2.641, 39.721], [2.647, 39.719]]] } + } + }, + { + "id": "boundary", + "name": "Example boundary", + "role": "boundary", + "geojson": { + "type": "Feature", + "properties": { "name": "Illustrative area" }, + "geometry": { "type": "Polygon", "coordinates": [[[2.629, 39.71], [2.648, 39.71], [2.648, 39.725], [2.629, 39.71]]] } + } + } + ] +} +``` + +Multiple maps have independent layers and selection: + +```geomap +{ + "layers": [ + { + "id": "single-point", + "name": "Single point", + "role": "poi", + "geojson": { "type": "Point", "coordinates": [2.62300, 39.71080] } + } + ] +} +``` diff --git a/index.html b/index.html index 904f85d..217ad4e 100644 --- a/index.html +++ b/index.html @@ -36,6 +36,7 @@ /* Standard scrollbar properties override the custom parts unless both are auto. */ @supports selector(::-webkit-scrollbar){.primary{scrollbar-width:auto;scrollbar-color:auto}.primary::-webkit-scrollbar-button{display:none;width:0;height:0}} +
Skip to content @@ -805,6 +806,7 @@ +