diff options
Diffstat (limited to 'map')
| -rw-r--r-- | map/controls.js | 39 | ||||
| -rw-r--r-- | map/index.html | 18 | ||||
| -rw-r--r-- | map/main.js | 284 | ||||
| -rw-r--r-- | map/style.css | 28 |
4 files changed, 369 insertions, 0 deletions
diff --git a/map/controls.js b/map/controls.js new file mode 100644 index 0000000..e407a0c --- /dev/null +++ b/map/controls.js @@ -0,0 +1,39 @@ +export class CustomControl { + _map = undefined; + _container = undefined; + _data = undefined; + + constructor(data) { + if (Object.hasOwn(data, "label") && + Object.hasOwn(data, "icon") && + Object.hasOwn(data, "onclick")) { + this._data = data; + } else { + throw new Error("missing required property"); + } + } + + onAdd(map) { + this._map = map; + this._container = document.createElement('div'); + this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group'; + + const button = document.createElement('button'); + button.type = 'button'; + button.title = this._data.label; + button.textContent = this._data.icon; + button.onclick = () => { + this._data.onclick(); + } + + this._container.appendChild(button); + // this._map.on('mousedown', () => { }); + return this._container; + } + + onRemove() { + this._container.parentNode.removeChild(this._container); + // this._map.off('mousedown', () => { }); + this._map = undefined; + } +} diff --git a/map/index.html b/map/index.html new file mode 100644 index 0000000..a0ca740 --- /dev/null +++ b/map/index.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="color-scheme" content="dark light"> + <link href="style.css" rel="stylesheet"> + <script type="module" src="main.js"></script> + </head> + <body> + <header> + <nav> + <a href="/"><img src="/media/interface/logo.svg" alt="gzr.im"></a> + </nav> + </header> + <main id="map"></main> + </body> +</html> diff --git a/map/main.js b/map/main.js new file mode 100644 index 0000000..5340ca1 --- /dev/null +++ b/map/main.js @@ -0,0 +1,284 @@ +import * as MaplibreGL from 'https://cdn.skypack.dev/maplibre-gl?min'; +import * as Diplomat from 'https://cdn.skypack.dev/@americana/diplomat?min'; +import * as PMTiles from 'https://cdn.skypack.dev/pmtiles?min'; +import * as D3 from 'https://cdn.skypack.dev/d3?min'; +import turf from 'https://cdn.skypack.dev/turf?min'; + + +// local imports +import { + CustomControl, +} from './controls.js'; + +// add the PMTiles plugin to the maplibregl global. +const pmproto = new PMTiles.Protocol(); +MaplibreGL.addProtocol('pmtiles', pmproto.tile); + +// globals +export const map = initMap(); +export const urlState = initUrlState(); + + +function initUrlState() { + const params = new URLSearchParams(window.location.search); + + if (params.has('show')) { + const show = params.get('show'); + const vals = show.split(','); + if (vals.includes('japan')) { + loadJapan(); + } + } + + return params; +} + +function initMap() { + const map = new MaplibreGL.Map({ + container: 'map', + style: 'https://tiles.openfreemap.org/styles/positron', + }); + + // overwrite basemap style + const style = D3.json('https://tiles.openfreemap.org/styles/positron') + style.then((data) => { + data.layers = data.layers.map((l) => { + if (l.id === "boundary_2") { + l.paint["line-color"] = "red"; + } + if (l.id === "boundary_3") { + l.minzoom = 4; + l.paint["line-color"] = "orange"; + } + if (l.id === "label_state") { + l.minzoom = 4; + l.paint["text-color"] = "orange"; + } + if (l.id === "label_other") { + l.minzoom = 4; + l.paint["text-color"] = "orange"; + } + return l; + }); + map.setStyle(data); + }); + + map.addControl(new MaplibreGL.FullscreenControl()); + + map.addControl(new MaplibreGL.NavigationControl({ + showZoom: true, + showCompass: true, + visualizeRoll: true, + visualizePitch: true, + })); + + map.addControl(new MaplibreGL.GlobeControl()); + + map.addControl(new MaplibreGL.GeolocateControl({ + positionOptions: { + enableHighAccuracy: true + }, + })); + + map.addControl(new CustomControl({ + label: 'Japan', + icon: '🇯🇵', + onclick: () => { + loadJapan(); + }, + })); + map.addControl(new CustomControl({ + label: 'Raw', + icon: 'R', + onclick: () => { + loadRaw(); + }, + })); + map.addControl(new CustomControl({ + label: 'Visited', + icon: 'V', + onclick: () => { + loadVisited(); + }, + })); + + // https://github.com/osm-americana/diplomat/ + map.once("styledata", (event) => { + Diplomat.localizeStyle(map); + }); + + return map; +} + + +function loadRaw() { + const raw = D3.json('export-data/data.geojson'); + raw.then((data) => { + if (map.getSource('raw') === undefined) { + map.addSource('raw', { + type: 'geojson', + data: data, + }); + map.addLayer({ + id: 'raw', + source: 'raw', + type: 'heatmap', + paint: { + // Increase the heatmap weight based on frequency and property magnitude + 'heatmap-weight': [ + 'interpolate', + ['linear'], + ['get', 'mag'], + 0, + 0, + 6, + 1 + ], + // Increase the heatmap color weight weight by zoom level + // heatmap-intensity is a multiplier on top of heatmap-weight + 'heatmap-intensity': [ + 'interpolate', + ['linear'], + ['zoom'], + 0, + 1, + 9, + 3 + ], + // Color ramp for heatmap. Domain is 0 (low) to 1 (high). + // Begin color ramp at 0-stop with a 0-transparency color + // to create a blur-like effect. + 'heatmap-color': [ + 'interpolate', + ['linear'], + ['heatmap-density'], + 0, + 'rgba(33,102,172,0)', + 0.2, + 'rgb(103,169,207)', + 0.4, + 'rgb(209,229,240)', + 0.6, + 'rgb(253,219,199)', + 0.8, + 'rgb(239,138,98)', + 1, + 'rgb(178,24,43)' + ], + // Adjust the heatmap radius by zoom level + 'heatmap-radius': [ + 'interpolate', + ['linear'], + ['zoom'], + 0, + 2, + 9, + 20 + ], + }, + }); + } + map.fitBounds(turf.bbox(data), { + padding: 50, + }); + }) +} + +function loadVisited() { + // add data + const vis = D3.json('export-data/visited.geojson'); + vis.then((data) => { + if (map.getSource('visited') === undefined) { + map.addSource('visited', { + type: 'geojson', + data: data, + }); + // boundaries + map.addLayer({ + id: 'visited-bounds', + type: 'fill', + source: 'visited', + layout: {}, + paint: { + 'fill-color': 'hotpink', + 'fill-opacity': 0.6, + }, + filter: ['==', '$type', 'Polygon'], + }); + // labels + map.addLayer({ + id: 'visited-labels', + type: 'symbol', + source: 'visited', + layout: { + 'text-field': '{iso3166}\n{timestamp}', + 'text-font': ['Noto Sans Regular'], + 'text-size': 12, + 'text-variable-anchor': ['top', 'bottom', 'left', 'right'], + 'text-radial-offset': 0.3, + 'text-justify': 'auto', + }, + paint: { + 'text-color': 'black', + 'text-opacity': 1.0, + }, + filter: ['==', '$type', 'Polygon'], + }); + } + map.fitBounds(turf.bbox(data), { + padding: 50, + }); + }); +} + +function loadJapan() { + function normalizeLocation() { + let addr = window.location.toString(); + let idx = addr.lastIndexOf("/"); + return addr.slice(0, idx); + } + + const jpn_url = 'export-data/japan-admin_level=4.pmtiles' + const jpn = new PMTiles.PMTiles(jpn_url); + pmproto.add(jpn); + jpn.getHeader().then(hdr => { + map.fitBounds([[hdr.minLon, hdr.minLat],[hdr.maxLon, hdr.maxLat]], { + padding: 1, + }); + // map.addSource('jpn', { + // type: 'vector', + // url: `pmtiles://${normalizeLocation()}/${jpn_url}`, + // attribution: '© <a href="https://osm-boundaries.com/about">OSM-Boundaries</a>' + // }); + // map.addLayer({ + // id: 'jpn-bounds', + // source: 'jpn', + // 'source-layer': 'japan-bounds', + // type: 'line', + // layout: {}, + // paint: { + // 'line-color': 'orange', + // 'line-width': 2, + // 'line-opacity': 0.5, + // }, + // }); + // map.addLayer({ + // id: 'jpn-labels', + // source: 'jpn', + // 'source-layer': 'japan-labels', + // type: 'symbol', + // layout: { + // 'text-field': '{name_en}', + // 'text-font': ['Noto Sans Regular'], + // 'text-size': 12, + // 'text-variable-anchor': ['top', 'bottom', 'left', 'right'], + // 'text-radial-offset': 0.2, + // 'text-justify': 'auto', + // }, + // paint: { + // 'text-color': 'black', + // 'text-opacity': 1.0, + // }, + // }); + }); +} diff --git a/map/style.css b/map/style.css new file mode 100644 index 0000000..b9639bf --- /dev/null +++ b/map/style.css @@ -0,0 +1,28 @@ +@import url("https://esm.sh/maplibre-gl/dist/maplibre-gl.css"); + +body { + margin: 0; +} + +#map { + width: 100vw; + height: 100vh; +} + +header { + position: absolute; + top: 0; + left: 0; + z-index: 9999; + padding: 15px 10px; + + & nav a { + background: black; + color: white; + + & [alt="gzr.im"] { + max-height: 55px; + vertical-align: bottom; + } + } +} |