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/511mn/layer.js b/layers/dot-cams/ia/layer.js
similarity index 91%
rename from layers/511mn/layer.js
rename to layers/dot-cams/ia/layer.js
index 0f7cfd7..bf985d2 100644
--- a/layers/511mn/layer.js
+++ b/layers/dot-cams/ia/layer.js
@@ -39,9 +39,7 @@ vectorLayer.customPopupCallback = function(feature) {
if (view.category.toLowerCase() == "video") {
const video = document.getElementById('popupVideo');
- const videoID = view.url.split('/').pop();
-
- const videoSrc = `https://video.dot.state.mn.us/public/${videoID}.stream/playlist.m3u8`;
+ const videoSrc = view.sources[0].src;
if (Hls.isSupported()) {
var hls = new Hls();
hls.loadSource(videoSrc);
diff --git a/layers/511mn/pin.svg b/layers/dot-cams/ia/pin.svg
similarity index 100%
rename from layers/511mn/pin.svg
rename to layers/dot-cams/ia/pin.svg
diff --git a/layers/511mn/query.graphql b/layers/dot-cams/ia/query.graphql
similarity index 88%
rename from layers/511mn/query.graphql
rename to layers/dot-cams/ia/query.graphql
index d032daa..8c53b14 100644
--- a/layers/511mn/query.graphql
+++ b/layers/dot-cams/ia/query.graphql
@@ -18,6 +18,11 @@ query MapFeatures($input: MapFeaturesArgs!, $plowType: String) {
uri
... on CameraView {
url
+ sources {
+ type
+ src
+ }
+ title
}
category
}
diff --git a/layers/dot-cams/index.js b/layers/dot-cams/index.js
new file mode 100644
index 0000000..ba1a093
--- /dev/null
+++ b/layers/dot-cams/index.js
@@ -0,0 +1,23 @@
+import mn from './mn/layer.js';
+import wi from './wi/layer.js';
+import ia from './ia/layer.js';
+
+const dot_cams = {
+ name: "State DOT Cameras",
+ layers: [
+ {
+ name: "MNDOT/511MN",
+ layer: mn,
+ },
+ {
+ name: "WisDOT/511WI",
+ layer: wi,
+ },
+ {
+ name: "Iowa DOT/511IA",
+ layer: ia,
+ },
+ ],
+};
+
+export default dot_cams;
diff --git a/layers/511mn/process_data.py b/layers/dot-cams/mn/get_data.py
old mode 100644
new mode 100755
similarity index 78%
rename from layers/511mn/process_data.py
rename to layers/dot-cams/mn/get_data.py
index 4d3bf3e..285171d
--- a/layers/511mn/process_data.py
+++ b/layers/dot-cams/mn/get_data.py
@@ -40,9 +40,17 @@ for c in camera_views:
print(c)
raise Exception(f"Unexpected number of features: {len(c['features'])}")
- if re.match(r"Show [\d]* cameras", c['tooltip']):
+ 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",
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/mn/pin.svg b/layers/dot-cams/mn/pin.svg
new file mode 100644
index 0000000..fb93212
--- /dev/null
+++ b/layers/dot-cams/mn/pin.svg
@@ -0,0 +1,21 @@
+
+
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
new file mode 100644
index 0000000..c21fb45
--- /dev/null
+++ b/layers/dot-cams/wi/get_data.py
@@ -0,0 +1,74 @@
+#!/usr/bin/python3
+
+import requests
+import json
+
+query={
+ "columns": [ # no clue what any of this is, so here it stays
+ {
+ "data": None,
+ "name": "",
+ },
+ {
+ "name": "sortId",
+ "s": True,
+ },
+ {
+ "name": "region",
+ "s": True,
+ },
+ {
+ "name": "county",
+ "s": True,
+ },
+ {
+ "name": "roadway",
+ "s": True,
+ },
+ {
+ "name": "description1",
+ },
+ {
+ "data": 6,
+ "name": "",
+ },
+ ],
+ "start": 0,
+ "length": 100,
+}
+
+cameras = []
+available_cameras = 999_999 # lots
+
+while len(cameras) < available_cameras:
+ res = requests.get("https://511wi.gov/List/GetData/Cameras", {
+ "query": json.dumps(query),
+ "lang": "en",
+ })
+ res.raise_for_status()
+ res = res.json()
+ available_cameras = res['recordsTotal']
+ for c in res['data']:
+ cameras.append({
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [c['longitude'], c['latitude']], # yes, [lon, lat] since it's [x, y]
+ },
+ "properties": {
+ 'address': c['displayName'],
+ 'website': c['videoUrl'],
+ 'originalData': c,
+ },
+ })
+ query['start'] += 100
+
+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/wi/layer.js b/layers/dot-cams/wi/layer.js
new file mode 100644
index 0000000..7a8a6e3
--- /dev/null
+++ b/layers/dot-cams/wi/layer.js
@@ -0,0 +1,46 @@
+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.videoUrl;
+ 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/wi/pin.svg b/layers/dot-cams/wi/pin.svg
new file mode 100644
index 0000000..fb93212
--- /dev/null
+++ b/layers/dot-cams/wi/pin.svg
@@ -0,0 +1,21 @@
+
+
diff --git a/layers/index.js b/layers/index.js
index 95999b4..109133c 100644
--- a/layers/index.js
+++ b/layers/index.js
@@ -14,7 +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';
+import dot_cams from './dot-cams/index.js';
const layerCategories = [
{ // Base maps
@@ -76,12 +76,9 @@ const layerCategories = [
name: "Bikepacking.com Routes",
layer: bikepackingLayer,
},
- {
- name: "511MN Cameras",
- layer: _511mncamerasLayer,
- }
]
},
+ dot_cams,
trips,
chains,
census_bureau,