diff --git a/.gitignore b/.gitignore index 2fbb441..1056b50 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules dist *.geojson *.shp +layers/dot-cams/castle-rock/data/states.js diff --git a/layers/dot-cams/README.md b/layers/dot-cams/README.md new file mode 100644 index 0000000..6fdc998 --- /dev/null +++ b/layers/dot-cams/README.md @@ -0,0 +1,15 @@ +https://traveler.modot.org/map/ + +https://map.wyoroad.info/wtimap/index.html + +https://www.sd511.org/#&zoom=6.396744103345674&lon=-96.21250629888505&lat=44.0011361134118&states + +https://www.travelmidwest.com/lmiga/cameraReport.jsp?location=GATEWAY.IL + +# CA +kern maybe same as WI? +https://kern511.org/cctv?start=0&length=10&order%5Bi%5D=1&order%5Bdir%5D=asc + +https://dot.ca.gov/programs/traffic-operations/traveler-information/511 + +"rr": "https://riverregion511.org" castlerock diff --git a/layers/dot-cams/castle-rock/get_data.py b/layers/dot-cams/castle-rock/get_data.py new file mode 100755 index 0000000..6a4111e --- /dev/null +++ b/layers/dot-cams/castle-rock/get_data.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +import requests +import json +import re + +with open('states.json') as f: + states = json.loads(f.read()) + +with open("query.graphql") as f: + QUERY = f.read() + +data = {} + +for state, baseURL in states.items(): + PAYLOAD = [ + { + "query": QUERY, + "variables": { + "input": { + # Cover the whole state (this is pretty overkill, admittedly) + "north":90, + "south":0, + "east":0, + "west":-179, + "zoom":12, + "layerSlugs": ["normalCameras"], + "nonClusterableUris": ["dashboard"], + }, + "plowType":"plowCameras", + }, + }, + ] + + res = requests.post(f'{baseURL}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 .* cameras", c['tooltip']): + raise Exception(f"Not zoomed in enough! Finding aggregate cameras: {c}") + + for view in c['views']: + if len(view['sources']) != 1 if view['category'] == 'VIDEO' else 0: + print(view) + raise Exception(f"Unexpected number of sources ({len(view['sources'])})") + for source in view['sources'] or []: + if source['type'] != 'application/x-mpegURL': + raise Exception(f"Unexpected type {source['type']}") + + 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(f"data/{state}.geojson", "w") as f: + f.write(json.dumps(geojson)) + + print(f"{len(cameras)} locations found for {state}") + print(f"{viewCount} total views for {state}") + +# hack hack hack +# +# If I write this to one big file, I can't take advantage of any lazy loading +# for performance reasons, so I'm constrained to having a bunch of files. I +# can't programmatically import those, since es6 imports don't allow for that. +# So, codegen it is (and fairly gross codegen at that!). +with open('data/states.js', 'w') as f: + for state in states: + f.write(f"import {state} from './{state}.geojson?url';\n") + f.write('\nexport default {\n') + for state in states: + f.write(f" {state}: {state},\n") + f.write("};\n") diff --git a/layers/dot-cams/castle-rock/index.js b/layers/dot-cams/castle-rock/index.js new file mode 100644 index 0000000..2f24c52 --- /dev/null +++ b/layers/dot-cams/castle-rock/index.js @@ -0,0 +1,75 @@ +import VectorLayer from 'ol/layer/Vector'; +import {Vector as VectorSource} from 'ol/source.js'; +import GeoJSON from 'ol/format/GeoJSON.js'; + +import Hls from 'hls.js'; + +import {Style} from 'ol/style.js'; +import Icon from 'ol/style/Icon.js'; + +import states from './data/states.js'; + +import pin from './pin.svg?url'; // TODO: remove `?url`? + +// https://en.wikipedia.org/wiki/Department_of_transportation#List_of_U.S._state_and_insular_area_departments_of_transportation +const dot_names = { + mn: "Minnesota: MNDOT/511MN", + ia: "Iowa: Iowa DOT/511IA", + wi: "Wisconsin: WisDOT/511WI", + co: "Colorado: CDOT/COtrip", + ks: "Kansas: KDOT/KanDrive", + ne: "Nebraska: NDOT/Nebraska 511", + ma: "Massachusetts: MassDOT/Mass511", +} + +let vectorLayers = [] + +for (let [state, url] of Object.entries(states)) { + const vectorLayer = new VectorLayer({ + source: new VectorSource({ + url: url, + format: new GeoJSON, + }), + style: new Style({ + image: new Icon({ + anchor: [0.5, 1], + src: pin, + }), + }), + }); + + vectorLayer.customPopup = function(feature) { + const view = feature.values_.originalData.views[0]; + if (view.category.toLowerCase() == "video") { + return ``; + } else if (view.category.toLowerCase() == "image") { + return ``; + } else { + throw new Exception(`unknown category ${view.category}`); + } + }; + + vectorLayer.customPopupCallback = function(feature) { + const view = feature.values_.originalData.views[0]; + if (view.category.toLowerCase() == "video") { + const video = document.getElementById('popupVideo'); + + const videoSrc = view.sources[0].src; + if (Hls.isSupported()) { + var hls = new Hls(); + hls.loadSource(videoSrc); + hls.attachMedia(video); + } + // iDevice support, untested (only works in Safari; required for iPhones) + else if (video.canPlayType('application/vnd.apple.mpegurl')) { + video.src = videoSrc; + } + } + } + vectorLayers.push({ + name: dot_names[state] ?? state, + layer: vectorLayer, + }); +} + +export default vectorLayers; diff --git a/layers/dot-cams/ia/pin.svg b/layers/dot-cams/castle-rock/pin.svg similarity index 100% rename from layers/dot-cams/ia/pin.svg rename to layers/dot-cams/castle-rock/pin.svg diff --git a/layers/dot-cams/ia/query.graphql b/layers/dot-cams/castle-rock/query.graphql similarity index 100% rename from layers/dot-cams/ia/query.graphql rename to layers/dot-cams/castle-rock/query.graphql diff --git a/layers/dot-cams/castle-rock/states.json b/layers/dot-cams/castle-rock/states.json new file mode 100644 index 0000000..ce4d810 --- /dev/null +++ b/layers/dot-cams/castle-rock/states.json @@ -0,0 +1,8 @@ +{ + "mn": "https://511mn.org/", + "co": "https://maps.cotrip.org/", + "ia": "https://511ia.org/", + "ks": "https://www.kandrive.gov/", + "ma": "https://mass511.com/", + "ne": "https://new.511.nebraska.gov/" +} diff --git a/layers/dot-cams/ia/get_data.py b/layers/dot-cams/ia/get_data.py deleted file mode 100755 index fad76c5..0000000 --- a/layers/dot-cams/ia/get_data.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/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":45.2, - "south":38.2, - "east":-82.9, - "west":-98.3, - "zoom":11, - "layerSlugs": ["normalCameras"], - "nonClusterableUris": ["dashboard"] - }, - "plowType":"plowCameras", - } - } -] - -res = requests.post('https://511ia.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 .* cameras", c['tooltip']): - raise Exception(f"Not zoomed in enough! Finding aggregate cameras: {c}") - - for view in c['views']: - if len(view['sources']) != 1 if view['category'] == 'VIDEO' else 0: - print(view) - raise Exception(f"Unexpected number of sources ({len(view['sources'])})") - for source in view['sources'] or []: - if source['type'] != 'application/x-mpegURL': - raise Exception(f"Unexpected type {source['type']}") - - 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/dot-cams/ia/layer.js b/layers/dot-cams/ia/layer.js deleted file mode 100644 index bf985d2..0000000 --- a/layers/dot-cams/ia/layer.js +++ /dev/null @@ -1,55 +0,0 @@ -import VectorLayer from 'ol/layer/Vector'; -import {Vector as VectorSource} from 'ol/source.js'; -import GeoJSON from 'ol/format/GeoJSON.js'; - -import Hls from 'hls.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, - }), - }), -}); - -vectorLayer.customPopup = function(feature) { - const view = feature.values_.originalData.views[0]; - if (view.category.toLowerCase() == "video") { - return ``; - } else if (view.category.toLowerCase() == "image") { - return ``; - } else { - throw new Exception(`unknown category ${view.category}`); - } -}; - -vectorLayer.customPopupCallback = function(feature) { - const view = feature.values_.originalData.views[0]; - if (view.category.toLowerCase() == "video") { - const video = document.getElementById('popupVideo'); - - const videoSrc = view.sources[0].src; - if (Hls.isSupported()) { - var hls = new Hls(); - hls.loadSource(videoSrc); - hls.attachMedia(video); - } - // iDevice support, untested (only works in Safari; required for iPhones) - else if (video.canPlayType('application/vnd.apple.mpegurl')) { - video.src = videoSrc; - } - } -} - -export default vectorLayer; diff --git a/layers/dot-cams/index.js b/layers/dot-cams/index.js index 646d69c..71f88ad 100644 --- a/layers/dot-cams/index.js +++ b/layers/dot-cams/index.js @@ -1,22 +1,15 @@ -import mn from './mn/layer.js'; import wi from './wi/layer.js'; -import ia from './ia/layer.js'; + +import castlerocklayers from './castle-rock/index.js'; const dot_cams = { name: "State DOT Cameras", layers: [ - { - name: "MNDOT/511MN", - layer: mn, - }, + ...castlerocklayers, { name: "WisDOT/511WI", layer: wi, }, - { - name: "Iowa DOT/511IA", - layer: ia, - }, ], details: `Enable All`, }; diff --git a/layers/dot-cams/mn/get_data.py b/layers/dot-cams/mn/get_data.py deleted file mode 100755 index 285171d..0000000 --- a/layers/dot-cams/mn/get_data.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/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 .* cameras", c['tooltip']): - raise Exception(f"Not zoomed in enough! Finding aggregate cameras: {c}") - - for view in c['views']: - if len(view['sources']) != 1 if view['category'] == 'VIDEO' else 0: - print(view) - raise Exception(f"Unexpected number of sources ({len(view['sources'])})") - for source in view['sources'] or []: - if source['type'] != 'application/x-mpegURL': - raise Exception(f"Unexpected type {source['type']}") - - 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/dot-cams/mn/layer.js b/layers/dot-cams/mn/layer.js deleted file mode 100644 index bf985d2..0000000 --- a/layers/dot-cams/mn/layer.js +++ /dev/null @@ -1,55 +0,0 @@ -import VectorLayer from 'ol/layer/Vector'; -import {Vector as VectorSource} from 'ol/source.js'; -import GeoJSON from 'ol/format/GeoJSON.js'; - -import Hls from 'hls.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, - }), - }), -}); - -vectorLayer.customPopup = function(feature) { - const view = feature.values_.originalData.views[0]; - if (view.category.toLowerCase() == "video") { - return ``; - } else if (view.category.toLowerCase() == "image") { - return ``; - } else { - throw new Exception(`unknown category ${view.category}`); - } -}; - -vectorLayer.customPopupCallback = function(feature) { - const view = feature.values_.originalData.views[0]; - if (view.category.toLowerCase() == "video") { - const video = document.getElementById('popupVideo'); - - const videoSrc = view.sources[0].src; - if (Hls.isSupported()) { - var hls = new Hls(); - hls.loadSource(videoSrc); - hls.attachMedia(video); - } - // iDevice support, untested (only works in Safari; required for iPhones) - else if (video.canPlayType('application/vnd.apple.mpegurl')) { - video.src = videoSrc; - } - } -} - -export default vectorLayer; diff --git a/layers/dot-cams/mn/pin.svg b/layers/dot-cams/mn/pin.svg deleted file mode 100644 index fb93212..0000000 --- a/layers/dot-cams/mn/pin.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - diff --git a/layers/dot-cams/mn/query.graphql b/layers/dot-cams/mn/query.graphql deleted file mode 100644 index 8c53b14..0000000 --- a/layers/dot-cams/mn/query.graphql +++ /dev/null @@ -1,45 +0,0 @@ -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 - sources { - type - src - } - title - } - category - } - } - ... on Plow { - views(limit: 5, plowType: $plowType) { - uri - ... on PlowCameraView { - url - } - category - } - } - } - error { - message - type - } - } -}