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
- }
- }
-}