diff --git a/.gitignore b/.gitignore
index 1056b50..9b035f8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,4 @@ node_modules
dist
*.geojson
*.shp
-layers/dot-cams/castle-rock/data/states.js
+layers/dot-cams/*/data/states.js
diff --git a/layers/dot-cams/README.md b/layers/dot-cams/README.md
index 6fdc998..72c36bf 100644
--- a/layers/dot-cams/README.md
+++ b/layers/dot-cams/README.md
@@ -1,10 +1,124 @@
-https://traveler.modot.org/map/
+# Department of Transportation Cameras by State
+
+Many states are through one of a few providers:
+
+ * Castle Rock: https://www.castlerockits.com/oneweb
+ * IBI Travel IQ: https://www.ibigroup.com/ibi-products/traveliq/
+
+| State | DOT Name[^name] | Online Service Name | Video? | Web provider | Link |
+| ----- | --------------- | ------------------- | ------ | ------------ | ------------------------------- |
+| AK | ALDOT | | | Travel IQ | https://511.alaska.gov/ |
+| AL | PF | | | | |
+| AR | ADOT | | | | |
+| AZ | ARDOT | | | | |
+| CA | Caltrans | | | | |
+| CO | CDOT | | | | |
+| CT | CTDOT | | | | |
+| DE | DelDOT | | | | |
+| FL | FDOT | | | | |
+| GA | GDOT | | | | |
+| HI | HDOT | | | | |
+| IA | ITD | | | | |
+| ID | IDOT | | | | |
+| IL | INDOT | | | | |
+| IN | DOT | | | | |
+| KS | KDOT | | | | |
+| KY | KYTC | | | | |
+| LA | DOTD | | | | |
+| MA | MaineDOT | | | | |
+| MD | MDOT | | | | |
+| ME | MassDOT | | | | |
+| MI | MDOT | | | | |
+| MN | MNDOT | 511MN | | Castle Rock | |
+| MO | MoDOT | | | | https://traveler.modot.org/map/ |
+| MS | MDOT | | | | |
+| MT | MDT | | | | |
+| NC | NDOT | | | | |
+| ND | NDOT | | | | |
+| NE | NHDOT TODO: fix | | | | |
+| NH | NJDOT | | | | |
+| NJ | NMDOT | | | | |
+| NM | NYSDOT | | | | |
+| NV | NCDOT | | | | |
+| NY | NDDOT | | | | |
+| OH | ODOT | | | | |
+| OK | ODOT | | | | |
+| OR | ODOT | | | | |
+| PA | PennDOT | | | | |
+| RI | RIDOT | | | | |
+| SC | SCDOT | | | | |
+| SD | SDDOT | | | | |
+| TN | TDOT | | | | |
+| TX | TxDOT | | | | |
+| UT | UDOT | | | | |
+| VA | VTrans | | | | |
+| VT | VDOT | | | | |
+| WA | WSDOT | | | | |
+| WI | WisDOT | | | | |
+| WV | WVDOT | | | | |
+| WY | WYDOT | | | | |
+
+
+[^name]: From https://en.wikipedia.org/wiki/Department_of_transportation#List_of_U.S._state_and_insular_area_departments_of_transportation
+
+
+
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
+https://www.traffic-cams.com/seatonville/illinois/all
+
+https://www.wyoroad.info/
+https://map.wyoroad.info/wtimap/index.html
+
+https://www.511mt.net
+
+https://wsdot.com/Travel/Real-time/Map/
+
+oregon: https://tripcheck.com/
+
+https://www.nmroads.com/mapIndex.html?
+
+https://drivetexas.org/?r=co / https://its.txdot.gov/its/District/FTW/cameras
+
+oklahoma: https://oktraffic.org/#/map
+
+arkansas: https://www.idrivearkansas.com/
+
+tn: https://smartway.tn.gov/traffic?features=incident,traffic
+
+https://goky.ky.gov/
+https://maps.kytc.ky.gov/trafficcameras/?xmin=-9450477.801928485&xmax=-9383289.654065905&ymin=4696826.35185232&ymax=4742421.039224366
+
+https://www.511sc.org/#zoom=7.392317422778981&lon=-80.72462271068872&lat=33.54446902822535&dmsg&rest&cams&other&cong&wthr&acon&incd&trfc iteris
+
+https://drivenc.gov/#
+https://eapps.ncdot.gov/services/traffic-prod/v1/cameras/
+https://eapps.ncdot.gov/services/traffic-prod/v1/cameras/images?filename=US70_YearganRd.jpg&t=1706599344471
+
+https://www.mdottraffic.com/default.aspx?showMain=true
+
+Michigan is just...down? https://mdotjboss.state.mi.us/MiDrive/map
+
+ohio: https://www.ohgo.com/all-ohio?lt=39.949999999999996&ln=-83.05&z=7&ls=incident,construction
+
+https://511pa.com/
+
+https://www.dot.ri.gov/travel/index.php
+
+https://511nj.org/camera
+
+https://www.511virginia.org/
+
+https://wv511.org/
+
+https://chart.maryland.gov/TrafficCameras/GetTrafficCameras
+
+http://goakamai.org/cameras
+
# CA
kern maybe same as WI?
@@ -13,3 +127,15 @@ 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
+
+
+No video, only photos ??
+ "ak": "https://511.alaska.gov/",
+ "az": "https://az511.com/",
+ "ct": "https://ctroads.org/",
+ "new england": "https://newengland511.org/",
+ "id": "https://511.idaho.gov/",
+ "ut": "https://www.udottraffic.utah.gov/",
+
+Some kind of signature needed:
+ "fl": "https://fl511.com/",
diff --git a/layers/dot-cams/al/get_data.py b/layers/dot-cams/al/get_data.py
index 8c732dc..7549c77 100755
--- a/layers/dot-cams/al/get_data.py
+++ b/layers/dot-cams/al/get_data.py
@@ -7,18 +7,60 @@ cameras = []
res = requests.get("https://api.algotraffic.com/v3.0/Cameras")
res.raise_for_status()
+# {
+# "id": 1164,
+# "location": {
+# "latitude": 30.56705,
+# "longitude": -88.19211,
+# "city": "Theodore",
+# "county": "Mobile",
+# "displayRouteDesignator": "I-10",
+# "routeDesignator": "I-10",
+# "routeDesignatorType": "Interstate",
+# "displayCrossStreet": "Theodore Dawes Rd",
+# "crossStreet": "Theodore Dawes Rd",
+# "crossStreetType": "Arterial",
+# "direction": "East",
+# "linearReference": 14.0
+# },
+# "responsibleRegion": "Southwest",
+# "hlsUrl": "https://cdn3.wowza.com/5/aTZuSEJVaHcxakdx/mobile-fastly/mob-cam-c090.stream/playlist.m3u8",
+# "imageUrl": "https://api.algotraffic.com/v3/Cameras/1164/snapshot.jpg",
+# "accessLevel": "Public"
+# }
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,
- },
- })
+ try:
+ if c['accessLevel'] != 'Public':
+ print("warn: access level is not public; ignoring:", c)
+ continue
+ if not c['hlsUrl'].startswith('https://'):
+ raise Exception("invalid hlsUrl")
+ if not c['imageUrl'].startswith('https://'):
+ raise Exception("invalid imageUrl")
+ cameras.append({
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [c['location']['longitude'], c['location']['latitude']], # yes, [lon, lat] since it's [x, y]
+ },
+ "properties": {
+ "name": c['location']['displayRouteDesignator'] + '/' + c['location']['displayCrossStreet'],
+ "views": [
+ {
+ 'hasVideo': True,
+ 'src': c['hlsUrl'],
+ },
+ # {
+ # 'hasVideo': False,
+ # 'src': c['imageUrl'],
+ # }
+ ]
+ },
+ })
+ except Exception as e:
+ print(c)
+ raise e
geojson = {
"type": "FeatureCollection",
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
index b3ec383..4067bd7 100755
--- a/layers/dot-cams/castle-rock/get_data.py
+++ b/layers/dot-cams/castle-rock/get_data.py
@@ -4,13 +4,21 @@ import requests
import json
import re
-with open('states.json') as f:
- states = json.loads(f.read())
+states = {
+ "Minnesota": "https://511mn.org/",
+ "Colorado": "https://maps.cotrip.org/",
+ "Iowa": "https://511ia.org/",
+ "Indiana": "https://511in.org/",
+ "Kansas": "https://www.kandrive.gov/",
+ "Massachusetts": "https://mass511.com/",
+ "Nebraska": "https://new.511.nebraska.gov/"
+}
with open("query.graphql") as f:
QUERY = f.read()
for state, baseURL in states.items():
+ print(f"{state}: ", end="", flush=True)
PAYLOAD = [
{
"query": QUERY,
@@ -38,6 +46,8 @@ for state, baseURL in states.items():
cameras = []
viewCount = 0
+ photoCount = 0
+ videoCount = 0
for c in camera_views:
if len(c['features']) != 1:
@@ -47,22 +57,36 @@ for state, baseURL in states.items():
if re.match(r"Show .* cameras", c['tooltip']):
raise Exception(f"Not zoomed in enough! Finding aggregate cameras: {c}")
+ if len(c['views']) == 0:
+ raise Exception("Camera has no views")
+
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'])})")
+ if view['category'] != c['views'][0]['category']:
+ print(f"warn: Differing types detected: {c['views']}")
+ if view['category'] == 'VIDEO':
+ if state == "Nebraska":
+ print(c)
+ videoCount += 1
+ if len(view['sources']) != 1:
+ raise Exception(f"Unexpected number of sources ({len(view['sources'])})")
+ else:
+ photoCount += 1
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,
+ 'name': c['tooltip'],
+ 'views': [
+ {
+ 'hasVideo': v['category'] == 'VIDEO',
+ 'src': v['sources'][0]['src'] if v['category'] == 'VIDEO' else v['url'],
+ } for v in c['views']
+ ],
+ # 'originalData': c,
},
})
@@ -74,8 +98,8 @@ for state, baseURL in states.items():
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}")
+ print(f"{len(cameras)} locations found")
+ print(f"{state}: {photoCount} photo + {videoCount} video cameras")
# hack hack hack
#
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/pin.svg b/layers/dot-cams/castle-rock/pin.svg
deleted file mode 100644
index fb93212..0000000
--- a/layers/dot-cams/castle-rock/pin.svg
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
diff --git a/layers/dot-cams/castle-rock/states.json b/layers/dot-cams/castle-rock/states.json
deleted file mode 100644
index 45f9d93..0000000
--- a/layers/dot-cams/castle-rock/states.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "mn": "https://511mn.org/",
- "co": "https://maps.cotrip.org/",
- "ia": "https://511ia.org/",
- "in_": "https://511in.org/",
- "ks": "https://www.kandrive.gov/",
- "ma": "https://mass511.com/",
- "ne": "https://new.511.nebraska.gov/"
-}
diff --git a/layers/dot-cams/index.js b/layers/dot-cams/index.js
index 736eac2..b047a88 100644
--- a/layers/dot-cams/index.js
+++ b/layers/dot-cams/index.js
@@ -1,20 +1,80 @@
-import al from './al/layer.js';
-import wi from './travel-iq/index.js';
+import VectorLayer from 'ol/layer/Vector';
+import {Vector as VectorSource} from 'ol/source.js';
+import GeoJSON from 'ol/format/GeoJSON.js';
-import castlerocklayers from './castle-rock/index.js';
-import travelIqLayers from './travel-iq/index.js';
+import Hls from 'hls.js';
-const dot_cams = {
+import {Style} from 'ol/style.js';
+import Icon from 'ol/style/Icon.js';
+
+import pin from './pin.svg?url'; // TODO: remove `?url`?
+import pinVideo from './pin-video.svg?url'; // TODO: remove `?url`?
+
+import castleRockStates from './castle-rock/data/states.js';
+import travelIqStates from './travel-iq/data/states.js';
+import al from './al/data.geojson?url';
+
+import dot_names from './layer_names.js';
+
+const allStates = {
+ ...castleRockStates,
+ ...travelIqStates,
+ 'Alabama': al,
+}
+console.log(allStates, castleRockStates, travelIqStates);
+let dot_cams = {
name: "State DOT Cameras",
- layers: [
- ...castlerocklayers,
- ...travelIqLayers,
- {
- name: "Alabama: ALDOT/ALGO",
- layer: al,
- },
- ],
details: `Enable All`,
+ layers: [],
};
+for (let [state, url] of Object.entries(allStates)) {
+ const vectorLayer = new VectorLayer({
+ source: new VectorSource({
+ url: url,
+ format: new GeoJSON,
+ }),
+ style: function(feature, resolution){
+ return new Style({
+ image: new Icon({
+ anchor: [0.5, 1],
+ src: feature.values_.views[0].hasVideo ? pinVideo : pin,
+ }),
+ });
+ },
+ });
+
+ vectorLayer.customPopup = function(feature) {
+ const view = feature.values_.views[0];
+ if (view.hasVideo) {
+ return `
${feature.values_.name}
`; + } else { + return `