diff --git a/docs/third-party-notices.adoc b/docs/third-party-notices.adoc index 250aaad..6fa39bd 100644 --- a/docs/third-party-notices.adoc +++ b/docs/third-party-notices.adoc @@ -21,6 +21,10 @@ BSD-3-Clause licensed. | protomaps-themes-base | 4.5.0 | https://github.com/protomaps/basemaps + +| maplibre-contour (Overlays panel's "Contour lines", `public/js/contour.js`) +| 0.1.0 +| https://github.com/onthegomap/maplibre-contour |=== Also vendored under `public/vendor/`, but not BSD-3-Clause like the diff --git a/public/js/contour.js b/public/js/contour.js new file mode 100644 index 0000000..3ffc30f --- /dev/null +++ b/public/js/contour.js @@ -0,0 +1,99 @@ +import { addProtocol } from "/vendor/maplibre-gl/maplibre-gl.mjs"; +import { ONLINE_TERRAIN_TILES } from "/js/overlays.js"; +import { firstSymbolLayerId } from "/js/radar.js"; +import { CONTOUR_ICON } from "/js/icons.js"; + +const SOURCE_ID = "contour-lines"; +const LINE_LAYER_ID = "contour-lines-line"; + +// Topographic contour lines, generated client-side by the vendored +// maplibre-contour plugin (public/vendor/maplibre-contour/) from the same +// online Terrarium DEM tiles overlays.js's HillshadeControl already +// fetches. Online only, unlike Terrain shading/Terrain shadow: the +// plugin's DemSource has no public hook to route its own tile fetch +// through the pmtiles:// protocol HillshadeControl uses for the offline +// basemap, so — same story as the Wikipedia layer and Radar — this just +// quietly renders nothing without a network. +export class ContourControl { + constructor(map) { + this._map = map; + this._active = false; + this._demSource = new window.mlcontour.DemSource({ + url: ONLINE_TERRAIN_TILES[0], + encoding: "terrarium", + maxzoom: 15, + worker: true, + cacheSize: 100, + }); + this._demSource.setupMaplibre({ addProtocol }); + + // setStyle() (switching Online/Offline) tears down all custom sources/ + // layers — put contour lines back if they were on, same as every other + // Overlays control. + map.on("style.load", () => { + if (this._active) this._addLayer(); + }); + } + + mount(li) { + li.innerHTML = ` + + `; + this._checkbox = li.querySelector(".overlays-panel__toggle"); + this._checkbox.addEventListener("change", (event) => this._toggle(event.target.checked)); + } + + _toggle(active) { + this._active = active; + if (active) this._addLayer(); + else this._removeLayer(); + } + + _addLayer() { + if (this._map.getSource(SOURCE_ID)) return; + + // Starting-point thresholds (README's own generic example) — meters, + // more/tighter contours at higher zoom. Expect to eyeball-tune against + // real Tatras-scale terrain. + const tiles = [ + this._demSource.contourProtocolUrl({ + thresholds: { + 11: [200, 1000], + 12: [100, 500], + 14: [50, 200], + 15: [20, 100], + }, + contourLayer: "contours", + elevationKey: "ele", + levelKey: "level", + }), + ]; + this._map.addSource(SOURCE_ID, { type: "vector", tiles, maxzoom: 15 }); + this._map.addLayer( + { + id: LINE_LAYER_ID, + type: "line", + source: SOURCE_ID, + "source-layer": "contours", + paint: { + // Roughly the same brown as the mid-slope band of overlays.js's + // own ELEVATION_COLOR_RAMP, so this reads as part of the same + // terrain treatment rather than a bolted-on layer. + "line-color": "#8a6d3b", + "line-width": ["match", ["get", "level"], 1, 1.2, 0.6], + "line-opacity": 0.6, + }, + }, + firstSymbolLayerId(this._map), + ); + } + + _removeLayer() { + if (this._map.getLayer(LINE_LAYER_ID)) this._map.removeLayer(LINE_LAYER_ID); + if (this._map.getSource(SOURCE_ID)) this._map.removeSource(SOURCE_ID); + } +} diff --git a/public/js/icons.js b/public/js/icons.js index 5ecd1c8..ab3fa44 100644 --- a/public/js/icons.js +++ b/public/js/icons.js @@ -193,6 +193,18 @@ export const TERRAIN_SHADOW_ICON = ` `; +// Hand-drawn, not NPS-sourced — no contour-line pictogram in that set +// either. Concentric rings via fill-rule evenodd, same solid-glyph +// convention as WILDERNESS_ICON/RADAR_CLOUD_ICON/TERRAIN_SHADOW_ICON above — +// used as the Overlays panel's Contour lines glyph (contour.js). +export const CONTOUR_ICON = ` + + +`; + // src/bicycle-trail-22.svg — pulled out to its own constant since it's also // used unrasterized, inline, as PARK4NIGHT_TAGS.vtt below. const BICYCLE_TRAIL_ICON = ` diff --git a/public/js/overlays.js b/public/js/overlays.js index 3861e5f..b2c366c 100644 --- a/public/js/overlays.js +++ b/public/js/overlays.js @@ -4,6 +4,7 @@ import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js"; import { svgToImageData, badgeImageId, escapeHtml } from "/js/gpx.js"; import { RadarControl } from "/js/radar.js"; import { ShadowControl } from "/js/shadow.js"; +import { ContourControl } from "/js/contour.js"; const ICON_IMAGE_SIZE = 48; // rasterized once at this size, rendered much smaller by icon-size below const ICON_SIZE = 0.3; // 48 * 0.3 = ~14px, sized to sit inside the marker badge below @@ -125,6 +126,7 @@ export class OverlaysControl { this._hillshade = new HillshadeControl(map); this._radar = new RadarControl(map); this._shadow = new ShadowControl(map); + this._contour = new ContourControl(map); this._wikiEnabled = false; // the checkbox's own state this._wikiMoveHandler = null; // the moveend listener driving its live refetch, while enabled this._wikiRequestSeq = 0; // ignores an in-flight refetch a newer one has already superseded @@ -157,12 +159,14 @@ export class OverlaysControl {
  • +
  • `; - const [hillshadeItem, wikiItem, radarItem, shadowItem] = list.children; + const [hillshadeItem, wikiItem, radarItem, shadowItem, contourItem] = list.children; this._hillshade.mount(hillshadeItem); this._mountWiki(wikiItem); this._radar.mount(radarItem); this._shadow.mount(shadowItem); + this._contour.mount(contourItem); onlyOneSidePanelOpen(this._panel, () => this._icon.classList.remove("active")); diff --git a/public/js/radar.js b/public/js/radar.js index 00dd5a9..4bbbfca 100644 --- a/public/js/radar.js +++ b/public/js/radar.js @@ -357,7 +357,9 @@ function readCachedForecast() { } } -function firstSymbolLayerId(map) { +// Exported: contour.js's own contour-line layers want the same +// "sit above base fill/roads, below place labels" placement. +export function firstSymbolLayerId(map) { const layers = map.getStyle()?.layers || []; const symbolLayer = layers.find((layer) => layer.type === "symbol"); return symbolLayer ? symbolLayer.id : undefined; diff --git a/public/vendor/maplibre-contour/maplibre-contour.min.js b/public/vendor/maplibre-contour/maplibre-contour.min.js new file mode 100644 index 0000000..c54b8c8 --- /dev/null +++ b/public/vendor/maplibre-contour/maplibre-contour.min.js @@ -0,0 +1,15 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).mlcontour=e()}(this,(function(){"use strict";var t,e,i;function r(r,s){if(t)if(e){var n="var sharedChunk = {}; ("+t+")(sharedChunk); ("+e+")(sharedChunk);",o={};t(o),i=s(o),"undefined"!=typeof window&&(i.workerUrl=window.URL.createObjectURL(new Blob([n],{type:"text/javascript"})))}else e=s;else t=s}return r(0,(function(t){class e{constructor(t,e){this.start=t,this.end=e,this.points=[],this.append=this.append.bind(this),this.prepend=this.prepend.bind(this)}append(t,e){this.points.push(Math.round(t),Math.round(e))}prepend(t,e){this.points.splice(0,0,Math.round(t),Math.round(e))}lineString(){return this.toArray()}isEmpty(){return this.points.length<2}appendFragment(t){this.points.push(...t.points),this.end=t.end}toArray(){return this.points}}const i=[[],[[[1,2],[0,1]]],[[[2,1],[1,2]]],[[[2,1],[0,1]]],[[[1,0],[2,1]]],[[[1,2],[0,1]],[[1,0],[2,1]]],[[[1,0],[1,2]]],[[[1,0],[0,1]]],[[[0,1],[1,0]]],[[[1,2],[1,0]]],[[[0,1],[1,0]],[[2,1],[1,2]]],[[[2,1],[1,0]]],[[[0,1],[2,1]]],[[[1,2],[2,1]]],[[[0,1],[1,2]]],[]];function r(t,e,i,r){return(e=2*e+r[0])+(i=2*i+r[1])*(t+1)*2}function s(t,e,i){return(e-t)/(i-t)}function n(t,n,o=4096,a=1){if(!t)return{};const h=o/(n.width-1);let l,c,d,u,f,p;const w={},g=new Map,m=new Map;function b(t,e,i){0===t[0]?i(h*(p-1),h*(f-s(d,e,l))):2===t[0]?i(h*p,h*(f-s(u,e,c))):0===t[1]?i(h*(p-s(c,e,l)),h*(f-1)):i(h*(p-s(u,e,d)),h*f)}for(f=1-a;fs,o=c>s,a=d>s,h=u>s;for(const l of i[(t?8:0)|(o?4:0)|(h?2:0)|(a?1:0)]){let t=g.get(s);t||g.set(s,t=new Map);let i=m.get(s);i||m.set(s,i=new Map);const o=l[0],a=l[1],h=r(n.width,p,f,o),c=r(n.width,p,f,a);let d,u;if(d=i.get(h))if(i.delete(h),u=t.get(c))if(t.delete(c),d===u){if(b(a,s,d.append),!d.isEmpty()){let t=w[s];t||(w[s]=t=[]),t.push(d.lineString())}}else d.appendFragment(u),i.set(d.end=u.end,d);else b(a,s,d.append),i.set(d.end=c,d);else if(d=t.get(c))t.delete(c),b(o,s,d.prepend),t.set(d.start=h,d);else{const r=new e(h,c);b(o,s,r.append),b(a,s,r.append),t.set(h,r),i.set(c,r)}}}}}for(const[t,e]of g.entries()){let i=null;for(const r of e.values())r.isEmpty()||(null==i&&(i=w[t]||(w[t]=[])),i.push(r.lineString()))}return w} +/*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */function o(t,e){var i={};for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&e.indexOf(r)<0&&(i[r]=t[r]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols){var s=0;for(r=Object.getOwnPropertySymbols(t);ste?1:0)),e}function l(t){return h(t).map((([t,e])=>[t,..."number"==typeof e?[e]:e].join("*"))).join("~")}function c(t){return h(t).map((([t,e])=>`${encodeURIComponent(t)}=${encodeURIComponent(e)}`)).join(",")}let d=null;function u(){return null==d&&(d="undefined"!=typeof OffscreenCanvas&&new OffscreenCanvas(1,1).getContext("2d")&&"function"==typeof createImageBitmap),d||!1}let f=null;function p(t,e,i){let r=()=>{};const s=setTimeout((()=>{r(new Error("timed out")),null==i||i.abort()}),t);w(i,(()=>{r(new Error("aborted")),clearTimeout(s)}));const n=new Promise(((t,e)=>{r=e}));return Promise.race([n,e.finally((()=>clearTimeout(s)))])}function w(t,e){e&&(null==t||t.signal.addEventListener("abort",e))}function g(t){var e;return Boolean(null===(e=null==t?void 0:t.signal)||void 0===e?void 0:e.aborted)}let m,b,y,v,P=0;class x{constructor(t=100){this.size=()=>this.items.size,this.get=(t,e,i)=>{let r=this.items.get(t);if(r)r.lastUsed=++P,r.waiting++;else{const i=new AbortController,s=e(t,i);r={abortController:i,item:s,lastUsed:++P,waiting:1},this.items.set(t,r),this.prune()}const s=this.items,n=r.item.then((t=>t),(e=>(s.delete(t),Promise.reject(e))));let o=!1;return w(i,(()=>{var e;r&&r.abortController&&!o&&(o=!0,--r.waiting<=0&&(null===(e=r.abortController)||void 0===e||e.abort(),s.delete(t)))})),n},this.clear=()=>this.items.clear(),this.maxSize=t,this.items=new Map}prune(){if(this.items.size>this.maxSize){let t,e=1/0;this.items.forEach(((i,r)=>{i.lastUsedr.src=""));return V(yield new Promise(((e,s)=>{r.onload=()=>{g(i)||e(r),URL.revokeObjectURL(r.src),r.onload=null},r.onerror=()=>s(new Error("Could not load image.")),r.src=t.size?URL.createObjectURL(t):""})),e,y,v)}))};function V(t,e,i,r){if(i.width=t.width,i.height=t.height,!r)throw new Error("failed to get context");r.drawImage(t,0,0,t.width,t.height);const s=r.getImageData(0,0,t.width,t.height).data;return M(t.width,t.height,e,s)}function M(t,e,i,r){const s="mapbox"===i?(t,e,i)=>.1*(256*t*256+256*e+i)-1e4:(t,e,i)=>256*t+e+i/256-32768,n=new Float32Array(t*e);for(let t=0;t{if(0===t)return this;const r=1<this.get(t+s,e+n)))},this.subsamplePixelCenters=t=>{const e=(t,e,i)=>isNaN(t)?e:isNaN(e)?t:t+(e-t)*i;if(t<=1)return this;const i=.5-1/(2*t);return new S(this.width*t,this.height*t,((r,s)=>{const n=r/t-i,o=s/t-i,a=Math.floor(n),h=Math.floor(o),l=this.get(a,h),c=this.get(a+1,h),d=this.get(a,h+1),u=this.get(a+1,h+1),f=n-a,p=o-h,w=e(l,c,f),g=e(d,u,f);return e(w,g,p)}))},this.averagePixelCentersToGrid=(t=1)=>new S(this.width+1,this.height+1,((e,i)=>{let r=0,s=0,n=0;for(let o=e-t;o1===t?this:new S(this.width,this.height,((e,i)=>this.get(e,i)*t)),this.materialize=(t=2)=>{const e=this.width+2*t,i=new Float32Array(e*(this.height+2*t));let r=0;for(let e=-t;ei[(s+t)*e+r+t]))},this.get=i,this.width=t,this.height=e}static fromRawDem(t){return new S(t.width,t.height,((e,i)=>{const r=t.data[i*t.width+e];return s=r,!isNaN(s)&&s>=-12e3&&s<=9e3?r:NaN;var s}))}static combineNeighbors(t){if(9!==t.length)throw new Error("Must include a tile plus 8 neighbors");const e=t[4];if(!e)return;const i=e.width,r=e.height;return new S(i,r,((e,s)=>{let n=0;s<0?s+=r:s>3,s=this.pos;this.type=7&i,t(r,e,this),this.pos===s&&this.skip(i)}return e}readMessage(t,e){return this.readFields(t,e,this.readVarint()+this.pos)}readFixed32(){const t=this.dataView.getUint32(this.pos,!0);return this.pos+=4,t}readSFixed32(){const t=this.dataView.getInt32(this.pos,!0);return this.pos+=4,t}readFixed64(){const t=this.dataView.getUint32(this.pos,!0)+this.dataView.getUint32(this.pos+4,!0)*T;return this.pos+=8,t}readSFixed64(){const t=this.dataView.getUint32(this.pos,!0)+this.dataView.getInt32(this.pos+4,!0)*T;return this.pos+=8,t}readFloat(){const t=this.dataView.getFloat32(this.pos,!0);return this.pos+=4,t}readDouble(){const t=this.dataView.getFloat64(this.pos,!0);return this.pos+=8,t}readVarint(t){const e=this.buf;let i,r;return r=e[this.pos++],i=127&r,r<128?i:(r=e[this.pos++],i|=(127&r)<<7,r<128?i:(r=e[this.pos++],i|=(127&r)<<14,r<128?i:(r=e[this.pos++],i|=(127&r)<<21,r<128?i:(r=e[this.pos],i|=(15&r)<<28,function(t,e,i){const r=i.buf;let s,n;if(n=r[i.pos++],s=(112&n)>>4,n<128)return U(t,s,e);if(n=r[i.pos++],s|=(127&n)<<3,n<128)return U(t,s,e);if(n=r[i.pos++],s|=(127&n)<<10,n<128)return U(t,s,e);if(n=r[i.pos++],s|=(127&n)<<17,n<128)return U(t,s,e);if(n=r[i.pos++],s|=(127&n)<<24,n<128)return U(t,s,e);if(n=r[i.pos++],s|=(1&n)<<31,n<128)return U(t,s,e);throw new Error("Expected varint not more than 10 bytes")}(i,t,this)))))}readVarint64(){return this.readVarint(!0)}readSVarint(){const t=this.readVarint();return t%2==1?(t+1)/-2:t/2}readBoolean(){return Boolean(this.readVarint())}readString(){const t=this.readVarint()+this.pos,e=this.pos;return this.pos=t,t-e>=12&&C?C.decode(this.buf.subarray(e,t)):function(t,e,i){let r="",s=e;for(;s239?4:e>223?3:e>191?2:1;if(s+l>i)break;1===l?e<128&&(h=e):2===l?(n=t[s+1],128==(192&n)&&(h=(31&e)<<6|63&n,h<=127&&(h=null))):3===l?(n=t[s+1],o=t[s+2],128==(192&n)&&128==(192&o)&&(h=(15&e)<<12|(63&n)<<6|63&o,(h<=2047||h>=55296&&h<=57343)&&(h=null))):4===l&&(n=t[s+1],o=t[s+2],a=t[s+3],128==(192&n)&&128==(192&o)&&128==(192&a)&&(h=(15&e)<<18|(63&n)<<12|(63&o)<<6|63&a,(h<=65535||h>=1114112)&&(h=null))),null===h?(h=65533,l=1):h>65535&&(h-=65536,r+=String.fromCharCode(h>>>10&1023|55296),h=56320|1023&h),r+=String.fromCharCode(h),s+=l}return r}(this.buf,e,t)}readBytes(){const t=this.readVarint()+this.pos,e=this.buf.subarray(this.pos,t);return this.pos=t,e}readPackedVarint(t=[],e){const i=this.readPackedEnd();for(;this.pos127;);else if(2===e)this.pos=this.readVarint()+this.pos;else if(5===e)this.pos+=4;else{if(1!==e)throw new Error(`Unimplemented type: ${e}`);this.pos+=8}}writeTag(t,e){this.writeVarint(t<<3|e)}realloc(t){let e=this.length||16;for(;e268435455||t<0?function(t,e){let i,r;t>=0?(i=t%4294967296|0,r=t/4294967296|0):(i=~(-t%4294967296),r=~(-t/4294967296),4294967295^i?i=i+1|0:(i=0,r=r+1|0));if(t>=0x10000000000000000||t<-0x10000000000000000)throw new Error("Given varint doesn't fit into 10 bytes");e.realloc(10),function(t,e,i){i.buf[i.pos++]=127&t|128,t>>>=7,i.buf[i.pos++]=127&t|128,t>>>=7,i.buf[i.pos++]=127&t|128,t>>>=7,i.buf[i.pos++]=127&t|128,t>>>=7,i.buf[i.pos]=127&t}(i,0,e),function(t,e){const i=(7&t)<<4;if(e.buf[e.pos++]|=i|((t>>>=3)?128:0),!t)return;if(e.buf[e.pos++]=127&t|((t>>>=7)?128:0),!t)return;if(e.buf[e.pos++]=127&t|((t>>>=7)?128:0),!t)return;if(e.buf[e.pos++]=127&t|((t>>>=7)?128:0),!t)return;if(e.buf[e.pos++]=127&t|((t>>>=7)?128:0),!t)return;e.buf[e.pos++]=127&t}(r,e)}(t,this):(this.realloc(4),this.buf[this.pos++]=127&t|(t>127?128:0),t<=127||(this.buf[this.pos++]=127&(t>>>=7)|(t>127?128:0),t<=127||(this.buf[this.pos++]=127&(t>>>=7)|(t>127?128:0),t<=127||(this.buf[this.pos++]=t>>>7&127))))}writeSVarint(t){this.writeVarint(t<0?2*-t-1:2*t)}writeBoolean(t){this.writeVarint(+t)}writeString(t){t=String(t),this.realloc(4*t.length),this.pos++;const e=this.pos;this.pos=function(t,e,i){for(let r,s,n=0;n55295&&r<57344){if(!s){r>56319||n+1===e.length?(t[i++]=239,t[i++]=191,t[i++]=189):s=r;continue}if(r<56320){t[i++]=239,t[i++]=191,t[i++]=189,s=r;continue}r=s-55296<<10|r-56320|65536,s=null}else s&&(t[i++]=239,t[i++]=191,t[i++]=189,s=null);r<128?t[i++]=r:(r<2048?t[i++]=r>>6|192:(r<65536?t[i++]=r>>12|224:(t[i++]=r>>18|240,t[i++]=r>>12&63|128),t[i++]=r>>6&63|128),t[i++]=63&r|128)}return i}(this.buf,t,this.pos);const i=this.pos-e;i>=128&&I(e,i,this),this.pos=e-1,this.writeVarint(i),this.pos+=i}writeFloat(t){this.realloc(4),this.dataView.setFloat32(this.pos,t,!0),this.pos+=4}writeDouble(t){this.realloc(8),this.dataView.setFloat64(this.pos,t,!0),this.pos+=8}writeBytes(t){const e=t.length;this.writeVarint(e),this.realloc(e);for(let i=0;i=128&&I(i,r,this),this.pos=i-1,this.writeVarint(r),this.pos+=r}writeMessage(t,e,i){this.writeTag(t,2),this.writeRawMessage(e,i)}writePackedVarint(t,e){e.length&&this.writeMessage(t,E,e)}writePackedSVarint(t,e){e.length&&this.writeMessage(t,B,e)}writePackedBoolean(t,e){e.length&&this.writeMessage(t,j,e)}writePackedFloat(t,e){e.length&&this.writeMessage(t,A,e)}writePackedDouble(t,e){e.length&&this.writeMessage(t,D,e)}writePackedFixed32(t,e){e.length&&this.writeMessage(t,z,e)}writePackedSFixed32(t,e){e.length&&this.writeMessage(t,R,e)}writePackedFixed64(t,e){e.length&&this.writeMessage(t,$,e)}writePackedSFixed64(t,e){e.length&&this.writeMessage(t,L,e)}writeBytesField(t,e){this.writeTag(t,2),this.writeBytes(e)}writeFixed32Field(t,e){this.writeTag(t,5),this.writeFixed32(e)}writeSFixed32Field(t,e){this.writeTag(t,5),this.writeSFixed32(e)}writeFixed64Field(t,e){this.writeTag(t,1),this.writeFixed64(e)}writeSFixed64Field(t,e){this.writeTag(t,1),this.writeSFixed64(e)}writeVarintField(t,e){this.writeTag(t,0),this.writeVarint(e)}writeSVarintField(t,e){this.writeTag(t,0),this.writeSVarint(e)}writeStringField(t,e){this.writeTag(t,2),this.writeString(e)}writeFloatField(t,e){this.writeTag(t,5),this.writeFloat(e)}writeDoubleField(t,e){this.writeTag(t,1),this.writeDouble(e)}writeBooleanField(t,e){this.writeVarintField(t,+e)}}function U(t,e,i){return i?4294967296*e+(t>>>0):4294967296*(e>>>0)+(t>>>0)}function I(t,e,i){const r=e<=16383?1:e<=2097151?2:e<=268435455?3:Math.floor(Math.log(e)/(7*Math.LN2));i.realloc(r);for(let e=i.pos-1;e>=t;e--)i.buf[e+r]=i.buf[e]}function E(t,e){for(let i=0;i>31}function H(t,e){if(!e)throw new Error;const i=t.geometry,r=t.type;let s=0,n=0;for(const t of i){let i=1;r===G.POINT&&(i=t.length/2),e.writeVarint(Y(1,i));const o=t.length/2,a=r===G.POLYGON?o-1:o;for(let i=0;i{this.markFinish();const e=t=>{const e=this.marks[t]||[],i=Math.max(...e.map((t=>Math.max(...t)))),r=Math.min(...e.map((t=>Math.min(...t))));return Number.isFinite(i)?i-r:void 0},i=e("main")||0,r=e("fetch"),s=e("decode"),n=e("isoline");return{url:t,tilesUsed:this.tilesFetched,origin:this.timeOrigin,marks:this.marks,resources:[...this.resources,...et(this.fetched.map(Z))],duration:i,fetch:r,decode:s,process:n,wait:i-(r||0)-(s||0)-(n||0)}},this.error=t=>Object.assign(Object.assign({},this.finish(t)),{error:!0}),this.marker=t=>{var e;this.marks[t]||(this.marks[t]=[]);const i=[tt()];return null===(e=this.marks[t])||void 0===e||e.push(i),()=>i.push(tt())},this.useTile=t=>{this.urls.indexOf(t)<0&&(this.urls.push(t),this.tilesFetched++)},this.fetchTile=t=>{this.fetched.indexOf(t)<0&&this.fetched.push(t)},this.addAll=t=>{var e;this.tilesFetched+=t.tilesUsed;const i=t.origin-this.timeOrigin;for(const r in t.marks){const s=r;(this.marks[s]||(this.marks[s]=[])).push(...(null===(e=t.marks[s])||void 0===e?void 0:e.map((t=>t.map((t=>t+i)))))||[])}this.resources.push(...t.resources.map((t=>function(t,e){const i={};for(const r in t)0!==t[r]&&rt.test(r)?i[r]=Number(t[r])+e:i[r]=t[r];return i}(t,i))))},this.markFinish=this.marker(t)}}const rt=/(Start$|End$|^start|^end)/;const st=(t,e)=>a(void 0,void 0,void 0,(function*(){const i={signal:e.signal},r=yield fetch(t,i);if(!r.ok)throw new Error(`Bad response: ${r.status} for ${t}`);return{data:yield r.blob(),expires:r.headers.get("expires")||void 0,cacheControl:r.headers.get("cache-control")||void 0}}));let nt=0;t.A=class{constructor(t,e,i=2e4){this.callbacks={},this.cancels={},this.dest=t,this.timeoutMs=i,this.dest.onmessage=t=>a(this,[t],void 0,(function*({data:t}){const i=t;if("cancel"===i.type){const t=this.cancels[i.id];delete this.cancels[i.id],null==t||t.abort()}else if("response"===i.type){const t=this.callbacks[i.id];delete this.callbacks[i.id],t&&t(i.error?new Error(i.error):void 0,i.response,i.timings)}else if("request"===i.type){const t=new it("worker"),r=e[i.name],s=new AbortController,n=r.apply(r,[...i.args,s,t]),o=`${i.name}_${i.id}`;if(i.id&&n){this.cancels[i.id]=s;try{const e=yield n,r=null==e?void 0:e.transferrables;this.postMessage({id:i.id,type:"response",response:e,timings:t.finish(o)},r)}catch(e){this.postMessage({id:i.id,type:"response",error:(null==e?void 0:e.toString())||"error",timings:t.finish(o)})}delete this.cancels[i.id]}}}))}postMessage(t,e){this.dest.postMessage(t,e||[])}send(t,e,i,r,...s){const n=++nt,o=new Promise(((i,o)=>{this.postMessage({id:n,type:"request",name:t,args:s},e),this.callbacks[n]=(t,e,s)=>{null==r||r.addAll(s),t?o(t):i(e)}}));return w(i,(()=>{delete this.callbacks[n],this.postMessage({id:n,type:"cancel"})})),p(this.timeoutMs,o,i)}},t.H=S,t.L=class{constructor(t){this.loaded=Promise.resolve(),this.fetchAndParseTile=(t,e,i,r,s)=>{const n=this,o=this.demUrlPattern.replace("{z}",t.toString()).replace("{x}",e.toString()).replace("{y}",i.toString());return null==s||s.useTile(o),this.parsedCache.get(o,((r,o)=>a(this,void 0,void 0,(function*(){const r=yield n.fetchTile(t,e,i,o,s);if(g(o))throw new Error("canceled");const a=n.decodeImage(r.data,n.encoding,o),h=null==s?void 0:s.marker("decode"),l=yield a;return null==h||h(),l}))),r)},this.tileCache=new x(t.cacheSize),this.parsedCache=new x(t.cacheSize),this.contourCache=new x(t.cacheSize),this.timeoutMs=t.timeoutMs,this.demUrlPattern=t.demUrlPattern,this.encoding=t.encoding,this.maxzoom=t.maxzoom,this.decodeImage=t.decodeImage||k,this.getTile=t.getTile||st}fetchTile(t,e,i,r,s){const n=this.demUrlPattern.replace("{z}",t.toString()).replace("{x}",e.toString()).replace("{y}",i.toString());return null==s||s.useTile(n),this.tileCache.get(n,((t,e)=>{null==s||s.fetchTile(n);const i=null==s?void 0:s.marker("fetch");return p(this.timeoutMs,this.getTile(n,e).finally((()=>null==i?void 0:i())),e)}),r)}fetchDem(t,e,i,r,s,n){return a(this,void 0,void 0,(function*(){const o=Math.min(t-(r.overzoom||0),this.maxzoom),a=t-o,h=1<a(this,void 0,void 0,(function*(){const s=1<=s?void 0:this.fetchDem(t,(i+s)%s,n,r,c,o));const b=yield Promise.all(a);let y=S.combineNeighbors(b);if(!y||g(c))return{arrayBuffer:(new Uint8Array).buffer};const v=null==o?void 0:o.marker("isoline");if(y.width>=m)y=y.materialize(2);else for(;y.width{const i=Number(t);return{type:G.LINESTRING,geometry:e,properties:{[p]:i,[w]:Math.max(...h.map(((t,e)=>i%t==0?e:0)))}}}))}}});return null==v||v(),{arrayBuffer:x.buffer}}))),s)}},t.T=it,t._=a,t.a=function(t){return Object.fromEntries(t.replace(/^.*\?/,"").split("&").map((t=>{const e=t.split("=").map(decodeURIComponent),i=e[0];let r=e[1];switch(i){case"thresholds":s=r,r=Object.fromEntries(s.split("~").map((t=>t.split("*").map(Number))).map((([t,...e])=>[t,e])));break;case"extent":case"multiplier":case"overzoom":case"buffer":r=Number(r)}var s;return[i,r]})))},t.b=n,t.c=M,t.d=k,t.e=function(t){var{thresholds:e}=t,i=o(t,["thresholds"]);return h(Object.assign({thresholds:l(e)},i)).map((([t,e])=>`${encodeURIComponent(t)}=${encodeURIComponent(e)}`)).join("&")},t.f=function(t){return t.then((({arrayBuffer:t})=>{const e=function(t){const e=new ArrayBuffer(t.byteLength);return new Uint8Array(e).set(new Uint8Array(t)),e}(t);return{arrayBuffer:e,transferrables:[e]}}))},t.g=function(t,e){const{thresholds:i}=t,r=o(t,["thresholds"]);let s=[],n=-1/0;return Object.entries(i).forEach((([t,i])=>{const r=Number(t);r<=e&&r>n&&(n=r,s="number"==typeof i?[i]:i)})),Object.assign({levels:s},r)},t.p=function(t,e){return t.then((t=>{var{data:i}=t,r=o(t,["data"]);let s=i;return e&&(s=new Float32Array(i.length),s.set(i)),Object.assign(Object.assign({},r),{data:s,transferrables:[s.buffer]})}))}})),r(0,(function(t){const e=t=>Promise.reject(new Error(`No manager registered for ${t}`));const i="undefined"!=typeof self?self:"undefined"!=typeof window?window:global;i.actor=new t.A(i,new class{constructor(){this.managers={},this.init=(e,i)=>(this.managers[e.managerId]=new t.L(e),Promise.resolve()),this.fetchTile=(t,i,r,s,n,o)=>{var a;return(null===(a=this.managers[t])||void 0===a?void 0:a.fetchTile(i,r,s,n,o))||e(t)},this.fetchAndParseTile=(i,r,s,n,o,a)=>{var h;return t.p((null===(h=this.managers[i])||void 0===h?void 0:h.fetchAndParseTile(r,s,n,o,a))||e(i),!0)},this.fetchContourTile=(i,r,s,n,o,a,h)=>{var l;return t.f((null===(l=this.managers[i])||void 0===l?void 0:l.fetchContourTile(r,s,n,o,a,h))||e(i))}}})})),r(0,(function(t){const e={workerUrl:""};let i,r=0;class s{constructor(){this.decodeImage=(e,i,r)=>t.p(t.d(e,i,r),!1)}}class n{constructor(n){this.fetchTile=(t,e,i,r,s)=>this.actor.send("fetchTile",[],r,s,this.managerId,t,e,i),this.fetchAndParseTile=(t,e,i,r,s)=>this.actor.send("fetchAndParseTile",[],r,s,this.managerId,t,e,i),this.fetchContourTile=(t,e,i,r,s,n)=>this.actor.send("fetchContourTile",[],s,n,this.managerId,t,e,i,r);const o=this.managerId=++r;this.actor=n.actor||function(){if(!i){const r=new Worker(e.workerUrl),n=new s;i=new t.A(r,n)}return i}(),this.loaded=this.actor.send("init",[],new AbortController,void 0,Object.assign(Object.assign({},n),{managerId:o}))}}Blob.prototype.arrayBuffer||(Blob.prototype.arrayBuffer=function(){return new Promise(((t,e)=>{const i=new FileReader;i.onload=e=>{var i;return t(null===(i=e.target)||void 0===i?void 0:i.result)},i.onerror=e,i.readAsArrayBuffer(this)}))});const o=t=>(e,i)=>{if(i instanceof AbortController)return t(e,i);{const r=new AbortController;return t(e,r).then((t=>i(void 0,t.data,t.cacheControl,t.expires)),(t=>i(t))).catch((t=>i(t))),{cancel:()=>r.abort()}}},a=new Set;const h={generateIsolines:t.b,DemSource:class{constructor({url:e,cacheSize:i=100,id:r="dem",encoding:s="terrarium",maxzoom:h=12,worker:l=!0,timeoutMs:c=1e4,actor:d}){this.timingCallbacks=[],this.onTiming=t=>{this.timingCallbacks.push(t)},this.setupMaplibre=t=>{t.addProtocol(this.sharedDemProtocolId,this.sharedDemProtocol),t.addProtocol(this.contourProtocolId,this.contourProtocol)},this.sharedDemProtocolV4=(e,i)=>t._(this,void 0,void 0,(function*(){const[r,s,n]=this.parseUrl(e.url),o=new t.T("main");let a;try{const t=yield this.manager.fetchTile(r,s,n,i,o);a=o.finish(e.url);return{data:yield t.data.arrayBuffer(),cacheControl:t.cacheControl,expires:t.expires}}catch(t){throw a=o.error(e.url),t}finally{this.timingCallbacks.forEach((t=>t(a)))}})),this.contourProtocolV4=(e,i)=>t._(this,void 0,void 0,(function*(){const r=new t.T("main");let s;try{const[n,o,a]=this.parseUrl(e.url),h=t.a(e.url),l=yield this.manager.fetchContourTile(n,o,a,t.g(h,n),i,r);return s=r.finish(e.url),{data:l.arrayBuffer}}catch(t){throw s=r.error(e.url),t}finally{this.timingCallbacks.forEach((t=>t(s)))}})),this.contourProtocol=o(this.contourProtocolV4),this.sharedDemProtocol=o(this.sharedDemProtocolV4),this.contourProtocolUrl=e=>`${this.contourProtocolUrlBase}?${t.e(e)}`;let u=r,f=1;for(;a.has(u);)u=r+f++;a.add(u),this.sharedDemProtocolId=`${u}-shared`,this.contourProtocolId=`${u}-contour`,this.sharedDemProtocolUrl=`${this.sharedDemProtocolId}://{z}/{x}/{y}`,this.contourProtocolUrlBase=`${this.contourProtocolId}://{z}/{x}/{y}`;const p=l?n:t.L;this.manager=new p({demUrlPattern:e,cacheSize:i,encoding:s,maxzoom:h,timeoutMs:c,actor:d})}getDemTile(t,e,i,r){return this.manager.fetchAndParseTile(t,e,i,r||new AbortController)}parseUrl(t){const[,e,i,r]=/\/\/(\d+)\/(\d+)\/(\d+)/.exec(t)||[];return[Number(e),Number(i),Number(r)]}},HeightTile:t.H,LocalDemManager:t.L,decodeParsedImage:t.c,set workerUrl(t){e.workerUrl=t},get workerUrl(){return e.workerUrl}};return h})),i})); diff --git a/views/layout.erb b/views/layout.erb index d80bca0..8166e3c 100644 --- a/views/layout.erb +++ b/views/layout.erb @@ -21,6 +21,7 @@ + <%# Vendored libs above don't change between deploys, so they're left unversioned; app.js's own modules do, and map.js statically imports them by bare path — an import map remaps those paths to a versioned @@ -31,6 +32,7 @@ { "imports": { "/js/auth.js": "/js/auth.js?v=<%= ASSET_VERSION %>", + "/js/contour.js": "/js/contour.js?v=<%= ASSET_VERSION %>", "/js/dem.js": "/js/dem.js?v=<%= ASSET_VERSION %>", "/js/gnss.js": "/js/gnss.js?v=<%= ASSET_VERSION %>", "/js/gpx.js": "/js/gpx.js?v=<%= ASSET_VERSION %>",