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/ia/pin.svg b/layers/dot-cams/ia/pin.svg new file mode 100644 index 0000000..fb93212 --- /dev/null +++ b/layers/dot-cams/ia/pin.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/layers/dot-cams/ia/query.graphql b/layers/dot-cams/ia/query.graphql new file mode 100644 index 0000000..8c53b14 --- /dev/null +++ b/layers/dot-cams/ia/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/index.js b/layers/dot-cams/index.js index f4dd5a9..ba1a093 100644 --- a/layers/dot-cams/index.js +++ b/layers/dot-cams/index.js @@ -1,5 +1,6 @@ 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", @@ -12,6 +13,10 @@ const dot_cams = { name: "WisDOT/511WI", layer: wi, }, + { + name: "Iowa DOT/511IA", + layer: ia, + }, ], };