From b2662b823c54f1c5a806de07d190363023fa4816 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sat, 3 Apr 2021 14:33:19 +0200 Subject: [PATCH 01/97] Add small documentation improvements --- Customizations/JSON/TagRenderingConfigJson.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Customizations/JSON/TagRenderingConfigJson.ts b/Customizations/JSON/TagRenderingConfigJson.ts index 8d61296..de091b3 100644 --- a/Customizations/JSON/TagRenderingConfigJson.ts +++ b/Customizations/JSON/TagRenderingConfigJson.ts @@ -1,11 +1,16 @@ import {AndOrTagConfigJson} from "./TagConfigJson"; +/** + * A TagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet. + * If the desired tags are missing and a question is defined, a question will be shown instead. + * + / export interface TagRenderingConfigJson { /** * Renders this value. Note that "{key}"-parts are substituted by the corresponding values of the element. * If neither 'textFieldQuestion' nor 'mappings' are defined, this text is simply shown as default value. * - * Note that this is a HTML-interpreted value, so you can add links as e.g. {website} + * Note that this is a HTML-interpreted value, so you can add links as e.g. '{website}' or include images such as `This is of type A
` */ render?: string | any, @@ -139,4 +144,4 @@ export interface TagRenderingConfigJson { * However, it will _only_ be shown if it matches the overpass-tags of the layer it was originally defined in. */ roaming?: boolean -} \ No newline at end of file +} From 82a097fd147ea1b082fc6794718da04183828be1 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 3 Apr 2021 21:33:11 +0200 Subject: [PATCH 02/97] Improve docs --- Docs/Making_Your_Own_Theme.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Docs/Making_Your_Own_Theme.md b/Docs/Making_Your_Own_Theme.md index adfb9d5..9071073 100644 --- a/Docs/Making_Your_Own_Theme.md +++ b/Docs/Making_Your_Own_Theme.md @@ -84,6 +84,17 @@ Every field is documented in the source code itself - you can find them here: There are few tags available that are calculated for convenience - e.g. the country an object is located at. [An overview of all these metatags is available here](Docs/CalculatedTags.md) + Some hints +------------ + +### Everything is HTML + +All the texts are actually *HTML*-snippets, so you can use `` to add bold, or `` to add images to mappings or tagrenderings. + +Some remarks: + +- links are disabled when answering a question (e.g. a link in a mapping) as it should trigger the answer - not trigger to open the link. +- If you include images, e.g. to clarify a type, make sure these are _icons_ or _diagrams_ - not actual pictures! If users see a picture, they think it is a picture of _that actual object_, not a type to clarify the type. An icon is however perceived as something more abstract. Some pitfalls --------------- From b108f99aabeb0e4a34842a0141860d6a898d09ad Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 4 Apr 2021 03:22:56 +0200 Subject: [PATCH 03/97] Add support for smaller theme encodings --- InitUiElements.ts | 14 +++++- UI/CustomGenerator/CustomGeneratorPanel.ts | 5 ++- Utils.ts | 38 ++++++++++++++++ package-lock.json | 10 +++++ package.json | 2 + scripts/genKeys.sh | 12 ++++++ test/Utils.spec.ts | 50 ++++++++++++++++++++++ 7 files changed, 128 insertions(+), 3 deletions(-) create mode 100755 scripts/genKeys.sh create mode 100644 test/Utils.spec.ts diff --git a/InitUiElements.ts b/InitUiElements.ts index c1e4b67..80f9d1c 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -36,6 +36,8 @@ import Translations from "./UI/i18n/Translations"; import MapControlButton from "./UI/MapControlButton"; import Combine from "./UI/Base/Combine"; import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler"; +import LZString from "lz-string"; +import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson"; export class InitUiElements { @@ -209,7 +211,17 @@ export class InitUiElements { hashFromLocalStorage.setData(hash); dedicatedHashFromLocalStorage.setData(hash); } - const layoutToUse = new LayoutConfig(JSON.parse(atob(hash)), false); + + let json = {} + try{ + json = JSON.parse(atob(hash)); + } catch (e) { + // We try to decode with lz-string + json = JSON.parse( Utils.UnMinify(LZString.decompressFromBase64(hash))) + } + + // @ts-ignore + const layoutToUse = new LayoutConfig(json, false); userLayoutParam.setData(layoutToUse.id); return layoutToUse; } catch (e) { diff --git a/UI/CustomGenerator/CustomGeneratorPanel.ts b/UI/CustomGenerator/CustomGeneratorPanel.ts index 99414cb..e1a1592 100644 --- a/UI/CustomGenerator/CustomGeneratorPanel.ts +++ b/UI/CustomGenerator/CustomGeneratorPanel.ts @@ -17,7 +17,8 @@ import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource"; import HelpText from "./HelpText"; import Svg from "../../Svg"; import Constants from "../../Models/Constants"; - +import LZString from "lz-string"; +import {Utils} from "../../Utils"; export default class CustomGeneratorPanel extends UIElement { private mainPanel: UIElement; @@ -40,7 +41,7 @@ export default class CustomGeneratorPanel extends UIElement { private InitMainPanel(layout: LayoutConfigJson, userDetails: UserDetails, connection: OsmConnection) { const es = new UIEventSource(layout); - const encoded = es.map(config => btoa(JSON.stringify(config))); + const encoded = es.map(config => LZString.compressToBase64(Utils.MinifyJSON(JSON.stringify(config, null, 0)))); encoded.addCallback(encoded => LocalStorageSource.Get("last-custom-theme")) const liveUrl = encoded.map(encoded => `./index.html?userlayout=${es.data.id}#${encoded}`) const testUrl = encoded.map(encoded => `./index.html?test=true&userlayout=${es.data.id}#${encoded}`) diff --git a/Utils.ts b/Utils.ts index 941255b..4ac8d4c 100644 --- a/Utils.ts +++ b/Utils.ts @@ -10,6 +10,8 @@ export class Utils { public static runningFromConsole = false; public static readonly assets_path = "./assets/svg/"; + private static knownKeys = ["addExtraTags", "and", "calculatedTags", "changesetmessage", "clustering", "color", "condition", "customCss", "dashArray", "defaultBackgroundId", "description", "descriptionTail", "doNotDownload", "enableAddNewPoints", "enableBackgroundLayerSelection", "enableGeolocation", "enableLayers", "enableMoreQuests", "enableSearch", "enableShareScreen", "enableUserBadge", "freeform", "hideFromOverview", "hideInAnswer", "icon", "iconOverlays", "iconSize", "id", "if", "ifnot", "isShown", "key", "language", "layers", "lockLocation", "maintainer", "mappings", "maxzoom", "maxZoom", "minNeededElements", "minzoom", "multiAnswer", "name", "or", "osmTags", "passAllFeatures", "presets", "question", "render", "roaming", "roamingRenderings", "rotation", "shortDescription", "socialImage", "source", "startLat", "startLon", "startZoom", "tagRenderings", "tags", "then", "title", "titleIcons", "type", "version", "wayHandling", "widenFactor", "width"] + private static extraKeys = ["nl", "en", "fr", "de", "pt", "es", "name", "phone", "email", "amenity", "leisure", "highway", "building", "yes", "no", "true", "false"] static EncodeXmlValue(str) { return str.replace(/&/g, '&') @@ -202,6 +204,42 @@ export class Utils { return {x: Utils.lon2tile(lon, z), y: Utils.lat2tile(lat, z), z: z} } + public static MinifyJSON(stringified: string): string { + stringified = stringified.replace(/\|/g, "||"); + + const keys = Utils.knownKeys.concat(Utils.extraKeys); + for (let i = 0; i < keys.length; i++) { + const knownKey = keys[i]; + let code = i; + if (i >= 124) { + code += 1; // Character 127 is our 'escape' character | + } + let replacement = "|" + String.fromCharCode(code) + stringified = stringified.replace(new RegExp(`\"${knownKey}\":`, "g"), replacement); + } + + return stringified; + } + + public static UnMinify(minified: string): string { + + const parts = minified.split("|"); + let result = parts.shift(); + const keys = Utils.knownKeys.concat(Utils.extraKeys); + + for (const part of parts) { + if (part == "") { + // Empty string => this was a || originally + result += "|" + continue + } + const i = part.charCodeAt(0); + result += "\"" + keys[i] + "\":" + part.substring(1) + } + + return result; + } + private static tile2long(x, z) { return (x / Math.pow(2, z) * 360 - 180); } diff --git a/package-lock.json b/package-lock.json index 435acb0..988d865 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4018,6 +4018,11 @@ "@types/leaflet": "*" } }, + "@types/lz-string": { + "version": "1.3.34", + "resolved": "https://registry.npmjs.org/@types/lz-string/-/lz-string-1.3.34.tgz", + "integrity": "sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow==" + }, "@types/node": { "version": "7.10.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.14.tgz", @@ -7943,6 +7948,11 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" }, + "lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=" + }, "magic-string": { "version": "0.22.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", diff --git a/package.json b/package.json index 4b750ea..1767a59 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@types/leaflet-markercluster": "^1.0.3", "@types/leaflet-providers": "^1.2.0", "@types/leaflet.markercluster": "^1.4.3", + "@types/lz-string": "^1.3.34", "autoprefixer": "^9.8.6", "country-language": "^0.1.7", "email-validator": "^2.0.4", @@ -49,6 +50,7 @@ "leaflet.markercluster": "^1.4.1", "libphonenumber": "0.0.10", "libphonenumber-js": "^1.7.55", + "lz-string": "^1.4.4", "mangrove-reviews": "^0.1.3", "moment": "^2.29.0", "opening_hours": "^3.5.0", diff --git a/scripts/genKeys.sh b/scripts/genKeys.sh new file mode 100755 index 0000000..241b11d --- /dev/null +++ b/scripts/genKeys.sh @@ -0,0 +1,12 @@ +#! /bin/bash + +# Generates all the keys that are frequently used in the JSON in order to compress them +touch keys.csv +for f in ../Customizations/JSON/*Json.ts +do + echo "$f" + cat $f | tr -d "[]{}," | sed "s/^[ \t]*//" | grep -v "^/\?\*" | grep -v "import \.*" | grep -v "^export" | sed "s/?\?:.*//" >> keys.csv +done +cat keys.csv | wc -l +cat keys.csv | sort | uniq | sed "s/^\(.*\)$/\"\1\",/" | tr -d "\n" +rm keys.csv \ No newline at end of file diff --git a/test/Utils.spec.ts b/test/Utils.spec.ts new file mode 100644 index 0000000..4b99c45 --- /dev/null +++ b/test/Utils.spec.ts @@ -0,0 +1,50 @@ +import T from "./TestHelper"; +import {Utils} from "../Utils"; +import {equal} from "assert"; +import {existsSync, mkdirSync, readFileSync, writeFile, writeFileSync} from "fs"; +import LZString from "lz-string"; +new T("Utils",[ + ["Minify-json",() => { + const str = JSON.stringify({title: "abc", "and":"xyz", "render":"somevalue"}, null, 0); + const minified = Utils.MinifyJSON(str); + console.log(minified) + console.log("Minified version has ", minified.length, "chars") + const restored = Utils.UnMinify(minified) + console.log(restored) + console.log("Restored version has ", restored.length, "chars") + equal(str, restored) + + }], + ["Minify-json of the bookcases",() => { + let str = readFileSync("/home/pietervdvn/git/MapComplete/assets/layers/public_bookcases/public_bookcases.json", "UTF8") + str = JSON.stringify(JSON.parse(str), null, 0) + const minified = Utils.MinifyJSON(str); + console.log("Minified version has ", minified.length, "chars") + const restored = Utils.UnMinify(minified) + console.log("Restored version has ", restored.length, "chars") + equal(str, restored) + + }], + ["Minify-json with LZ-string of the bookcases",() => { + let str = readFileSync("/home/pietervdvn/git/MapComplete/assets/layers/public_bookcases/public_bookcases.json", "UTF8") + str = JSON.stringify(JSON.parse(str), null, 0) + const minified =LZString.compressToBase64(Utils.MinifyJSON(str)); + console.log("Minified version has ", minified.length, "chars") + const restored = Utils.UnMinify(LZString.decompressFromBase64(minified)) + console.log("Restored version has ", restored.length, "chars") + equal(str, restored) + + }], + ["Minify-json with only LZ-string of the bookcases",() => { + let str = readFileSync("/home/pietervdvn/git/MapComplete/assets/layers/public_bookcases/public_bookcases.json", "UTF8") + str = JSON.stringify(JSON.parse(str), null, 0) + const minified =LZString.compressToBase64(str); + console.log("Minified version has ", minified.length, "chars") + const restored = LZString.decompressFromBase64(minified) + console.log("Restored version has ", restored.length, "chars") + equal(str, restored) + + }] + + +]) \ No newline at end of file From cb281edf7e1d6f1d61393d9df06189896b1e1954 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 4 Apr 2021 03:23:18 +0200 Subject: [PATCH 04/97] Version bump --- Models/Constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Models/Constants.ts b/Models/Constants.ts index 52df36f..834d842 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.6.4b"; + public static vNumber = "0.6.5"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { From e96e46c743d8253245cca40cd40cf288e7e300cb Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 4 Apr 2021 19:23:37 +0200 Subject: [PATCH 05/97] Add reviews to fritures --- assets/themes/fritures/fritures.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/themes/fritures/fritures.json b/assets/themes/fritures/fritures.json index ae64d57..99cb659 100644 --- a/assets/themes/fritures/fritures.json +++ b/assets/themes/fritures/fritures.json @@ -223,7 +223,9 @@ } } ] - } + }, + "questions", + "reviews" ], "icon": { "render": "./assets/themes/fritures/fries.svg" @@ -253,4 +255,4 @@ } ], "roamingRenderings": [] -} \ No newline at end of file +} From a109b0968114152541ced78cf3be0628d08a97ee Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 5 Apr 2021 20:40:20 +0200 Subject: [PATCH 06/97] Small tweak to the AED theme: show acces if defined --- Models/Constants.ts | 2 +- assets/themes/aed/aed.json | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Models/Constants.ts b/Models/Constants.ts index 52df36f..d5eced1 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.6.4b"; + public static vNumber = "0.6.4c"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/assets/themes/aed/aed.json b/assets/themes/aed/aed.json index ef7e454..f15d3a1 100644 --- a/assets/themes/aed/aed.json +++ b/assets/themes/aed/aed.json @@ -125,7 +125,12 @@ "nl": "Toegankelijkheid is {access}", "de": "Zugang ist {access}" }, - "condition": "indoor=yes", + "condition": { + "or": [ + "indoor=yes", + "access~*" + ] + }, "freeform": { "key": "access", "addExtraTags": [ @@ -209,7 +214,10 @@ } }, { - "render": "{defibrillator:location}", + "render": { + "nl": "Meer informatie over de locatie:
{defibrillator:location}", + "en": "Extra information about the location:
{defibrillator:location}" + }, "question": { "en": "Please give some explanation on where the defibrillator can be found", "ca": "Dóna detalls d'on es pot trobar el desfibril·lador", From 0069214f32d3893dac35cc95d033392863864997 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 5 Apr 2021 20:57:07 +0200 Subject: [PATCH 07/97] Fix comments --- Customizations/JSON/TagRenderingConfigJson.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Customizations/JSON/TagRenderingConfigJson.ts b/Customizations/JSON/TagRenderingConfigJson.ts index de091b3..cf36f66 100644 --- a/Customizations/JSON/TagRenderingConfigJson.ts +++ b/Customizations/JSON/TagRenderingConfigJson.ts @@ -3,8 +3,7 @@ import {AndOrTagConfigJson} from "./TagConfigJson"; /** * A TagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet. * If the desired tags are missing and a question is defined, a question will be shown instead. - * - / + */ export interface TagRenderingConfigJson { /** * Renders this value. Note that "{key}"-parts are substituted by the corresponding values of the element. From 8097486ffe64b528753b70e00ac80e23bceb6d96 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 6 Apr 2021 16:12:44 +0200 Subject: [PATCH 08/97] Small fix to mapillary image loading + addition of test --- Logic/Actors/ImageSearcher.ts | 2 +- Models/Constants.ts | 2 +- package.json | 2 +- test/ImageSearcher.spec.ts | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 test/ImageSearcher.spec.ts diff --git a/Logic/Actors/ImageSearcher.ts b/Logic/Actors/ImageSearcher.ts index 0c1c934..3ea5101 100644 --- a/Logic/Actors/ImageSearcher.ts +++ b/Logic/Actors/ImageSearcher.ts @@ -81,7 +81,7 @@ export class ImageSearcher extends UIEventSource<{ key: string, url: string }[]> let mapillary = tags.mapillary; const prefix = "https://www.mapillary.com/map/im/"; - let regex = /https?:\/\/www.mapillary.com\/app\/.*&pKey=([^&]*)/ + let regex = /https?:\/\/www.mapillary.com\/app\/.*pKey=([^&]*).*/ let match = mapillary.match(regex); if (match) { mapillary = match[1]; diff --git a/Models/Constants.ts b/Models/Constants.ts index d5eced1..cf6b7b1 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.6.4c"; + public static vNumber = "0.6.4d"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/package.json b/package.json index 4b750ea..ea53695 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", "start": "npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", - "test": "ts-node test/Tag.spec.ts && ts-node test/TagQuestion.spec.ts", + "test": "ts-node test/Tag.spec.ts && ts-node test/TagQuestion.spec.ts && ts-node test/ImageSearcher.spec.ts", "generate:editor-layer-index": "cd assets/ && wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json", "generate:images": "ts-node scripts/generateIncludedImages.ts", "generate:translations": "ts-node scripts/generateTranslations.ts", diff --git a/test/ImageSearcher.spec.ts b/test/ImageSearcher.spec.ts new file mode 100644 index 0000000..69462be --- /dev/null +++ b/test/ImageSearcher.spec.ts @@ -0,0 +1,35 @@ +import {Utils} from "../Utils"; + +Utils.runningFromConsole = true; +import {equal} from "assert"; +import T from "./TestHelper"; +import {FromJSON} from "../Customizations/JSON/FromJSON"; +import Locale from "../UI/i18n/Locale"; +import Translations from "../UI/i18n/Translations"; +import {UIEventSource} from "../Logic/UIEventSource"; +import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig"; +import EditableTagRendering from "../UI/Popup/EditableTagRendering"; +import {Translation} from "../UI/i18n/Translation"; +import {OH, OpeningHour} from "../UI/OpeningHours/OpeningHours"; +import PublicHolidayInput from "../UI/OpeningHours/PublicHolidayInput"; +import {SubstitutedTranslation} from "../UI/SubstitutedTranslation"; +import {Tag} from "../Logic/Tags/Tag"; +import {And} from "../Logic/Tags/And"; +import {ImageSearcher} from "../Logic/Actors/ImageSearcher"; + + +new T("ImageSearcher", [ + [ + "Should find images", + () => { + const tags = new UIEventSource({ + "mapillary": "https://www.mapillary.com/app/?pKey=bYH6FFl8LXAPapz4PNSh3Q" + }); + const searcher = ImageSearcher.construct(tags) + const result = searcher.data[0]; + equal(result.url, "https://www.mapillary.com/map/im/bYH6FFl8LXAPapz4PNSh3Q"); + } + ] + + +]) \ No newline at end of file From b3f02572d5553d0087ce9aacab1fb57ffa2cfa51 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 6 Apr 2021 17:28:11 +0200 Subject: [PATCH 09/97] Update of calculated tags documentation --- Docs/CalculatedTags.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/Docs/CalculatedTags.md b/Docs/CalculatedTags.md index 9731592..d531ca3 100644 --- a/Docs/CalculatedTags.md +++ b/Docs/CalculatedTags.md @@ -34,7 +34,7 @@ Adds the time that the data got loaded - pretty much the time of downloading fro Calculating tags with Javascript -------------------------------- -In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. **\_lat**, **lon**, **\_country**), as detailed above. +In some cases, it is useful to have some tags calculated based on other properties. Some useful tags are available by default (e.g. **lat**, **lon**, **\_country**), as detailed above. It is also possible to calculate your own tags - but this requires some javascript knowledge. @@ -46,11 +46,29 @@ Before proceeding, some warnings: In the layer object, add a field **calculatedTags**, e.g.: -"calculatedTags": { "\_someKey": "javascript-expression", "name": "feat.properties.name ?? feat.properties.ref ?? feat.properties.operator", "\_distanceCloserThen3Km": "feat.distanceTo( some\_lon, some\_lat) < 3 ? 'yes' : 'no'" } +"calculatedTags": \[ "\_someKey=javascript-expression", "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator", "\_distanceCloserThen3Km=feat.distanceTo( some\_lon, some\_lat) < 3 ? 'yes' : 'no'" \] + +The above code will be executed for every feature in the layer. The feature is accessible as **feat** and is an amended geojson object: - **area** contains the surface area (in square meters) of the object - **lat** and **lon** contain the latitude and longitude Some advanced functions are available on **feat** as well: + +* distanceTo +* overlapWith +* closest ### distanceTo Calculates the distance between the feature and a specified point * longitude -* latitude \ No newline at end of file +* latitude + +### overlapWith + +Gives a list of features from the specified layer which this feature overlaps with, the amount of overlap in m². The returned value is **{ feat: GeoJSONFeature, overlap: number}** + +* ...layerIds - one or more layer ids of the layer from which every feature is checked for overlap) + +### closest + +Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. + +* list of features \ No newline at end of file From 6d5cd95d941595f645ffe8a645f7605c7d68c5c7 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Tue, 6 Apr 2021 18:14:37 +0200 Subject: [PATCH 10/97] Small comment update --- Docs/Making_Your_Own_Theme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Docs/Making_Your_Own_Theme.md b/Docs/Making_Your_Own_Theme.md index adfb9d5..8c051f2 100644 --- a/Docs/Making_Your_Own_Theme.md +++ b/Docs/Making_Your_Own_Theme.md @@ -128,4 +128,6 @@ For example, in the [cyclofix-theme](https://mapcomplete.osm.org/cyclofix), ther If all the layers are deselected except the bike wash layer, a shop having this tag will still match and will still show up as shop. +### Not reading the .JSON-specs +There are a few advanced features to do fancy stuff available, which are documented only in the spec above - for example, reusing background images and substituting the colours or HTML-rendering. If you need advanced stuff, read it through! From 4d5c250f8f15a538192d3ca554c32a471cf348d7 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 6 Apr 2021 18:17:07 +0200 Subject: [PATCH 11/97] Add HTML rendering options to icons --- Customizations/JSON/LayerConfig.ts | 56 +++++++++++++++---- Customizations/JSON/LayerConfigJson.ts | 3 + Logic/Tags/SubstitutingTag.ts | 4 +- UI/SubstitutedTranslation.ts | 40 ++++++------- .../public_bookcases/public_bookcases.json | 8 ++- 5 files changed, 77 insertions(+), 34 deletions(-) diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index 7d85a61..71011ce 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -17,6 +17,7 @@ import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation"; import SourceConfig from "./SourceConfig"; import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import {Tag} from "../../Logic/Tags/Tag"; +import SubstitutingTag from "../../Logic/Tags/SubstitutingTag"; export default class LayerConfig { @@ -218,11 +219,35 @@ export default class LayerConfig { this.dashArray = tr("dashArray", ""); - if(json["showIf"] !== undefined){ - throw "Invalid key on layerconfig "+this.id+": showIf. Did you mean 'isShown' instead?"; + if (json["showIf"] !== undefined) { + throw "Invalid key on layerconfig " + this.id + ": showIf. Did you mean 'isShown' instead?"; } } + /** + * Splits the parts of the icon, at ";" but makes sure that everything between "" and "" stays together + * @param template + * @constructor + * @private + */ + private static SplitParts(template: string): string[] { + const htmlParts = template.split(""); + const parts = [] + for (const htmlPart of htmlParts) { + if (htmlPart.indexOf("") >= 0) { + const subparts = htmlPart.split(""); + if (subparts.length != 2) { + throw "Invalid rendering with embedded html: " + htmlPart; + } + parts.push("html:" + subparts[0]); + parts.push(...subparts[1].split(";")) + } else { + parts.push(...htmlPart.split(";")) + } + } + return parts.filter(prt => prt != ""); + } + public CustomCodeSnippets(): string[] { if (this.calculatedTags === undefined) { return [] @@ -343,15 +368,16 @@ export default class LayerConfig { const iconUrlStatic = render(this.icon); const self = this; const mappedHtml = tags.map(tgs => { - // What do you mean, 'tgs' is never read? - // It is read implicitly in the 'render' method - const iconUrl = render(self.icon); - const rotation = render(self.rotation, "0deg"); - - let htmlParts: UIElement[] = []; - let sourceParts = iconUrl.split(";"); - function genHtmlFromString(sourcePart: string): UIElement { + console.log("Got source part ", sourcePart) + if (sourcePart.indexOf("html:") == 0) { + // We use § as a replacement for ; + const html = sourcePart.substring("html:".length) + const inner = new FixedUiElement(SubstitutingTag.substituteString(html, tgs)).SetClass("block w-min text-center") + const outer = new Combine([inner]).SetClass("flex flex-col items-center") + return outer; + } + const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; let html: UIElement = new FixedUiElement(``); const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/) @@ -365,6 +391,14 @@ export default class LayerConfig { } + // What do you mean, 'tgs' is never read? + // It is read implicitly in the 'render' method + const iconUrl = render(self.icon); + const rotation = render(self.rotation, "0deg"); + + let htmlParts: UIElement[] = []; + let sourceParts = LayerConfig.SplitParts(iconUrl); + for (const sourcePart of sourceParts) { htmlParts.push(genHtmlFromString(sourcePart)) } @@ -377,7 +411,7 @@ export default class LayerConfig { } if (iconOverlay.badge) { const badgeParts: UIElement[] = []; - const partDefs = iconOverlay.then.GetRenderValue(tgs).txt.split(";"); + const partDefs = LayerConfig.SplitParts(iconOverlay.then.GetRenderValue(tgs).txt); for (const badgePartStr of partDefs) { badgeParts.push(genHtmlFromString(badgePartStr)) diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts index b21bb3f..b78cb5c 100644 --- a/Customizations/JSON/LayerConfigJson.ts +++ b/Customizations/JSON/LayerConfigJson.ts @@ -105,6 +105,9 @@ export interface LayerConfigJson { * As a result, on could use a generic pin, then overlay it with a specific icon. * To make things even more practical, one can use all svgs from the folder "assets/svg" and _substitute the color_ in it. * E.g. to draw a red pin, use "pin:#f00", to have a green circle with your icon on top, use `circle:#0f0;` + * + * Also note that one can specify to use HTML by entering some html between "" and "
{name}
" */ icon?: string | TagRenderingConfigJson; diff --git a/Logic/Tags/SubstitutingTag.ts b/Logic/Tags/SubstitutingTag.ts index 58591d5..567693c 100644 --- a/Logic/Tags/SubstitutingTag.ts +++ b/Logic/Tags/SubstitutingTag.ts @@ -18,11 +18,11 @@ export default class SubstitutingTag implements TagsFilter { this._value = value; } - private static substituteString(template: string, dict: any): string { + public static substituteString(template: string, dict: any): string { for (const k in dict) { template = template.replace(new RegExp("\\{" + k + "\\}", 'g'), dict[k]) } - return template; + return template.replace(/{.*}/g, ""); } asHumanString(linkToWiki: boolean, shorten: boolean, properties) { diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index cd132fd..4483887 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -9,7 +9,7 @@ import SpecialVisualizations from "./SpecialVisualizations"; import {Utils} from "../Utils"; export class SubstitutedTranslation extends UIElement { - private static cachedTranslations: + private static cachedTranslations: Map, SubstitutedTranslation>>> = new Map, SubstitutedTranslation>>>(); private readonly tags: UIEventSource; private readonly translation: Translation; @@ -34,39 +34,39 @@ export class SubstitutedTranslation extends UIElement { this.SetClass("w-full") } - private static GenerateMap(){ - return new Map, SubstitutedTranslation>() - } - private static GenerateSubCache(){ - return new Map, SubstitutedTranslation>>(); - } - public static construct( translation: Translation, tags: UIEventSource): SubstitutedTranslation { - - /* let cachedTranslations = Utils.getOrSetDefault(SubstitutedTranslation.cachedTranslations, SubstitutedTranslation.GenerateSubCache); - const innerMap = Utils.getOrSetDefault(cachedTranslations, translation, SubstitutedTranslation.GenerateMap); - const cachedTranslation = innerMap.get(tags); - if (cachedTranslation !== undefined) { - return cachedTranslation; - }*/ + /* let cachedTranslations = Utils.getOrSetDefault(SubstitutedTranslation.cachedTranslations, SubstitutedTranslation.GenerateSubCache); + const innerMap = Utils.getOrSetDefault(cachedTranslations, translation, SubstitutedTranslation.GenerateMap); + + const cachedTranslation = innerMap.get(tags); + if (cachedTranslation !== undefined) { + return cachedTranslation; + }*/ const st = new SubstitutedTranslation(translation, tags); - // innerMap.set(tags, st); + // innerMap.set(tags, st); return st; } public static SubstituteKeys(txt: string, tags: any) { for (const key in tags) { - // Poor mans replace all - txt = txt.split("{" + key + "}").join(tags[key]); + txt = txt.replace(new RegExp("{" + key + "}", "g"), tags[key]) } - return txt; + return txt.replace(/{.*}/g, ""); + } + + private static GenerateMap() { + return new Map, SubstitutedTranslation>() + } + + private static GenerateSubCache() { + return new Map, SubstitutedTranslation>>(); } InnerRender(): string { - if(this.content.length == 1){ + if (this.content.length == 1) { return this.content[0].Render(); } return new Combine(this.content).Render(); diff --git a/assets/layers/public_bookcases/public_bookcases.json b/assets/layers/public_bookcases/public_bookcases.json index c159920..8087a36 100644 --- a/assets/layers/public_bookcases/public_bookcases.json +++ b/assets/layers/public_bookcases/public_bookcases.json @@ -37,7 +37,13 @@ ] }, "icon": { - "render": "./assets/themes/bookcases/bookcase.svg" + "render": "./assets/themes/bookcases/bookcase.svg;", + "mappings": [ + { + "if": "name~*", + "then": "./assets/themes/bookcases/bookcase.svg;
{name}
" + } + ] }, "color": { "render": "#0000ff" From 454f30cf3bf0bbb036d527a5d3d606b3a2228047 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 6 Apr 2021 18:34:45 +0200 Subject: [PATCH 12/97] Add docs on URL-parameters --- Docs/URL_Parameters.md | 108 +++++++++++++++++++++++++++++++++++ Logic/Web/QueryParameters.ts | 12 +++- Models/Constants.ts | 2 +- 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 Docs/URL_Parameters.md diff --git a/Docs/URL_Parameters.md b/Docs/URL_Parameters.md new file mode 100644 index 0000000..4952640 --- /dev/null +++ b/Docs/URL_Parameters.md @@ -0,0 +1,108 @@ +custom-css +------------ +If specified, the custom css from the given link will be loaded additionaly + +test +------ +If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org +The default value is _false_ + +layout +-------- +The layout to load into MapComplete + +userlayout +------------ + +The default value is _false_ + +layer-control-toggle +---------------------- +Whether or not the layer control is shown +The default value is _false_ + +tab +----- +The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >50 changesets) +The default value is _0_ + +z +--- +The initial/current zoom level +The default value is set by the loaded theme + +lat +----- +The initial/current latitude +The default value is set by the loaded theme + +lon +----- +The initial/current longitude of the app +The default value is set by the loaded theme + +fs-userbadge +-------------- +Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode. +The default value is _true_ + +fs-search +----------- +Disables/Enables the search bar +The default value is _true_ + +fs-layers +----------- +Disables/Enables the layer control +The default value is _true_ + +fs-add-new +------------ +Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) +The default value is _true_ + +fs-welcome-message +-------------------- +Disables/enables the help menu or welcome message +The default value is _true_ + +fs-iframe +----------- +Disables/Enables the iframe-popup +The default value is _false_ + +fs-more-quests +---------------- +Disables/Enables the 'More Quests'-tab in the welcome message +The default value is _true_ + +fs-share-screen +----------------- +Disables/Enables the 'Share-screen'-tab in the welcome message +The default value is _true_ + +fs-geolocation +---------------- +Disables/Enables the geolocation button +The default value is _true_ + +debug +------- +If true, shows some extra debugging help such as all the available tags on every object +The default value is _false_ + +oauth_token +------------- +Used to complete the login +No default value set + +background +------------ +The id of the background layer to start with +The default value is set by the loaded theme + +layer- +----------------- +Wether or not layer with __ is shown +The default value is _true_ + diff --git a/Logic/Web/QueryParameters.ts b/Logic/Web/QueryParameters.ts index 406afa2..b1adfcd 100644 --- a/Logic/Web/QueryParameters.ts +++ b/Logic/Web/QueryParameters.ts @@ -38,6 +38,10 @@ export class QueryParameters { QueryParameters.knownSources[key] = source; } } + + window["mapcomplete_query_parameter_overview"] = () => { + console.log(QueryParameters.GenerateQueryParameterDocs()) + } } private static Serialize() { @@ -84,7 +88,13 @@ export class QueryParameters { public static GenerateQueryParameterDocs(): string { const docs = []; for (const key in QueryParameters.documentation) { - docs.push("**" + key + "**: " + QueryParameters.documentation[key] + " (default value: _" + QueryParameters.defaults[key] + "_)") + docs.push([ + " "+key+" ", + "-".repeat(key.length + 2), + QueryParameters.documentation[key], + QueryParameters.defaults[key] === undefined ? "No default value set" : `The default value is _${QueryParameters.defaults[key]}_` + + ].join("\n")) } return docs.join("\n\n"); } diff --git a/Models/Constants.ts b/Models/Constants.ts index e7a8c2b..01627fb 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.6.5d"; + public static vNumber = "0.6.6-rc-d"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { From 61377d907bb8fc30d23c69450ddc86a8a25a2439 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 6 Apr 2021 19:41:41 +0200 Subject: [PATCH 13/97] Erase the hash when an element is unselected --- Customizations/JSON/LayerConfig.ts | 1 - InitUiElements.ts | 1 + Logic/Actors/SelectedFeatureHandler.ts | 4 ++++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index 71011ce..0d1a160 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -369,7 +369,6 @@ export default class LayerConfig { const self = this; const mappedHtml = tags.map(tgs => { function genHtmlFromString(sourcePart: string): UIElement { - console.log("Got source part ", sourcePart) if (sourcePart.indexOf("html:") == 0) { // We use § as a replacement for ; const html = sourcePart.substring("html:".length) diff --git a/InitUiElements.ts b/InitUiElements.ts index 80f9d1c..f5e948f 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -448,5 +448,6 @@ export class InitUiElements { ); }); + } } \ No newline at end of file diff --git a/Logic/Actors/SelectedFeatureHandler.ts b/Logic/Actors/SelectedFeatureHandler.ts index 968a3d1..5f5bf5f 100644 --- a/Logic/Actors/SelectedFeatureHandler.ts +++ b/Logic/Actors/SelectedFeatureHandler.ts @@ -27,6 +27,10 @@ export default class SelectedFeatureHandler { featureSource.features.addCallback(_ => self.selectFeature()); selectedFeature.addCallback(feature => { + if(feature === undefined){ + hash.setData("") + } + const h = feature?.properties?.id; if(h !== undefined){ hash.setData(h) From ba223bd8d0c21af590791e4f1beb87db5dc009fd Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 6 Apr 2021 19:44:56 +0200 Subject: [PATCH 14/97] Version bump --- Models/Constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Models/Constants.ts b/Models/Constants.ts index 01627fb..35db587 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.6.6-rc-d"; + public static vNumber = "0.6.6-rc-e"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { From b6401e4ca14ca9fd645973ecaec36685cf17118e Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 6 Apr 2021 19:59:49 +0200 Subject: [PATCH 15/97] Fix issue in nature reserves title --- assets/layers/nature_reserve/nature_reserve.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/layers/nature_reserve/nature_reserve.json b/assets/layers/nature_reserve/nature_reserve.json index faabad8..b7a3324 100644 --- a/assets/layers/nature_reserve/nature_reserve.json +++ b/assets/layers/nature_reserve/nature_reserve.json @@ -20,7 +20,7 @@ { "if": { "and": [ - "name:nl~" + "name:nl~*" ] }, "then": { From 117c62e01139283c7a29d7ce4fb3e4f535225858 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 6 Apr 2021 20:00:18 +0200 Subject: [PATCH 16/97] Version bump --- Models/Constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Models/Constants.ts b/Models/Constants.ts index 35db587..62bc426 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.6.6-rc-e"; + public static vNumber = "0.6.6"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { From e1d7b256f934d989d5d5e037f233c9df8b4997f6 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 6 Apr 2021 20:02:41 +0200 Subject: [PATCH 17/97] Small improvement to custom theme generator --- UI/CustomGenerator/TagRenderingPanel.ts | 53 ++++++++++++++----------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/UI/CustomGenerator/TagRenderingPanel.ts b/UI/CustomGenerator/TagRenderingPanel.ts index 0633411..302a1a3 100644 --- a/UI/CustomGenerator/TagRenderingPanel.ts +++ b/UI/CustomGenerator/TagRenderingPanel.ts @@ -21,15 +21,14 @@ import Constants from "../../Models/Constants"; export default class TagRenderingPanel extends InputElement { + public IsImage = false; + public options: { title?: string; description?: string; disableQuestions?: boolean; isImage?: boolean; }; + public readonly validText: UIElement; + IsSelected: UIEventSource = new UIEventSource(false); private intro: UIElement; private settingsTable: UIElement; - - public IsImage = false; private readonly _value: UIEventSource; - public options: { title?: string; description?: string; disableQuestions?: boolean; isImage?: boolean; }; - public readonly validText : UIElement; - constructor(languages: UIEventSource, currentlySelected: UIEventSource>, userDetails: UserDetails, @@ -47,11 +46,11 @@ export default class TagRenderingPanel extends InputElement", options?.title ?? "TagRendering", "", + this.intro = new Combine(["

", options?.title ?? "TagRendering", "

", options?.description ?? "A tagrendering converts OSM-tags into a value on screen. Fill out the field 'render' with the text that should appear. Note that `{key}` will be replaced with the corresponding `value`, if present.
For specific known tags (e.g. if `foo=bar`, make a mapping). "]) this.IsImage = options?.isImage ?? false; @@ -61,10 +60,20 @@ export default class TagRenderingPanel extends InputElement, id: string | string[], name: string, description: string | UIElement): SingleSetting { return new SingleSetting(value, input, id, name, description); } - + this._value.addCallback(value => { - if(value?.freeform?.key == ""){ + let doPing = false; + if (value?.freeform?.key == "") { value.freeform = undefined; + doPing = true; + } + + if (value?.render == "") { + value.render = undefined; + doPing = true; + } + + if (doPing) { this._value.ping(); } }) @@ -72,7 +81,7 @@ export default class TagRenderingPanel extends InputElementFreeform key", @@ -90,11 +99,11 @@ export default class TagRenderingPanel extends InputElement)[] = [ setting( - options?.noLanguage ? new TextField({placeholder:"Rendering"}) : - new MultiLingualTextFields(languages), "render", "Value to show", + options?.noLanguage ? new TextField({placeholder: "Rendering"}) : + new MultiLingualTextFields(languages), "render", "Value to show", "Renders this value. Note that {key}-parts are substituted by the corresponding values of the element. If neither 'textFieldQuestion' nor 'mappings' are defined, this text is simply shown as default value." + "

" + - "Furhtermore, some special functions are supported:"+SpecialVisualizations.HelpMessage.Render()), + "Furhtermore, some special functions are supported:" + SpecialVisualizations.HelpMessage.Render()), questionsNotUnlocked ? `You need at least ${Constants.userJourney.themeGeneratorFullUnlock} changesets to unlock the 'question'-field and to use your theme to edit OSM data` : "", ...(options?.disableQuestions ? [] : questionSettings), @@ -114,17 +123,17 @@ export default class TagRenderingPanel extends InputElement { - try{ - new TagRenderingConfig(json,undefined, options?.title ?? ""); + try { + new TagRenderingConfig(json, undefined, options?.title ?? ""); return ""; - }catch(e){ - return ""+e+"" + } catch (e) { + return "" + e + "" } })); - + } InnerRender(): string { @@ -138,8 +147,6 @@ export default class TagRenderingPanel extends InputElement = new UIEventSource(false); - IsValid(t: TagRenderingConfigJson): boolean { return false; } From fbdd36b270ee9138c61becef0906eaeca69ba1b6 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 6 Apr 2021 21:10:18 +0200 Subject: [PATCH 18/97] Small fix: restore special components --- Customizations/JSON/LayerConfig.ts | 2 +- UI/SubstitutedTranslation.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index 0d1a160..ba6bd4f 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -332,7 +332,7 @@ export default class LayerConfig { function render(tr: TagRenderingConfig, deflt?: string) { const str = (tr?.GetRenderValue(tags.data)?.txt ?? deflt); - return SubstitutedTranslation.SubstituteKeys(str, tags.data); + return SubstitutedTranslation.SubstituteKeys(str, tags.data).replace(/{.*}/g, ""); } const iconSize = render(this.iconSize, "40,40,center").split(","); diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index 4483887..24c200f 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -54,7 +54,7 @@ export class SubstitutedTranslation extends UIElement { for (const key in tags) { txt = txt.replace(new RegExp("{" + key + "}", "g"), tags[key]) } - return txt.replace(/{.*}/g, ""); + return txt; } private static GenerateMap() { @@ -117,8 +117,8 @@ export class SubstitutedTranslation extends UIElement { } } - // IF we end up here, no changes have to be made - return [new FixedUiElement(template)]; + // IF we end up here, no changes have to be made - except to remove any resting {} + return [new FixedUiElement(template.replace(/{.*}/g, ""))]; } } \ No newline at end of file From f91cb0a22a18cb13168d951a9c5aa448689c5ad9 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Tue, 6 Apr 2021 23:55:18 +0200 Subject: [PATCH 19/97] Add social image --- Docs/Making_Your_Own_Theme.md | 9 +- Models/Constants.ts | 2 +- Svg.ts | 7 +- assets/SocialImage.png | Bin 0 -> 1650429 bytes assets/generic_osm_background.png | Bin 0 -> 2317260 bytes assets/social_image_front.png | Bin 0 -> 209933 bytes assets/svg/SocialImageForeground.svg | 2522 ++++++++++++++++++++++++++ assets/weblogo1000.png | Bin 0 -> 178835 bytes index.html | 18 +- 9 files changed, 2551 insertions(+), 7 deletions(-) create mode 100644 assets/SocialImage.png create mode 100644 assets/generic_osm_background.png create mode 100644 assets/social_image_front.png create mode 100644 assets/svg/SocialImageForeground.svg create mode 100644 assets/weblogo1000.png diff --git a/Docs/Making_Your_Own_Theme.md b/Docs/Making_Your_Own_Theme.md index 51836ca..bb5c8a8 100644 --- a/Docs/Making_Your_Own_Theme.md +++ b/Docs/Making_Your_Own_Theme.md @@ -50,7 +50,7 @@ The preferred way to add your theme is via a Pull Request. A Pull Request is les 1) Fork this repository 2) Go to `assets/themes` and create a new directory `yourtheme` 3) Create a new file `yourtheme.json`, paste the theme configuration in there. You can find your theme configuration in the customThemeBuilder (the tab with the *Floppy disk* icon) -4) Copy all the images into this new directory: external assets can suddenly break and leak privacy +4) Copy all the images into this new directory. **No external sources are allowed!** External image sources leak privacy or can break. - Make sure the license is suitable, preferable a Creative Commons license. Attribution can be added at the bottom of this document - If an SVG version is available, use the SVG version - Make sure all the links in `yourtheme.json` are updated. You can use `./assets/themes/yourtheme/yourimage.svg` instead of the HTML link @@ -58,9 +58,10 @@ The preferred way to add your theme is via a Pull Request. A Pull Request is les - Open [AllKnownLayouts.ts](https://github.com/pietervdvn/MapComplete/blob/master/Customizations/AllKnownLayouts.ts) - Add an import statement, e.g. `import * as yourtheme from "../assets/themes/yourtheme/yourthemes.json";` - Add your theme to the `LayoutsList`, by adding a line `new LayoutConfig(yourtheme)` - 6) Test your theme: run the project as described [above](../README.md#Dev) - 7) Happy with your theme? Time to open a Pull Request! - 8) Thanks a lot for improving MapComplete! + 6) Add some finishing touches, such as a social image. See [this blog post](https://www.h3xed.com/web-and-internet/how-to-use-og-image-meta-tag-facebook-reddit) for some hints + 7) Test your theme: run the project as described [above](../README.md#Dev) + 8) Happy with your theme? Time to open a Pull Request! + 9) Thanks a lot for improving MapComplete! The .JSON-format diff --git a/Models/Constants.ts b/Models/Constants.ts index 62bc426..5210c9f 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.6.6"; + public static vNumber = "0.6.6a"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/Svg.ts b/Svg.ts index cae6cc7..a66fee7 100644 --- a/Svg.ts +++ b/Svg.ts @@ -39,6 +39,11 @@ export default class Svg { public static Ornament_Horiz_6_svg() { return new FixedUiElement(Svg.Ornament_Horiz_6);} public static Ornament_Horiz_6_ui() { return new FixedUiElement(Svg.Ornament_Horiz_6_img);} + public static SocialImageForeground = " image/svg+xml 010110010011010110010011  010110010011010110010011  " + public static SocialImageForeground_img = Img.AsImageElement(Svg.SocialImageForeground) + public static SocialImageForeground_svg() { return new FixedUiElement(Svg.SocialImageForeground);} + public static SocialImageForeground_ui() { return new FixedUiElement(Svg.SocialImageForeground_img);} + public static add = " image/svg+xml " public static add_img = Img.AsImageElement(Svg.add) public static add_svg() { return new FixedUiElement(Svg.add);} @@ -344,4 +349,4 @@ export default class Svg { public static wikipedia_svg() { return new FixedUiElement(Svg.wikipedia);} public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);} -public static All = {"Ornament-Horiz-0.svg": Svg.Ornament_Horiz_0,"Ornament-Horiz-1.svg": Svg.Ornament_Horiz_1,"Ornament-Horiz-2.svg": Svg.Ornament_Horiz_2,"Ornament-Horiz-3.svg": Svg.Ornament_Horiz_3,"Ornament-Horiz-4.svg": Svg.Ornament_Horiz_4,"Ornament-Horiz-5.svg": Svg.Ornament_Horiz_5,"Ornament-Horiz-6.svg": Svg.Ornament_Horiz_6,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} +public static All = {"Ornament-Horiz-0.svg": Svg.Ornament_Horiz_0,"Ornament-Horiz-1.svg": Svg.Ornament_Horiz_1,"Ornament-Horiz-2.svg": Svg.Ornament_Horiz_2,"Ornament-Horiz-3.svg": Svg.Ornament_Horiz_3,"Ornament-Horiz-4.svg": Svg.Ornament_Horiz_4,"Ornament-Horiz-5.svg": Svg.Ornament_Horiz_5,"Ornament-Horiz-6.svg": Svg.Ornament_Horiz_6,"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} diff --git a/assets/SocialImage.png b/assets/SocialImage.png new file mode 100644 index 0000000000000000000000000000000000000000..6b8d14457429999d1a89a96b7a6ec1e199101cb3 GIT binary patch literal 1650429 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

bi)WIH$xvLntKZ(Tatb~f-kv+ci@)Do%_HNwE2 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