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 ``; + } + }; + + vectorLayer.customPopupCallback = function(feature) { + const view = feature.values_.views[0]; + if (view.hasVideo) { + const video = document.getElementById('popupVideo'); + + if (Hls.isSupported()) { + var hls = new Hls(); + hls.loadSource(view.src); + hls.attachMedia(video); + } + // iDevice support, untested (only works in Safari; required for iPhones) + else if (video.canPlayType('application/vnd.apple.mpegurl')) { + video.src = view.src; + } + } + } + dot_cams.layers.push({ + name: dot_names[state] ?? state, + layer: vectorLayer, + }); +} + +dot_cams.layers.sort((a, b) => a.name > b.name ? 1 : -1); // Names are always unique + export default dot_cams; diff --git a/layers/dot-cams/layer_names.js b/layers/dot-cams/layer_names.js new file mode 100644 index 0000000..2bf0f7a --- /dev/null +++ b/layers/dot-cams/layer_names.js @@ -0,0 +1,9 @@ +export default { + 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", +}; diff --git a/layers/dot-cams/pin-video.svg b/layers/dot-cams/pin-video.svg new file mode 100644 index 0000000..0e0daa8 --- /dev/null +++ b/layers/dot-cams/pin-video.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/layers/dot-cams/al/pin.svg b/layers/dot-cams/pin.svg similarity index 100% rename from layers/dot-cams/al/pin.svg rename to layers/dot-cams/pin.svg diff --git a/layers/dot-cams/travel-iq/get_data.py b/layers/dot-cams/travel-iq/get_data.py index bf66f2b..29f49e1 100755 --- a/layers/dot-cams/travel-iq/get_data.py +++ b/layers/dot-cams/travel-iq/get_data.py @@ -3,8 +3,13 @@ import requests import json -with open('states.json') as f: - states = json.loads(f.read()) +states = { + "Georgia": "https://511ga.org/", + "Louisiana": "https://www.511la.org/", + "Nevada": "https://www.nvroads.com/", + "NewYork": "https://www.511ny.org/", + "Wisconsin": "https://511wi.gov/" +} for state, baseURL in states.items(): query={ @@ -60,9 +65,11 @@ for state, baseURL in states.items(): "coordinates": [c['longitude'], c['latitude']], # yes, [lon, lat] since it's [x, y] }, "properties": { - 'address': c['displayName'], - 'website': c['videoUrl'], - 'originalData': c, + 'name': c['displayName'], + 'views': [{ + 'hasVideo': c['videoUrl'], + 'src': c['videoUrl'][0] if isinstance(c['videoUrl'], list) else c['videoUrl'], # LA returns multiple (identical?) streams + }], }, }) query['start'] += 100 diff --git a/layers/dot-cams/travel-iq/index.js b/layers/dot-cams/travel-iq/index.js deleted file mode 100644 index 5fd807b..0000000 --- a/layers/dot-cams/travel-iq/index.js +++ /dev/null @@ -1,60 +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 = { - wi: "WisDOT/511WI", -}; - -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) { - 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; - } - } - vectorLayers.push({ - name: dot_names[state] ?? state, - layer: vectorLayer, - }); -} - -export default vectorLayers; diff --git a/layers/dot-cams/travel-iq/pin.svg b/layers/dot-cams/travel-iq/pin.svg deleted file mode 100644 index fb93212..0000000 --- a/layers/dot-cams/travel-iq/pin.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - diff --git a/layers/dot-cams/travel-iq/states.json b/layers/dot-cams/travel-iq/states.json deleted file mode 100644 index 154736e..0000000 --- a/layers/dot-cams/travel-iq/states.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "wi": "https://511wi.gov/" -}