diff --git a/Customizations/AllKnownLayouts.ts b/Customizations/AllKnownLayouts.ts index 35612fb..a660137 100644 --- a/Customizations/AllKnownLayouts.ts +++ b/Customizations/AllKnownLayouts.ts @@ -13,6 +13,7 @@ import * as nature from "../assets/themes/nature/nature.json" import * as maps from "../assets/themes/maps/maps.json" import * as shops from "../assets/themes/shops/shops.json" import * as bike_monitoring_stations from "../assets/themes/bike_monitoring_station/bike_monitoring_stations.json" +import * as fritures from "../assets/themes/fritures/fritures.json" import {PersonalLayout} from "../Logic/PersonalLayout"; import {StreetWidth} from "./StreetWidth/StreetWidth"; @@ -66,6 +67,7 @@ export class AllKnownLayouts { FromJSON.LayoutFromJSON(nature), FromJSON.LayoutFromJSON(cyclestreets), FromJSON.LayoutFromJSON(maps), + FromJSON.LayoutFromJSON(fritures), AllKnownLayouts.GenerateBuurtNatuur(), AllKnownLayouts.GenerateBikeMonitoringStations(), diff --git a/InitUiElements.ts b/InitUiElements.ts index 729b509..9baf444 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -15,7 +15,7 @@ import {DropDown} from "./UI/Input/DropDown"; import {LayerSelection} from "./UI/LayerSelection"; import {Preset} from "./Customizations/LayerDefinition"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; -import {LayerUpdater} from "./Logic/LayerUpdater"; +import {UpdateFromOverpass} from "./Logic/UpdateFromOverpass"; import {UIEventSource} from "./Logic/UIEventSource"; import {QueryParameters} from "./Logic/Web/QueryParameters"; import {PersonalLayout} from "./Logic/PersonalLayout"; @@ -451,7 +451,7 @@ export class InitUiElements { ) ); State.state.bm = bm; - State.state.layerUpdater = new LayerUpdater(State.state); + State.state.layerUpdater = new UpdateFromOverpass(State.state); State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state).availableEditorLayers; const queryParam = QueryParameters.GetQueryParameter("background", State.state.layoutToUse.data.defaultBackground); diff --git a/Logic/FilteredLayer.ts b/Logic/FilteredLayer.ts index b01701f..15d3bd8 100644 --- a/Logic/FilteredLayer.ts +++ b/Logic/FilteredLayer.ts @@ -110,31 +110,6 @@ export class FilteredLayer { const tags = TagUtils.proprtiesToKV(feature.properties); if (this.filters.matches(tags)) { - const centerPoint = GeoOperations.centerpoint(feature); - const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature); - feature.properties["_surface"] = "" + sqMeters; - feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000)/10; - - const lat = centerPoint.geometry.coordinates[1]; - const lon = centerPoint.geometry.coordinates[0] - feature.properties["_lon"] = "" + lat; // We expect a string here for lat/lon - feature.properties["_lat"] = "" + lon; - // But the codegrid SHOULD be a number! - CodeGrid.getCode(lat, lon, (error, code) => { - if (error === null) { - feature.properties["_country"] = code; - } else { - console.warn("Could not determine country for", feature.properties.id, error); - } - }) - - if (feature.geometry.type !== "Point") { - if (this._wayHandling === LayerDefinition.WAYHANDLING_CENTER_AND_WAY) { - selfFeatures.push(centerPoint); - } else if (this._wayHandling === LayerDefinition.WAYHANDLING_CENTER_ONLY) { - feature = centerPoint; - } - } selfFeatures.push(feature); } else { leftoverFeatures.push(feature); diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts new file mode 100644 index 0000000..a61fc72 --- /dev/null +++ b/Logic/MetaTagging.ts @@ -0,0 +1,124 @@ +import {GeoOperations} from "./GeoOperations"; +import CodeGrid from "./Web/CodeGrid"; +import State from "../State"; +import opening_hours from "opening_hours"; + + +class SimpleMetaTagger { + private _f: (feature: any) => void; + public readonly keys: string[]; + public readonly doc: string; + + + constructor(keys: string[], doc: string, f: ((feature: any) => void)) { + this.keys = keys; + this.doc = doc; + this._f = f; + for (const key of keys) { + if (!key.startsWith('_')) { + throw `Incorrect metakey ${key}: it should start with underscore (_)` + } + } + } + + addMetaTags(features: any[]) { + for (const feature of features) { + this._f(feature); + } + } + + +} + + +/** + * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ... + * + * All metatags start with an underscore + */ +export default class MetaTagging { + + + public static metatags = [ + new SimpleMetaTagger(["_lat", "_lon"], "The latitude and longitude of the point (or centerpoint in the case of a way/area)", + (feature => { + const centerPoint = GeoOperations.centerpoint(feature); + const lat = centerPoint.geometry.coordinates[1]; + const lon = centerPoint.geometry.coordinates[0]; + feature.properties["_lat"] = "" + lat; + feature.properties["_lon"] = "" + lon; + }) + ), + new SimpleMetaTagger( + ["_surface", "_surface:ha"], "The surface area of the feature, in square meters and in hectare. Not set on points and ways", + (feature => { + const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature); + feature.properties["_surface"] = "" + sqMeters; + feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10; + + }) + ), + + + new SimpleMetaTagger( + ["_country"], "The country code of the point", + (feature => { + const centerPoint = GeoOperations.centerpoint(feature); + const lat = centerPoint.geometry.coordinates[1]; + const lon = centerPoint.geometry.coordinates[0] + // But the codegrid SHOULD be a number! + CodeGrid.getCode(lat, lon, (error, code) => { + if (error === null) { + feature.properties["_country"] = code; + State.state.allElements.addOrGetElement(feature).ping(); + } else { + console.warn("Could not determine country for", feature.properties.id, error); + } + }); + }) + ), + new SimpleMetaTagger( + ["_isOpen", "_isOpen:description"], "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no", + (feature => { + const tagsSource = State.state.allElements.addOrGetElement(feature); + tagsSource.addCallback(tags => { + + if (tags["opening_hours"] !== undefined && tags["_country"] !== undefined) { + + const oh = new opening_hours(tags["opening_hours"], { + lat: tags._lat, + lon: tags._lon, + address: { + country_code: tags._country + } + }, {tag_key: "opening_hours"}); + + const updateTags = () => { + tags["_isOpen"] = oh.getState() ? "yes" : "no"; + const comment = oh.getComment(); + if (comment) { + tags["_isOpen:description"] = comment; + } + const nextChange = oh.getNextChange() as Date; + window.setTimeout( + updateTags, + (nextChange.getTime() - (new Date()).getTime()) + ) + } + updateTags(); + } + + }) + + }) + ) + ]; + + static addMetatags(features: any[]) { + + for (const metatag of MetaTagging.metatags) { + metatag.addMetaTags(features); + } + + } +} diff --git a/Logic/LayerUpdater.ts b/Logic/UpdateFromOverpass.ts similarity index 98% rename from Logic/LayerUpdater.ts rename to Logic/UpdateFromOverpass.ts index ae50b0c..7f37051 100644 --- a/Logic/LayerUpdater.ts +++ b/Logic/UpdateFromOverpass.ts @@ -5,8 +5,9 @@ import {Bounds} from "./Bounds"; import {Overpass} from "./Osm/Overpass"; import State from "../State"; import {LayerDefinition} from "../Customizations/LayerDefinition"; +import MetaTagging from "./MetaTagging"; -export class LayerUpdater { +export class UpdateFromOverpass { public readonly sufficentlyZoomed: UIEventSource; public readonly runningQuery: UIEventSource = new UIEventSource(false); @@ -100,6 +101,8 @@ export class LayerUpdater { } } + MetaTagging.addMetatags(geojson.features); + function renderLayers(layers: FilteredLayer[]) { if (layers.length === 0) { self.runningQuery.setData(false); diff --git a/Logic/Web/CodeGrid.ts b/Logic/Web/CodeGrid.ts index bc6d9b3..78bb5f5 100644 --- a/Logic/Web/CodeGrid.ts +++ b/Logic/Web/CodeGrid.ts @@ -8,6 +8,7 @@ export default class CodeGrid { CodeGrid.grid.getCode(lat, lon, handle); } + private static InitGrid(): any { const grid = codegrid.CodeGrid("./tiles/"); diff --git a/State.ts b/State.ts index bbe2f57..8778cc2 100644 --- a/State.ts +++ b/State.ts @@ -8,7 +8,7 @@ import {OsmConnection} from "./Logic/Osm/OsmConnection"; import Locale from "./UI/i18n/Locale"; import Translations from "./UI/i18n/Translations"; import {FilteredLayer} from "./Logic/FilteredLayer"; -import {LayerUpdater} from "./Logic/LayerUpdater"; +import {UpdateFromOverpass} from "./Logic/UpdateFromOverpass"; import {UIEventSource} from "./Logic/UIEventSource"; import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; import {QueryParameters} from "./Logic/Web/QueryParameters"; @@ -66,7 +66,7 @@ export default class State { public favouriteLayers: UIEventSource; - public layerUpdater: LayerUpdater; + public layerUpdater: UpdateFromOverpass; public filteredLayers: UIEventSource = new UIEventSource([]) diff --git a/UI/Base/TabbedComponent.ts b/UI/Base/TabbedComponent.ts index a0c36ef..308d34d 100644 --- a/UI/Base/TabbedComponent.ts +++ b/UI/Base/TabbedComponent.ts @@ -33,7 +33,6 @@ export class TabbedComponent extends UIElement { headerBar = "
" + headerBar + "
" const content = this.content[this._source.data]; - console.log("Rendering tab", this._source.data); return headerBar + "
" + (content?.Render() ?? "") + "
"; } diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index 160a037..29d9b66 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -87,7 +87,7 @@ export class ImageUploadFlow extends UIElement { } const extraInfo = new Combine([ - Translations.t.image.respectPrivacy, + Translations.t.image.respectPrivacy.SetStyle("font-size:small;"), "
", this._licensePicker, "
", diff --git a/UI/Input/OpeningHours/PublicHolidayInput.ts b/UI/Input/OpeningHours/PublicHolidayInput.ts index f89ade8..c17baf9 100644 --- a/UI/Input/OpeningHours/PublicHolidayInput.ts +++ b/UI/Input/OpeningHours/PublicHolidayInput.ts @@ -114,6 +114,12 @@ export default class PublicHolidayInput extends InputElement { mode: "off" } } + + if(str === "PH open"){ + return { + mode: "open" + } + } if (!str.startsWith("PH ")) { return null; diff --git a/UI/MoreScreen.ts b/UI/MoreScreen.ts index 6cab5c0..4f0611e 100644 --- a/UI/MoreScreen.ts +++ b/UI/MoreScreen.ts @@ -17,10 +17,6 @@ export class MoreScreen extends UIElement { super(State.state.locationControl); this.ListenTo(State.state.osmConnection.userDetails); this.ListenTo(State.state.installedThemes); - - State.state.installedThemes.addCallback(themes => { - console.log("INSTALLED THEMES COUNT:", themes.length) - }) } private createLinkButton(layout: Layout, customThemeDefinition: string = undefined) { @@ -72,7 +68,6 @@ export class MoreScreen extends UIElement { InnerRender(): string { - console.log("Inner rendering MORE") const tr = Translations.t.general.morescreen; const els: UIElement[] = [] diff --git a/UI/OhVisualization.ts b/UI/OhVisualization.ts index adeed24..3a012c4 100644 --- a/UI/OhVisualization.ts +++ b/UI/OhVisualization.ts @@ -33,7 +33,7 @@ export default class OpeningHoursVisualization extends UIElement { const values = [[], [], [], [], [], [], []]; - const start = new Date(from); + const start = new Date(from); // We go one day more into the past, in order to force rendering of holidays in the start of the period start.setDate(from.getDate() - 1); diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 261c065..c18445a 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -166,6 +166,21 @@ export default class SpecialVisualizations { const source = LiveQueryHandler.FetchLiveData(url, shorthands.split(";")); return new VariableUiElement(source.map(data => data[neededValue] ?? "Loading...")); } + }, + + { + funcName: "all_tags", + docs: "Prints all key-value pairs of the object - used for debugging", + args:[], + constr: ((tags: UIEventSource) => { + return new VariableUiElement(tags.map(tags => { + const parts = []; + for (const key in tags) { + parts.push(key+"="+tags[key]); + } + return parts.join("
") + })).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;") + }) } ] diff --git a/assets/layers/nature_reserve/nature_reserve.json b/assets/layers/nature_reserve/nature_reserve.json index 444d646..850864f 100644 --- a/assets/layers/nature_reserve/nature_reserve.json +++ b/assets/layers/nature_reserve/nature_reserve.json @@ -327,6 +327,15 @@ "freeform": { "key": "description:0" } + }, + {"#": "Surface are", + "render": { + "en": "Surface area: {_surface:ha}Ha", + "mappings": { + "if": "_surface:ha=0", + "then": "" + } + } } ], "hideUnderlayingFeaturesMinPercentage": 10, diff --git a/assets/themes/fritures/fritures.json b/assets/themes/fritures/fritures.json new file mode 100644 index 0000000..c7b57a9 --- /dev/null +++ b/assets/themes/fritures/fritures.json @@ -0,0 +1,239 @@ +{ + "id": "id", + "title": { + "nl": "Friturenkaart", + "fr": "Carte des fritures" + }, + "description": { + "nl": "Op deze kaart vind je je favoriete frituur!" + }, + "language": [ + "nl", + "fr" + ], + "maintainer": "", + "icon": "https://upload.wikimedia.org/wikipedia/commons/5/55/French_fries_juliane_kr_r.svg", + "version": "0", + "startLat": 0, + "startLon": 0, + "startZoom": 1, + "widenFactor": 0.05, + "socialImage": "", + "layers": [ + { + "id": "fritures", + "name": { + "nl": "Frituren", + "fr": "Fritures" + }, + "minzoom": 8, + "overpassTags": { + "and": [ + "cuisine~.*friture.*" + ] + }, + "title": { + "render": { + "nl": "Frituur", + "fr": "Friture" + }, + "mappings": [ + { + "if": { + "and": [ + "name~*" + ] + }, + "then": { + "nl": " {name}", + "fr": " {name}" + } + } + ] + }, + "description": {}, + "tagRenderings": [ + { + "render": { + "nl": "{phone}", + "fr": "{phone}" + }, + "question": { + "en": "What is the phone number?", + "nl": "Wat is het telefoonnummer van deze frituur?", + "fr": "Quel est le numéro de téléphone de cette friture?" + }, + "freeform": { + "key": "phone", + "type": "phone" + } + }, + { + "render": { + "en": "{website}" + }, + "question": { + "en": "What is the website of this shop?", + "nl": "Wat is de website van deze frituur?", + "fr": "Quel est le site web de cette friture?" + }, + "freeform": { + "key": "website", + "type": "url" + } + }, + { + "render": { + "nl": "

Openingsuren

{opening_hours_table(opening_hours)}", + "fr": "

Horaires

{opening_hours_table(opening_hours)}" + }, + "question": { + "nl": "Wat zijn de openinguren van deze frituur?", + "fr": "Quand est ce-que ce friture ouvert?" + }, + "freeform": { + "key": "opening_hours", + "type": "opening_hours" + } + }, + { + "render": { + "nl": "" + }, + "question": { + "nl": "Heeft deze frituur vegetarische snacks?", + "fr": "Cette friture est-elle équipée de snacks végétariens ?" + }, + "mappings": [ + { + "if": { + "and": [ + "diet:vegetarian=yes" + ] + }, + "then": { + "nl": "Er zijn vegetarische snacks aanwezig", + "fr": "Des collations végétariennes sont disponibles" + } + }, + { + "if": { + "and": [ + "diet:vegetarian=limited" + ] + }, + "then": { + "nl": "Slechts enkele vegetarische snacks", + "fr": "Quelques snacks végétariens seulement" + } + }, + { + "if": { + "and": [ + "diet:vegetarian=no" + ] + }, + "then": { + "nl": "Geen vegetarische snacks beschikbaar", + "fr": "Pas d'en-cas végétariens disponibles" + } + } + ] + }, + { + "render": { + "nl": "" + }, + "question": { + "nl": "Heeft deze frituur veganistische snacks?", + "fr": "Cette friture est-elle équipée de snacks végétaliens ?" + }, + "mappings": [ + { + "if": { + "and": [ + "diet:vegan=yes" + ] + }, + "then": { + "nl": "Er zijn veganistische snacks aanwezig", + "fr": "Des collations végétaliens sont disponibles" + } + }, + { + "if": { + "and": [ + "diet:vegan=limited" + ] + }, + "then": { + "nl": "Slechts enkele veganistische snacks", + "fr": "Quelques snacks végétaliens seulement" + } + }, + { + "if": { + "and": [ + "diet:vegetarian=no" + ] + }, + "then": { + "nl": "Geen veganistische snacks beschikbaar", + "fr": "Pas d'en-cas végétaliens disponibles" + } + } + ] + }, + { + "question": { + "nl": "Bakt deze frituur in dierlijk vetof plantaardig olie?", + "fr": "Cette friteuse fonctionne-t-elle avec de la graisse animale ou végétale ?" + }, + "mappings": [ + { + "if": { + "and": [ + "friture:oil=vegetable" + ] + }, + "then": { + "nl": "Plantaardige olie", + "fr": "Huile végétale" + } + }, + { + "if": { + "and": [ + "friture:oil=animal" + ] + }, + "then": { + "nl": "Dierlijk vet", + "fr": "Graisse animale" + } + } + ], + "freeform": { + "key": "" + } + } + ], + "icon": { + "render": "https://upload.wikimedia.org/wikipedia/commons/5/55/French_fries_juliane_kr_r.svg" + }, + "width": { + "render": "8" + }, + "iconSize": { + "render": "40,40,center" + }, + "color": { + "render": "#00f" + }, + "presets": [], + "wayHandling": 1 + } + ], + "roamingRenderings": [], + "shortDescription": {} +} \ No newline at end of file