From 05fd104e563c8f9bedc72aeb967ad4772ce9c73b Mon Sep 17 00:00:00 2001 From: Chandler Swift Date: Mon, 29 Jan 2024 01:39:46 -0600 Subject: [PATCH 1/3] Add basic popup for item information --- index.html | 4 ++++ main.js | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 44 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/index.html b/index.html index 00f505d..e812452 100644 --- a/index.html +++ b/index.html @@ -15,6 +15,10 @@
+ diff --git a/main.js b/main.js index d333838..0ac7688 100644 --- a/main.js +++ b/main.js @@ -2,15 +2,36 @@ import './style.css'; import {Map, View} from 'ol'; import {fromLonLat, get} from 'ol/proj.js'; import {defaults as defaultControls} from 'ol/control.js'; +import Overlay from 'ol/Overlay.js'; import ToggleMenuControl from './ui/controls.js'; import layerCategories from './layers/index.js'; +// from https://openlayers.org/en/latest/examples/popup.html +const container = document.getElementById('popup'); +const content = document.getElementById('popup-content'); +const closer = document.getElementById('popup-closer'); +const popupOverlay = new Overlay({ + element: container, + autoPan: { + animation: { + duration: 250, + }, + }, +}); + +closer.onclick = function () { + popupOverlay.setPosition(undefined); + closer.blur(); + return false; +}; + const map = new Map({ controls: defaultControls().extend([new ToggleMenuControl()]), target: 'map', layers: [], + overlays: [popupOverlay], view: new View({ center: fromLonLat([-93.24151, 44.80376]), zoom: 10, @@ -56,3 +77,37 @@ for (let category of layerCategories) { } } } + +function objectToTable(o) { + let table = ``; + // TODO: hack hack hack + for (let [key, value] of Object.entries(o)) { + console.log(`typeof ${value} = ${typeof value}`); + if (typeof value === "object") { + value = objectToTable(value); + } + if (typeof value === "string" && value.startsWith('https://')) { + value = `${value}` + } + table += ``; + } + table += `
${key}${value}
`; + return table; +} + +// from https://openlayers.org/en/latest/examples/icon.html +map.on('click', function (evt) { + const feature = map.forEachFeatureAtPixel(evt.pixel, function (feature) { + return feature; + }); + if (!feature) { + return; + } + + // https://stackoverflow.com/a/208106 + const {geometry: _, ...featureData} = feature.values_; + + content.innerHTML = objectToTable(featureData); + popupOverlay.setPosition(evt.coordinate); + console.log(feature); +}); diff --git a/style.css b/style.css index b1beee7..3973b8b 100644 --- a/style.css +++ b/style.css @@ -82,3 +82,47 @@ aside summary { right: 0.5em; } } + +/* POPUP (from https://openlayers.org/en/latest/examples/popup.html) */ +.ol-popup { + position: absolute; + background-color: white; + box-shadow: 0 1px 4px rgba(0,0,0,0.2); + padding: 15px; + border-radius: 10px; + border: 1px solid #cccccc; + bottom: 12px; + left: -50px; + min-width: 280px; +} +.ol-popup:after, .ol-popup:before { + top: 100%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} +.ol-popup:after { + border-top-color: white; + border-width: 10px; + left: 48px; + margin-left: -10px; +} +.ol-popup:before { + border-top-color: #cccccc; + border-width: 11px; + left: 48px; + margin-left: -11px; +} +.ol-popup-closer { + text-decoration: none; + position: absolute; + top: 2px; + right: 8px; +} +.ol-popup-closer:after { + content: "✖"; +} +/* END POPUP */ From 61ee67875cd6cdaa0f45d0bc9338ff0ea2eb21ad Mon Sep 17 00:00:00 2001 From: Chandler Swift Date: Mon, 29 Jan 2024 01:39:46 -0600 Subject: [PATCH 2/3] Add basic popup for item information --- index.html | 4 ++++ main.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/index.html b/index.html index 00f505d..e812452 100644 --- a/index.html +++ b/index.html @@ -15,6 +15,10 @@
+ diff --git a/main.js b/main.js index d333838..aa623af 100644 --- a/main.js +++ b/main.js @@ -2,15 +2,36 @@ import './style.css'; import {Map, View} from 'ol'; import {fromLonLat, get} from 'ol/proj.js'; import {defaults as defaultControls} from 'ol/control.js'; +import Overlay from 'ol/Overlay.js'; import ToggleMenuControl from './ui/controls.js'; import layerCategories from './layers/index.js'; +// from https://openlayers.org/en/latest/examples/popup.html +const container = document.getElementById('popup'); +const content = document.getElementById('popup-content'); +const closer = document.getElementById('popup-closer'); +const popupOverlay = new Overlay({ + element: container, + autoPan: { + animation: { + duration: 250, + }, + }, +}); + +closer.onclick = function () { + popupOverlay.setPosition(undefined); + closer.blur(); + return false; +}; + const map = new Map({ controls: defaultControls().extend([new ToggleMenuControl()]), target: 'map', layers: [], + overlays: [popupOverlay], view: new View({ center: fromLonLat([-93.24151, 44.80376]), zoom: 10, @@ -56,3 +77,35 @@ for (let category of layerCategories) { } } } + +function objectToTable(o) { + // TODO: hack hack hack + let table = ``; + for (let [key, value] of Object.entries(o)) { + if (typeof value === "object") { + value = objectToTable(value); + } + if (typeof value === "string" && value.startsWith('https://')) { + value = `${value}` + } + table += ``; + } + table += `
${key}${value}
`; + return table; +} + +// from https://openlayers.org/en/latest/examples/icon.html +map.on('click', function (evt) { + const feature = map.forEachFeatureAtPixel(evt.pixel, function (feature) { + return feature; + }); + if (!feature) { + return; + } + + // exclude geometry -- https://stackoverflow.com/a/208106 + const {geometry: _, ...featureData} = feature.values_; + + content.innerHTML = objectToTable(featureData); + popupOverlay.setPosition(evt.coordinate); +}); diff --git a/style.css b/style.css index b1beee7..3973b8b 100644 --- a/style.css +++ b/style.css @@ -82,3 +82,47 @@ aside summary { right: 0.5em; } } + +/* POPUP (from https://openlayers.org/en/latest/examples/popup.html) */ +.ol-popup { + position: absolute; + background-color: white; + box-shadow: 0 1px 4px rgba(0,0,0,0.2); + padding: 15px; + border-radius: 10px; + border: 1px solid #cccccc; + bottom: 12px; + left: -50px; + min-width: 280px; +} +.ol-popup:after, .ol-popup:before { + top: 100%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} +.ol-popup:after { + border-top-color: white; + border-width: 10px; + left: 48px; + margin-left: -10px; +} +.ol-popup:before { + border-top-color: #cccccc; + border-width: 11px; + left: 48px; + margin-left: -11px; +} +.ol-popup-closer { + text-decoration: none; + position: absolute; + top: 2px; + right: 8px; +} +.ol-popup-closer:after { + content: "✖"; +} +/* END POPUP */ From 7707b6448b036eef24406c0b403301ab0352891f Mon Sep 17 00:00:00 2001 From: Chandler Swift Date: Mon, 29 Jan 2024 02:24:53 -0600 Subject: [PATCH 3/3] Add 511MN cameras layer --- layers/511mn/layer.js | 24 +++++++++++++ layers/511mn/pin.svg | 21 ++++++++++++ layers/511mn/process_data.py | 66 ++++++++++++++++++++++++++++++++++++ layers/511mn/query.graphql | 40 ++++++++++++++++++++++ layers/index.js | 6 ++++ 5 files changed, 157 insertions(+) create mode 100644 layers/511mn/layer.js create mode 100644 layers/511mn/pin.svg create mode 100644 layers/511mn/process_data.py create mode 100644 layers/511mn/query.graphql diff --git a/layers/511mn/layer.js b/layers/511mn/layer.js new file mode 100644 index 0000000..af140b4 --- /dev/null +++ b/layers/511mn/layer.js @@ -0,0 +1,24 @@ +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 url from './data.geojson?url'; // TODO: remove `?url`? +import pin from './pin.svg?url'; // TODO: remove `?url`? + +const vectorLayer = new VectorLayer({ + source: new VectorSource({ + url: url, + format: new GeoJSON, + }), + style: new Style({ + image: new Icon({ + anchor: [0.5, 1], + src: pin, + }), + }), +}); + +export default vectorLayer; diff --git a/layers/511mn/pin.svg b/layers/511mn/pin.svg new file mode 100644 index 0000000..fb93212 --- /dev/null +++ b/layers/511mn/pin.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/layers/511mn/process_data.py b/layers/511mn/process_data.py new file mode 100644 index 0000000..4d3bf3e --- /dev/null +++ b/layers/511mn/process_data.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + +import requests +import json +import re + +with open("query.graphql") as f: + QUERY = f.read() + +PAYLOAD = [ + { + "query": QUERY, + "variables": { + "input": { + # Cover the whole state (this is pretty overkill, admittedly) + "north":53.23294, + "south":40.40589, + "east":-78.6823, + "west":-107.24675, + "zoom":9, + "layerSlugs": ["normalCameras"], + "nonClusterableUris": ["dashboard"] + }, + "plowType":"plowCameras", + } + } +] + +res = requests.post('https://511mn.org/api/graphql', json=PAYLOAD) +res.raise_for_status() + +camera_views = res.json()[0]['data']['mapFeaturesQuery']['mapFeatures'] + +cameras = [] + +viewCount = 0 + +for c in camera_views: + if len(c['features']) != 1: + print(c) + raise Exception(f"Unexpected number of features: {len(c['features'])}") + + if re.match(r"Show [\d]* cameras", c['tooltip']): + raise Exception(f"Not zoomed in enough! Finding aggregate cameras: {c}") + + viewCount += len(c['views']) + cameras.append({ + "type": "Feature", + "geometry": c['features'][0]['geometry'], + "properties": { + 'address': c['tooltip'], + 'website': c['views'][0]['url'], + 'originalData': c, + }, + }) + +geojson = { + "type": "FeatureCollection", + "features": cameras, +} + +with open("data.geojson", "w") as f: + f.write(json.dumps(geojson)) + +print(f"{len(cameras)} locations found") +print(f"{viewCount} total views") diff --git a/layers/511mn/query.graphql b/layers/511mn/query.graphql new file mode 100644 index 0000000..d032daa --- /dev/null +++ b/layers/511mn/query.graphql @@ -0,0 +1,40 @@ +query MapFeatures($input: MapFeaturesArgs!, $plowType: String) { + mapFeaturesQuery(input: $input) { + mapFeatures { + bbox + tooltip + uri + features { + id + geometry + properties + } + ... on Event { + priority + } + __typename + ... on Camera { + views(limit: 5) { + uri + ... on CameraView { + url + } + category + } + } + ... on Plow { + views(limit: 5, plowType: $plowType) { + uri + ... on PlowCameraView { + url + } + category + } + } + } + error { + message + type + } + } +} diff --git a/layers/index.js b/layers/index.js index 4ae7f7b..e29b26c 100644 --- a/layers/index.js +++ b/layers/index.js @@ -14,6 +14,7 @@ import cellular from './cellular.js'; import light_pollution from './light_pollution.js'; import state_land from './state-land/index.js'; import trips from './trips/index.js'; +import _511mncamerasLayer from './511mn/layer.js'; const layerCategories = [ { // Base maps @@ -75,6 +76,11 @@ const layerCategories = [ name: "Bikepacking.com Routes", layer: bikepackingLayer, }, + { + name: "511MN Cameras", + layer: _511mncamerasLayer, + enabled: true, + } ] }, trips,