diff --git a/layers/dot-cams/README.md b/layers/dot-cams/README.md deleted file mode 100644 index 6fdc998..0000000 --- a/layers/dot-cams/README.md +++ /dev/null @@ -1,15 +0,0 @@ -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/al/get_data.py b/layers/dot-cams/al/get_data.py deleted file mode 100755 index 8c732dc..0000000 --- a/layers/dot-cams/al/get_data.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/python3 - -import requests -import json - -cameras = [] - -res = requests.get("https://api.algotraffic.com/v3.0/Cameras") -res.raise_for_status() - -for c in res.json(): - cameras.append({ - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [c['location']['longitude'], c['location']['latitude']], # yes, [lon, lat] since it's [x, y] - }, - "properties": { - '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") diff --git a/layers/dot-cams/al/layer.js b/layers/dot-cams/al/layer.js deleted file mode 100644 index cffc4fb..0000000 --- a/layers/dot-cams/al/layer.js +++ /dev/null @@ -1,46 +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) { - return ``; -}; - -vectorLayer.customPopupCallback = function(feature) { - - const video = document.getElementById('popupVideo'); - - const videoSrc = feature.values_.originalData.hlsUrl; - 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/castle-rock/get_data.py b/layers/dot-cams/castle-rock/get_data.py deleted file mode 100755 index 6a4111e..0000000 --- a/layers/dot-cams/castle-rock/get_data.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/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 deleted file mode 100644 index 2f24c52..0000000 --- a/layers/dot-cams/castle-rock/index.js +++ /dev/null @@ -1,75 +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 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/castle-rock/states.json b/layers/dot-cams/castle-rock/states.json deleted file mode 100644 index ce4d810..0000000 --- a/layers/dot-cams/castle-rock/states.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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 new file mode 100755 index 0000000..fad76c5 --- /dev/null +++ b/layers/dot-cams/ia/get_data.py @@ -0,0 +1,74 @@ +#!/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 new file mode 100644 index 0000000..bf985d2 --- /dev/null +++ b/layers/dot-cams/ia/layer.js @@ -0,0 +1,55 @@ +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/al/pin.svg b/layers/dot-cams/ia/pin.svg similarity index 100% rename from layers/dot-cams/al/pin.svg rename to layers/dot-cams/ia/pin.svg diff --git a/layers/dot-cams/castle-rock/query.graphql b/layers/dot-cams/ia/query.graphql similarity index 100% rename from layers/dot-cams/castle-rock/query.graphql rename to layers/dot-cams/ia/query.graphql diff --git a/layers/dot-cams/index.js b/layers/dot-cams/index.js index 71f88ad..ba1a093 100644 --- a/layers/dot-cams/index.js +++ b/layers/dot-cams/index.js @@ -1,17 +1,23 @@ +import mn from './mn/layer.js'; import wi from './wi/layer.js'; - -import castlerocklayers from './castle-rock/index.js'; +import ia from './ia/layer.js'; const dot_cams = { name: "State DOT Cameras", layers: [ - ...castlerocklayers, + { + name: "MNDOT/511MN", + layer: mn, + }, { name: "WisDOT/511WI", layer: wi, }, + { + name: "Iowa DOT/511IA", + layer: ia, + }, ], - details: `Enable All`, }; export default dot_cams; diff --git a/layers/dot-cams/mn/get_data.py b/layers/dot-cams/mn/get_data.py new file mode 100755 index 0000000..285171d --- /dev/null +++ b/layers/dot-cams/mn/get_data.py @@ -0,0 +1,74 @@ +#!/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 new file mode 100644 index 0000000..bf985d2 --- /dev/null +++ b/layers/dot-cams/mn/layer.js @@ -0,0 +1,55 @@ +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/castle-rock/pin.svg b/layers/dot-cams/mn/pin.svg similarity index 100% rename from layers/dot-cams/castle-rock/pin.svg rename to layers/dot-cams/mn/pin.svg diff --git a/layers/dot-cams/mn/query.graphql b/layers/dot-cams/mn/query.graphql new file mode 100644 index 0000000..8c53b14 --- /dev/null +++ b/layers/dot-cams/mn/query.graphql @@ -0,0 +1,45 @@ +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 + } + } +} diff --git a/layers/dot-cams/wi/get_data.py b/layers/dot-cams/wi/get_data.py old mode 100755 new mode 100644