diff --git a/generic-pin.svg b/generic-pin.svg new file mode 100644 index 0000000..88872f5 --- /dev/null +++ b/generic-pin.svg @@ -0,0 +1,13 @@ + + + + + diff --git a/layers/chains/culvers/layer.js b/layers/chains/culvers/layer.js index 75a076c..96b5560 100644 --- a/layers/chains/culvers/layer.js +++ b/layers/chains/culvers/layer.js @@ -35,7 +35,7 @@ vectorLayer.customPopupCallback = async function(feature) { fotd_parent.append(fotd_child, fotd_image); const [long, lat] = toLonLat(feature.getGeometry().getCoordinates()); - const res = await fetch(`https://corsproxy.io/?${encodeURIComponent(`https://www.culvers.com/api/restaurants/getLocations?lat=${lat}&long=${long}&limit=1&t=${Date.now()}`)}`) + const res = await fetch(`https://corsproxy.io/?${encodeURIComponent(`https://www.culvers.com/api/locator/getLocations?lat=${lat}&long=${long}&limit=1&t=${Date.now()}`)}`) const res_data = await res.json(); fotd_child.innerHTML = res_data.data.geofences[0].metadata.flavorOfDayName; fotd_image.src = `https://cdn.culvers.com/menu-item-detail/${res_data.data.geofences[0].metadata.flavorOfDaySlug}`; diff --git a/main.js b/main.js index 510e50d..db3d16f 100644 --- a/main.js +++ b/main.js @@ -11,6 +11,16 @@ import ToggleMenuControl from './ui/controls.js'; import layerCategories from './layers/index.js'; +import VectorLayer from 'ol/layer/Vector'; +import {Vector as VectorSource} from 'ol/source.js'; +import GeoJSON from 'ol/format/GeoJSON.js'; +import {Style} from 'ol/style.js'; +import Icon from 'ol/style/Icon.js'; + +import qs from 'qs'; + +import pin from './generic-pin.svg?url'; + // from https://openlayers.org/en/latest/examples/popup.html const container = document.getElementById('popup'); const content = document.getElementById('popup-content'); @@ -119,8 +129,8 @@ for (let category of layerCategories) { document.querySelector("aside").appendChild(catDiv); } -const urlParams = new URLSearchParams(window.location.search); -const urlLayers = urlParams.getAll('layer'); +const urlParams = qs.parse(window.location.search, { ignoreQueryPrefix: true }); +const urlLayers = urlParams.layer ?? []; for (let category of layerCategories) { for (let layer of category.layers) { @@ -140,6 +150,117 @@ for (let category of layerCategories) { } } +const customLayerDiv = document.createElement("div"); +customLayerDiv.innerHTML = ` +
+ Custom +
+
+
+ + (must be in GeoJSON format) + +
`; +const labelInput = customLayerDiv.querySelector('input[type=text]'); +const sourceInput = customLayerDiv.querySelector('input[type=url]'); +const colorInput = customLayerDiv.querySelector('input[type=color]'); + +customLayerDiv.querySelector("button").addEventListener("click", function(){ + if (!sourceInput.value.toLowerCase().endsWith(".geojson")) { + if (!confirm("Input URL doesn't end in .geojson, so is probably not a valid GeoJSON file. Do you want to continue anyway?")) { + return; + } + } + newCustomLayer(labelInput.value, sourceInput.value, colorInput.value.substring(1)); +}); +document.querySelector("aside").appendChild(customLayerDiv); + +// borrowed from https://github.com/ChartsCSS/charts.css/blob/main/src/general/_variables.scss#L7; randomly ordered +let colors = [ + [170, 90, 240], + [90, 165, 255], + [100, 210, 80], + [255, 180, 50], + [240, 50, 50], + [130, 50, 20], + [255, 220, 90], + [180, 180, 180], + [170, 150, 110], + [110, 110, 110], +]; + +let used_colors = []; + +// HACK +// TIL that [1,1] != [1,1], since arrays are objects, and objects are equal iff +// they are the same object. +Array.prototype.equals = function(other) { + return this.length == other.length && this.every((e, i) => e === other[i]); +}; + +function newCustomLayer(name, sourceURL, colorString) { + let color; + if (colorString) { + color = [ + parseInt(colorString.substr(0,2),16), + parseInt(colorString.substr(2,2),16), + parseInt(colorString.substr(4,2),16), + ]; + if (color.length != 3 || color.some(Number.isNaN)) { + alert("Invalid color provided; using random color instead."); + color = null; + } + } + if (!color) { + let available_colors = colors.filter(c => !used_colors.some(i => i.equals(c))); + if (available_colors) { + color = available_colors[0]; + } else { + color = [0, 0, 0]; // If we run out of colors, fall back to black + } + } + used_colors.push(color); + const li = document.createElement("li"); + const layer = new VectorLayer({ + source: new VectorSource({ + // In case people put in layers that don't serve proper CORS headers, we + // wrap them in this proxy so they Just Work. + url: `https://corsproxy.io/?${encodeURIComponent(sourceURL)}`, + format: new GeoJSON, + }), + style: new Style({ + image: new Icon({ + anchor: [0.5, 1], + src: pin, + color: color, + }), + }), + }); + li.innerHTML = ` + + `; + li.querySelector("input").addEventListener("change", function(e){ + if (e.target.checked) { + map.getLayers().push(layer); + } else { + map.getLayers().remove(layer); + } + }); + map.getLayers().push(layer); + customLayerDiv.querySelector("ul").appendChild(li); + + // Update input to make sure it's not suggesting an already-used color + let available_colors = colors.filter(c => !used_colors.some(i => i.equals(c))); + available_colors.push([0, 0, 0]); // Ensure we always have at least one color available + document.querySelector('input[type=color]').value = '#' + available_colors[0].map(x => x.toString(16).padStart(2, '0')).join(''); +} + +if (urlParams.customLayer) { + for (let customLayer of urlParams.customLayer) { + newCustomLayer(customLayer['name'], customLayer['url'], customLayer['color']) + } +} + let location_set = false; if (urlLayers.length > 0) { diff --git a/package-lock.json b/package-lock.json index c263146..415a5d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "hls.js": "^1.5.2", "ol": "latest", "ol-contextmenu": "^5.3.0", - "proj4": "2.9.0" + "proj4": "2.9.0", + "qs": "^6.13.0" }, "devDependencies": { "vite": "^4.3.9" @@ -379,6 +380,25 @@ "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-3.0.4.tgz", "integrity": "sha512-knSt9cCW8jj1ZSFcFeBZaX++OucmfPxxHiRwTahZfJlnQsek7O0bazTJHWD2RVj9LEoejUYF2de3/stf+QXcXw==" }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/color-name": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.0.tgz", @@ -409,11 +429,49 @@ "resolved": "https://registry.npmjs.org/color-space/-/color-space-2.0.1.tgz", "integrity": "sha512-nKqUYlo0vZATVOFHY810BSYjmCARrG7e5R3UE3CQlyjJTvv5kSSmPG1kzm/oDyyqjehM+lW1RnEt9It9GNa5JA==" }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/earcut": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.0.tgz", "integrity": "sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==" }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", @@ -465,6 +523,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/geotiff": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz", @@ -483,6 +550,85 @@ "node": ">=10.19" } }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hls.js": { "version": "1.5.15", "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.15.tgz", @@ -516,6 +662,18 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ol": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ol/-/ol-10.1.0.tgz", @@ -618,6 +776,21 @@ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/quick-lru": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz", @@ -666,6 +839,41 @@ "fsevents": "~2.3.2" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", diff --git a/package.json b/package.json index e82e108..9ec220e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "hls.js": "^1.5.2", "ol": "latest", "ol-contextmenu": "^5.3.0", - "proj4": "2.9.0" + "proj4": "2.9.0", + "qs": "^6.13.0" } } diff --git a/style.css b/style.css index 67c0768..0d56c1e 100644 --- a/style.css +++ b/style.css @@ -60,8 +60,7 @@ aside .close { display: none; } -aside details { - margin-bottom: 2em; +aside details summary { cursor: pointer; } @@ -71,6 +70,15 @@ aside summary { font-weight: bold; } +.color-badge { + display: inline-block; + height: 1em; + width: 2em; + vertical-align: middle; + border: 1px #444 solid; + border-radius: 4px; +} + @media (max-width: 400px) { .nav-open #map { left: 100%;