From 0d6412f824c13136a72be4b8de66b7950ca5a15b Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 12 Oct 2020 01:25:27 +0200 Subject: [PATCH] Add loading of live data --- Customizations/JSON/FromJSON.ts | 2 + Logic/FilteredLayer.ts | 2 +- Logic/LayerUpdater.ts | 8 ++ Logic/Osm/Overpass.ts | 11 +-- Logic/Web/CodeGrid.ts | 9 +- Logic/Web/LiveQueryHandler.ts | 53 ++++++++++ README.md | 3 + UI/SpecialVisualizations.ts | 26 ++++- UI/i18n/Translation.ts | 32 ++++--- UI/i18n/Translations.ts | 7 +- .../bike_monitoring_station.json | 58 +++++++++++ .../monitoring_station.svg | 96 +++++++++++++++++++ assets/themes/cyclofix/cyclofix.json | 4 +- test.ts | 15 +-- 14 files changed, 291 insertions(+), 35 deletions(-) create mode 100644 Logic/Web/LiveQueryHandler.ts create mode 100644 assets/layers/bike_monitoring_station/bike_monitoring_station.json create mode 100644 assets/layers/bike_monitoring_station/monitoring_station.svg diff --git a/Customizations/JSON/FromJSON.ts b/Customizations/JSON/FromJSON.ts index 2094c1f..6a8462b 100644 --- a/Customizations/JSON/FromJSON.ts +++ b/Customizations/JSON/FromJSON.ts @@ -17,6 +17,7 @@ import * as bike_repair_station from "../../assets/layers/bike_repair_station/bi import * as birdhides from "../../assets/layers/bird_hide/birdhides.json" import * as nature_reserve from "../../assets/layers/nature_reserve/nature_reserve.json" import * as bike_cafes from "../../assets/layers/bike_cafe/bike_cafes.json" +import * as bike_monitoring_station from "../../assets/layers/bike_monitoring_station/bike_monitoring_station.json" import * as cycling_themed_objects from "../../assets/layers/cycling_themed_object/cycling_themed_objects.json" import * as bike_shops from "../../assets/layers/bike_shop/bike_shop.json" import * as maps from "../../assets/layers/maps/maps.json" @@ -43,6 +44,7 @@ export class FromJSON { FromJSON.Layer(viewpoint), FromJSON.Layer(bike_parking), FromJSON.Layer(bike_repair_station), + FromJSON.Layer(bike_monitoring_station), FromJSON.Layer(birdhides), FromJSON.Layer(nature_reserve), FromJSON.Layer(bike_cafes), diff --git a/Logic/FilteredLayer.ts b/Logic/FilteredLayer.ts index e654c93..8a116f4 100644 --- a/Logic/FilteredLayer.ts +++ b/Logic/FilteredLayer.ts @@ -117,7 +117,7 @@ export class FilteredLayer { feature.properties["_lon"] = "" + lat; // We expect a string here for lat/lon feature.properties["_lat"] = "" + lon; // But the codegrid SHOULD be a number! - CodeGrid.grid.getCode(lat, lon, (error, code) => { + CodeGrid.getCode(lat, lon, (error, code) => { if (error === null) { feature.properties["_country"] = code; } else { diff --git a/Logic/LayerUpdater.ts b/Logic/LayerUpdater.ts index a7983fc..28da176 100644 --- a/Logic/LayerUpdater.ts +++ b/Logic/LayerUpdater.ts @@ -91,6 +91,14 @@ export class LayerUpdater { self.retries.setData(0); + let newIds = 1; + for (const feature of geojson.features) { + if(feature.properties.id === undefined){ + feature.properties.id = "ext/"+newIds; + newIds++; + } + } + function renderLayers(layers: FilteredLayer[]) { if (layers.length === 0) { self.runningQuery.setData(false); diff --git a/Logic/Osm/Overpass.ts b/Logic/Osm/Overpass.ts index 59d4ad3..9bed761 100644 --- a/Logic/Osm/Overpass.ts +++ b/Logic/Osm/Overpass.ts @@ -1,6 +1,6 @@ import {Bounds} from "../Bounds"; import {TagsFilter} from "../Tags"; -import $ from "jquery" +import * as $ from "jquery" import * as OsmToGeoJson from "osmtogeojson"; /** @@ -27,14 +27,13 @@ export class Overpass { } queryGeoJson(bounds: Bounds, continuation: ((any) => void), onFail: ((reason) => void)): void { - - let query = this.buildQuery( "[bbox:" + bounds.south + "," + bounds.west + "," + bounds.north + "," + bounds.east + "]") - if(Overpass.testUrl !== null){ + let query = this.buildQuery("[bbox:" + bounds.south + "," + bounds.west + "," + bounds.north + "," + bounds.east + "]") + + if (Overpass.testUrl !== null) { console.log("Using testing URL") query = Overpass.testUrl; } - $.getJSON(query, function (json, status) { if (status !== "success") { @@ -42,7 +41,7 @@ export class Overpass { onFail(status); } - if(json.elements === [] && json.remarks.indexOf("runtime error") > 0){ + if (json.elements === [] && json.remarks.indexOf("runtime error") > 0) { console.log("Timeout or other runtime error"); onFail("Runtime error (timeout)") return; diff --git a/Logic/Web/CodeGrid.ts b/Logic/Web/CodeGrid.ts index ade7cc8..bc6d9b3 100644 --- a/Logic/Web/CodeGrid.ts +++ b/Logic/Web/CodeGrid.ts @@ -1,7 +1,12 @@ import codegrid from "codegrid-js"; export default class CodeGrid { - public static readonly grid = CodeGrid.InitGrid(); + private static readonly grid = CodeGrid.InitGrid(); + + + public static getCode(lat: any, lon: any, handle: (error, code) => void) { + CodeGrid.grid.getCode(lat, lon, handle); + } private static InitGrid(): any { const grid = codegrid.CodeGrid("./tiles/"); @@ -15,4 +20,6 @@ export default class CodeGrid { }); return grid; } + + } \ No newline at end of file diff --git a/Logic/Web/LiveQueryHandler.ts b/Logic/Web/LiveQueryHandler.ts new file mode 100644 index 0000000..b2cca20 --- /dev/null +++ b/Logic/Web/LiveQueryHandler.ts @@ -0,0 +1,53 @@ +/** + * Fetches data from random data sources + */ +import {UIEventSource} from "../UIEventSource"; +import * as $ from "jquery" + +export default class LiveQueryHandler { + + + private static cache = {} // url --> UIEventSource + private static neededShorthands = {} // url -> (shorthand:paths)[] + + public static FetchLiveData(url: string, shorthands: string[]): UIEventSource string */> { + + const shorthandsSet: string[] = LiveQueryHandler.neededShorthands[url] ?? [] + + for (const shorthand of shorthands) { + if (shorthandsSet.indexOf(shorthand) < 0) { + shorthandsSet.push(shorthand); + } + } + LiveQueryHandler.neededShorthands[url] = shorthandsSet; + + + if (LiveQueryHandler[url] === undefined) { + const source = new UIEventSource({}); + LiveQueryHandler[url] = source; + + console.log("Fetching live data from a third-party (unknown) API:",url) + $.getJSON(url, function (data) { + for (const shorthandDescription of shorthandsSet) { + + const descr = shorthandDescription.trim().split(":"); + const shorthand = descr[0]; + const path = descr[1]; + const parts = path.split("."); + let trail = data; + for (const part of parts) { + if (trail !== undefined) { + trail = trail[part]; + } + } + source.data[shorthand] = trail; + } + source.ping(); + + }) + + } + return LiveQueryHandler[url]; + } + +} \ No newline at end of file diff --git a/README.md b/README.md index 5376dba..07d67a6 100644 --- a/README.md +++ b/README.md @@ -184,3 +184,6 @@ Park icon via http://www.onlinewebfonts.com/icon/425974, CC BY 3.0 (@sterankofra Forest icon via https://www.onlinewebfonts.com/icon/498112, CC BY Statistics icon via https://www.onlinewebfonts.com/icon/197818 + +Chronometer (on monitoring_station.svg): ANTU chronometer +https://commons.wikimedia.org/w/index.php?title=Antu_chronometer&action=edit&redlink=1 \ No newline at end of file diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 3d48223..c7ccae9 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -1,6 +1,8 @@ import {UIElement} from "./UIElement"; import OpeningHoursVisualization from "./OhVisualization"; import {UIEventSource} from "../Logic/UIEventSource"; +import {VariableUiElement} from "./Base/VariableUIElement"; +import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"; export default class SpecialVisualizations { @@ -8,13 +10,13 @@ export default class SpecialVisualizations { funcName: string, constr: ((tagSource: UIEventSource, argument: string[]) => UIElement), docs: string, - args: {name: string, defaultValue: string, doc: string}[] + args: { name: string, defaultValue?: string, doc: string }[] }[] = [{ funcName: "opening_hours_table", docs: "Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.", - args:[{name:"key", defaultValue: "opening_hours", doc: "The tag from which the table is constructed"}], + args: [{name: "key", defaultValue: "opening_hours", doc: "The tag from which the table is constructed"}], constr: (tagSource: UIEventSource, args) => { let keyname = args[0]; if (keyname === undefined || keyname === "") { @@ -24,6 +26,26 @@ export default class SpecialVisualizations { } }, + { + funcName: "live", + docs: "Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}", + args: [{ + name: "Url", doc: "The URL to load" + }, { + name: "Shorthands", + doc: "A list of shorthands, of the format 'shorthandname:path.path.path'. Seperated by ;" + }, { + name: "path", doc: "The path (or shorthand) that should be returned" + }], + constr: (tagSource: UIEventSource, args) => { + const url = args[0]; + const shorthands = args[1]; + const neededValue = args[2]; + const source = LiveQueryHandler.FetchLiveData(url, shorthands.split(";")); + return new VariableUiElement(source.map(data => data[neededValue] ?? "Loading...")); + } + } + ] } \ No newline at end of file diff --git a/UI/i18n/Translation.ts b/UI/i18n/Translation.ts index 1ee96c1..2aab695 100644 --- a/UI/i18n/Translation.ts +++ b/UI/i18n/Translation.ts @@ -47,23 +47,27 @@ export default class Translation extends UIElement { for (const knownSpecial of knownSpecials) { + do { + const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*)\\)}(.*)`); + if (matched === null) { + break; + } + const partBefore = matched[1]; + const argument = matched[2]; + const partAfter = matched[3]; - const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*)\\)}(.*)`); - if (matched === null) { - continue; - } - const partBefore = matched[1]; - const argument = matched[2]; - const partAfter = matched[3]; + try { + + const element = knownSpecial.constr(argument).Render(); + template = partBefore + element + partAfter; + } catch (e) { + console.error(e); + template = partBefore + partAfter; + } + + } while (true); - try { - const element = knownSpecial.constr(argument).Render(); - template = partBefore + element + partAfter; - } catch (e) { - console.error(e); - template = partBefore + partAfter; - } } newTranslations[lang] = template; } diff --git a/UI/i18n/Translations.ts b/UI/i18n/Translations.ts index d589ce0..b8afffc 100644 --- a/UI/i18n/Translations.ts +++ b/UI/i18n/Translations.ts @@ -103,10 +103,10 @@ export default class Translations { }), respectPrivacy: new T({ - en: "Please respect privacy. Do not photograph people nor license plates.
Respect copyright. Only upload images you made yourself. Do not upload Google Streetview Images - these will be removed.", + en: "Do not photograph people nor license plates. Do not upload Google Maps, Google Streetview or other copyrighted sources.", ca: "Respecta la privacitat. No fotografiïs gent o matrícules", es: "Respeta la privacidad. No fotografíes gente o matrículas", - nl: "Respecteer privacy. Fotografeer geen mensen of nummerplaten.
Repecteer auteursrechten. Voeg enkel foto's toe die je zelf maakte. Screenshots van andere services (zoals Google Streetview) worden verwijderd", + nl: "Fotografeer geen mensen of nummerplaten. Voeg geen Google Maps, Google Streetview of foto's met auteursrechten toe.", fr: "Merci de respecter la vie privée. Ne publiez pas les plaques d\'immatriculation", gl: "Respecta a privacidade. Non fotografes xente ou matrículas", de: "Bitte respektieren Sie die Privatsphäre. Fotografieren Sie weder Personen noch Nummernschilder" @@ -969,6 +969,9 @@ export default class Translations { } public static WT(s: string | Translation): Translation { + if(s === undefined){ + return undefined; + } if (typeof (s) === "string") { return new Translation({en: s}); } diff --git a/assets/layers/bike_monitoring_station/bike_monitoring_station.json b/assets/layers/bike_monitoring_station/bike_monitoring_station.json new file mode 100644 index 0000000..063a9e8 --- /dev/null +++ b/assets/layers/bike_monitoring_station/bike_monitoring_station.json @@ -0,0 +1,58 @@ +{ + "id": "bike_monitoring_station", + "name": { + "en": "Monitoring stations" + }, + "minzoom": 12, + "overpassTags": { + "and": [ + "man_made=monitoring_station", + "monitoring:bicycle=yes" + ] + }, + "title": { + "render": { + "nl": "Fietstelstation", + "en": "Bicycle counting station" + }, + "mappings": [ + { + "if": "name~*", + "then": { + "en": "Bicycle counting station {name}", + "nl": "Fietstelstation {name}" + } + }, + { + "if": "ref~*", + "then": { + "en": "Bicycle counting station {ref}", + "nl": "Fietstelstation {ref}" + } + } + ] + }, + "description": {}, + "tagRenderings": [ + { + "render": "{live({url},{url:format},hour)} cyclists last hour
{live({url},{url:format},day)} cyclists today
{live({url},{url:format},year)} cyclists this year
", + "condition": { + "and": ["url~*","url:format~*"] + } + } + ], + "hideUnderlayingFeaturesMinPercentage": 0, + "icon": { + "render": "./assets/layers/bike_monitoring_station/monitoring_station.svg" + }, + "width": { + "render": "8" + }, + "iconSize": { + "render": "40,40,center" + }, + "color": { + "render": "#00f" + }, + "presets": [] +} \ No newline at end of file diff --git a/assets/layers/bike_monitoring_station/monitoring_station.svg b/assets/layers/bike_monitoring_station/monitoring_station.svg new file mode 100644 index 0000000..de7a19f --- /dev/null +++ b/assets/layers/bike_monitoring_station/monitoring_station.svg @@ -0,0 +1,96 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/themes/cyclofix/cyclofix.json b/assets/themes/cyclofix/cyclofix.json index b0b018c..c050433 100644 --- a/assets/themes/cyclofix/cyclofix.json +++ b/assets/themes/cyclofix/cyclofix.json @@ -20,11 +20,11 @@ "icon": "./assets/themes/cyclofix/logo.svg", "version": "0", "startLat": 50.8465573, - + "defaultBackgroundId": "CartoDB.Voyager", "startLon": 4.3516970, "startZoom": 16, "widenFactor": 0.05, "socialImage": "./assets/themes/cyclofix/logo.svg", - "layers": ["bike_repair_station", "bike_cafes", "bike_shops", "drinking_water", "bike_parking","bike_themed_object"], + "layers": ["bike_repair_station", "bike_cafes", "bike_shops", "drinking_water", "bike_parking","bike_themed_object","bike_monitoring_station"], "roamingRenderings": [] } \ No newline at end of file diff --git a/test.ts b/test.ts index 47cf57d..4a50264 100644 --- a/test.ts +++ b/test.ts @@ -1,15 +1,16 @@ //* -import {UIEventSource} from "./Logic/UIEventSource"; -import OpeningHoursVisualization from "./UI/OhVisualization"; +import LiveQueryHandler from "./Logic/Web/LiveQueryHandler"; +import {VariableUiElement} from "./UI/Base/VariableUIElement"; -const oh = "Tu-Fr 09:00-17:00 'as usual'; mo off 'yyy'; su off 'xxx'" -const tags = new UIEventSource({opening_hours:oh}); -new OpeningHoursVisualization(tags, 'opening_hours').AttachTo('maindiv') +const source = LiveQueryHandler.FetchLiveData("https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CJM90", + "hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt".split(";")) +source.addCallback((data) => {console.log(data)}) +new VariableUiElement(source.map(data => { + return ["Data is:", data.hour, "last hour;", data.day, "last day; ", data.year, "last year;"].join(" ") +})).AttachTo('maindiv') - -window.setTimeout(() => {tags.data._country = "be"; }, 5000) /*/