diff --git a/Logic/DetermineLayout.ts b/Logic/DetermineLayout.ts index 4dcb1c1d7..6f76cdbe6 100644 --- a/Logic/DetermineLayout.ts +++ b/Logic/DetermineLayout.ts @@ -10,16 +10,17 @@ import {UIEventSource} from "./UIEventSource"; import {LocalStorageSource} from "./Web/LocalStorageSource"; import LZString from "lz-string"; import * as personal from "../assets/themes/personal/personal.json"; -import {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; +import {FixImages, FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; import SharedTagRenderings from "../Customizations/SharedTagRenderings"; import * as known_layers from "../assets/generated/known_layers.json" import {LayoutConfigJson} from "../Models/ThemeConfig/Json/LayoutConfigJson"; import {PrepareTheme} from "../Models/ThemeConfig/Conversion/PrepareTheme"; -import {Layer} from "leaflet"; - +import * as licenses from "../assets/generated/license_info.json" export default class DetermineLayout { + private static readonly _knownImages =new Set( Array.from(licenses).map(l => l.path)) + /** * Gets the correct layout for this website */ @@ -144,6 +145,7 @@ export default class DetermineLayout { sharedLayers: knownLayersDict } json = new FixLegacyTheme().convertStrict(json, "While loading a dynamic theme") + json = new FixImages(DetermineLayout._knownImages).convertStrict(json, "While fixing the images") json = new PrepareTheme(converState).convertStrict(json, "While preparing a dynamic theme") console.log("The layoutconfig is ", json) return json diff --git a/Models/ThemeConfig/Conversion/LegacyJsonConvert.ts b/Models/ThemeConfig/Conversion/LegacyJsonConvert.ts index f601f49c4..5959790ad 100644 --- a/Models/ThemeConfig/Conversion/LegacyJsonConvert.ts +++ b/Models/ThemeConfig/Conversion/LegacyJsonConvert.ts @@ -2,7 +2,8 @@ import {LayoutConfigJson} from "../Json/LayoutConfigJson"; import {Utils} from "../../../Utils"; import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"; import {LayerConfigJson} from "../Json/LayerConfigJson"; -import {DesugaringContext, DesugaringStep, Fuse, OnEvery} from "./Conversion"; +import {DesugaringStep, Fuse, OnEvery} from "./Conversion"; +import * as metapaths from "../../../assets/layoutconfigmeta.json" export class UpdateLegacyLayer extends DesugaringStep { @@ -157,3 +158,103 @@ export class FixLegacyTheme extends Fuse { ); } } + + +export class FixImages extends DesugaringStep { + private readonly _knownImages: Set; + + constructor(knownImages: Set) { + super("Walks over the entire theme and replaces images to the relative URL. Only works if the ID of the theme is an URL"); + this._knownImages = knownImages; + } + + /** + * Walks the path into the object till the end. + * + * If a list is encountered, this is tranparently walked recursively on every object. + * + * The leaf objects are replaced + */ + private static WalkPath(path: string[], object: any, replaceLeaf: ((leaf: any) => any)) { + const head = path[0] + if (path.length === 1) { + // We have reached the leaf + const leaf = object[head]; + if (leaf !== undefined) { + object[head] = replaceLeaf(leaf) + } + return + + } + const sub = object[head] + if (sub === undefined) { + return; + } + if (typeof sub !== "object") { + return; + } + if (sub["forEach"] !== undefined) { + sub.forEach(el => FixImages.WalkPath(path.slice(1), el, replaceLeaf)) + return; + } + FixImages.WalkPath(path.slice(1), sub, replaceLeaf) + + } + + convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } { + let url: URL; + console.log("Fixing images!") + try { + url = new URL(json.id) + } catch (e) { + // Not a URL, we don't rewrite + return {result: json} + } + + const absolute = url.protocol +"//"+url.host + let relative = url.protocol +"//"+ url.host + url.pathname + relative = relative.substring(0, relative.lastIndexOf("/")) + const self = this; + + function replaceString(leaf: string) { + if (self._knownImages.has(leaf)) { + return leaf; + } + if (leaf.startsWith("./")) { + return relative + leaf.substring(1) + } + if (leaf.startsWith("/")) { + return absolute + leaf + } + return leaf; + } + + json = Utils.Clone(json) + + let paths = metapaths["default"] ?? metapaths + + for (const metapath of paths) { + if (metapath.typeHint !== "image" && metapath.typeHint !== "icon") { + continue + } + FixImages.WalkPath(metapath.path, json, leaf => { + console.log("Detected leaf: ", leaf) + if (typeof leaf === "string") { + return replaceString(leaf) + } + + if (metapath.type["some"] !== undefined && (metapath.type).some(t => t["$ref"] == "\"#/definitions/TagRenderingConfigJson\"")) { + console.log("Possibly found a tagrendering") + + } + + return leaf; + }) + } + + + return { + result: json + }; + } +} \ No newline at end of file diff --git a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts index 95bd2cd08..5170f8953 100644 --- a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts +++ b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts @@ -37,7 +37,8 @@ export default interface PointRenderingConfigJson { * * Note: strings are interpreted as icons, so layering and substituting is supported. You can use `circle:white;./my_icon.svg` to add a background circle */ - iconBadges?: { if: string | AndOrTagConfigJson, + iconBadges?: { + if: string | AndOrTagConfigJson, /** * Badge to show * Type: icon diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index fb2e03566..05a78660e 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -119,45 +119,64 @@ export default class MoreScreen extends Combine { ]).SetClass("flex flex-col border border-gray-300 p-2 rounded-lg") } + private static createButtonFor(state: UserRelatedState, id: string): BaseUIElement { + const allPreferences = state.osmConnection.preferencesHandler.preferences.data; + const length = Number(allPreferences[id + "-combined-length"]) + let str = ""; + for (let i = 0; i < length; i++) { + str += allPreferences[id + "-" + i] + } + try { + const value: { + id: string + icon: string, + title: any, + shortDescription: any + } = JSON.parse(str) + + return MoreScreen.createLinkButton(state, value, true) + } catch (e) { + console.debug("Could not parse unofficial theme information for " + id, e) + return undefined + } + } + private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState, themeListClasses): BaseUIElement { const prefix = "mapcomplete-unofficial-theme-"; - return new VariableUiElement(state.osmConnection.preferencesHandler.preferences.map(allPreferences => { - console.log("All preferences are ", allPreferences) - const allThemes: BaseUIElement[] = [] - for (const key in allPreferences) { - if (key.startsWith(prefix) && key.endsWith("-combined-length")) { - const id = key.substring(0, key.length - "-length".length) - const length = Number(allPreferences[key]) - let str = ""; - for (let i = 0; i < length; i++) { - str += allPreferences[id + "-" + i] - } - console.log("Theme " + id + " has settings ", str) - try { - const value: { - id: string - icon: string, - title: any, - shortDescription: any - } = JSON.parse(str) + var currentIds: UIEventSource = state.osmConnection.preferencesHandler.preferences + .map(allPreferences => { + const ids: string[] = [] - const link = MoreScreen.createLinkButton(state, value, true).SetClass(buttonClass) - allThemes.push(link) - } catch (e) { - console.error("Could not parse unofficial theme information for " + id, e) + for (const key in allPreferences) { + if (key.startsWith(prefix) && key.endsWith("-combined-length")) { + const id = key.substring(0, key.length - "-length".length) + ids.push(id) } } - } - if (allThemes.length <= 0) { - return undefined; - } - return new Combine([ - Translations.t.general.customThemeIntro.Clone(), - new Combine(allThemes).SetClass(themeListClasses) - ]); - })); + return ids + }); + var stableIds = UIEventSource.ListStabilized(currentIds) + + return new VariableUiElement( + stableIds.map(ids => { + const allThemes: BaseUIElement[] = [] + for (const id of ids) { + const link = this.createButtonFor(state, id) + if (link !== undefined) { + allThemes.push(link.SetClass(buttonClass)) + } + } + + if (allThemes.length <= 0) { + return undefined; + } + return new Combine([ + Translations.t.general.customThemeIntro.Clone(), + new Combine(allThemes).SetClass(themeListClasses) + ]); + })); } private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string, themeListStyle: string) { diff --git a/scripts/fixSchemas.ts b/scripts/fixSchemas.ts index 629ea434b..96ce4ed49 100644 --- a/scripts/fixSchemas.ts +++ b/scripts/fixSchemas.ts @@ -134,7 +134,7 @@ function main() { return {typeHint: type.substr("type: ".length), type: schemePart.type ?? schemePart.anyOf} }, themeSchema) - // writeFileSync("./assets/layoutconfigmeta.json",JSON.stringify(withTypes.map(({path, t}) => ({path, ...t})), null, " ")) + writeFileSync("./assets/layoutconfigmeta.json",JSON.stringify(withTypes.map(({path, t}) => ({path, ...t})), null, " ")) } diff --git a/test/LegacyThemeLoader.spec.ts b/test/LegacyThemeLoader.spec.ts index 9cc2ccf80..25b66f5b0 100644 --- a/test/LegacyThemeLoader.spec.ts +++ b/test/LegacyThemeLoader.spec.ts @@ -1,10 +1,10 @@ import T from "./TestHelper"; -import {FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; +import {FixImages, FixLegacyTheme} from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"; import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; -import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; import {TagRenderingConfigJson} from "../Models/ThemeConfig/Json/TagRenderingConfigJson"; import {AddMiniMap} from "../Models/ThemeConfig/Conversion/PrepareTheme"; import {DetectShadowedMappings} from "../Models/ThemeConfig/Conversion/Validation"; +import * as Assert from "assert"; export default class LegacyThemeLoaderSpec extends T { @@ -143,215 +143,203 @@ export default class LegacyThemeLoaderSpec extends T { ] } - private static readonly organic_waste_theme = { - "id": "recycling-organic", + private static readonly verkeerde_borden ={ + "id": "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/VerkeerdeBordenDatabank.json", "title": { - "nl": "Inzamelpunt organisch alval" - }, - "shortDescription": { - "nl": "Inzamelpunt organisch alval" + "nl": "VerkeerdeBordenDatabank", + "en": "Erratic Signs Database" }, + "maintainer": "Seppe Santens", + "icon": "https://upload.wikimedia.org/wikipedia/commons/b/bc/Belgian_traffic_sign_A51.svg", "description": { - "nl": "Op deze kaart vindt u inzamelpunten voor organisch afval. Beheer deze naar goeddunken en vermogen." + "nl": "Een kaart om verkeerde of ontbrekende verkeersborden te tonen en te editeren.", + "en": "A map to show and edit incorrect or missing traffic signs." + }, + "version": "2021-09-16", + "startLat": 51.08881, + "startLon": 3.447282, + "startZoom": 15, + "clustering": { + "maxZoom": 8 }, - "language": [ - "nl" - ], - "maintainer": "", - "icon": "https://upload.wikimedia.org/wikipedia/commons/1/15/Compost_…able_waste_-_biodegradable_waste_-_biological_waste_icon.png", - "version": "0", - "startLat": 0, - "startLon": 0, - "startZoom": 1, - "widenFactor": 0.05, - "socialImage": "", "layers": [ { - "id": "recycling-organic", + "id": "trafficsign", "name": { - "nl": "Inzamelpunt organisch alval" + "nl": "verkeersbord", + "en": "traffic sign" }, - "minzoom": 12, - "title": { - "render": { - "nl": "Inzamelpunt organisch alval" - }, - "mappings": [ - { - "if": { - "and": [ - "name~*" - ] - }, - "then": { - "nl": "{name}" - } - } - ] - }, - "allowMove": true, - "deletion": {}, - "tagRenderings": [ - "images", - { - "freeform": { - "key": "opening_hours", - "type": "opening_hours", - "addExtraTags": [] - }, - "question": { - "nl": "Wat zijn de openingsuren?" - }, - "render": { - "nl": "{opening_hours_table()}" - }, - "mappings": [ - { - "if": { - "and": [ - "opening_hours=\"by appointment\"" - ] - }, - "then": { - "nl": "Op afspraak" - } - } - ], - "id": "Composthoekjes-opening_hours" - }, - { - "question": { - "nl": "Wat is de website voor meer informatie?" - }, - "freeform": { - "key": "website", - "type": "url" - }, - "render": { - "nl": "{website}" - }, - "id": "Composthoekjes-website" - }, - { - "question": { - "nl": "Wat is het type inzamelpunt?" - }, - "mappings": [ - { - "if": "recycling_type=container", - "then": "Container of vat" - }, - { - "if": "recycling_type=centre", - "then": "Verwerkingsplaats of containerpark" - }, - { - "if": "recycling_type=dump", - "then": "Composthoop" - } - - ], - "id": "Composthoekjes-type" - }, - { - "question": { - "nl": "Wie mag hier organisch afval bezorgen?" - }, - "mappings": [ - { - "if": "access=yes", - "then": "Publiek toegankelijk" - }, - { - "if": "access=no", - "then": "Privaat" - }, - { - "if": "access=permessive", - "then": "Mogelijks toegelaten tot nader order" - }, - { - "if": "access=", - "then": "Waarschijnlijk publiek toegankelijk", - "hideInAnswer": true - }, - { - "if": "access=residents", - "then": "Bewoners van gemeente", - "hideInAnswer": "recycling_type!=centre" - } - - ], - "id": "Composthoekjes-voor-wie" - }, - { - "question": { - "nl": "Wat is de naam van dit compost-inzamelpunt?" - }, - "freeform": { - "key": "name", - "addExtraTags": ["noname="] - }, - "render": { - "nl": "De naam van dit compost-inzamelpunt is {name}" - }, - "mappings": [ - { - "if": {"and": ["noname=yes", "name="]}, - "then": "Heeft geen naam" - }, - { - "if": "name=", - "then": "Geen naam bekend", - "hideInAnswer": true - } - ], - "id": "Composthoekjes-name" - }], - "presets": [ - { - "tags": [ - "amenity=recycling", - "recycling:organic=yes" - ], - "title": { - "nl": "een inzamelpunt voor organisch afval" - } - } - ], "source": { "osmTags": { "and": [ - "recycling:organic~*" + "traffic_sign~*", + "traffic_sign:issue~*" ] } }, - "mapRendering": [ + "minzoom": 10, + "title": { + "render": { + "nl": "verkeersbord", + "en": "traffic sign" + } + }, + "tagRenderings": [ + "images", { - "icon": { - "render": "circle:white;https://upload.wikimedia.org/wikipedia/commons/1/15/Compost_…able_waste_-_biodegradable_waste_-_biological_waste_icon.png" + "render": { + "nl": "ID verkeersbord: {traffic_sign}", + "en": "traffic sign ID: {traffic_sign}" }, - "iconSize": { - "render": "40,40,center" + "question": { + "nl": "Wat is het ID voor dit verkeersbord?", + "en": "What is ID for this traffic sign?" }, - "location": [ - "point" - ] + "freeform": { + "key": "traffic_sign" + }, + "id": "trafficsign-traffic_sign" }, { - "color": { - "render": "#00f" + "render": { + "nl": "Probleem bij dit verkeersbord: {traffic_sign:issue}", + "en": "Issue with this traffic sign: {traffic_sign:issue}" }, - "width": { - "render": "8" - } + "question": { + "nl": "Wat is het probleem met dit verkeersbord?", + "en": "What is the issue with this traffic sign?" + }, + "freeform": { + "key": "traffic_sign:issue" + }, + "id": "trafficsign-traffic_sign:issue" + }, + { + "question": { + "nl": "Wanneer werd dit verkeersbord laatst gesurveyed?", + "en": "When was this traffic sign last surveyed?" + }, + "render": { + "nl": "Dit verkeersbord werd laatst gesurveyed op {survey:date}", + "en": "This traffic sign was last surveyed on {survey:date}" + }, + "freeform": { + "key": "survey:date", + "type": "date" + }, + "mappings": [ + { + "if": "survey:date:={_now:date}", + "then": "Vandaag gesurveyed!" + } + ], + "id": "trafficsign-survey:date" + } + ], + "mapRendering": [ + { + "icon": "./TS_bolt.svg", + "location": [ + "point", + "centroid" + ] + } + ] + }, + { + "id": "notrafficsign", + "name": { + "nl": "geen verkeersbord", + "en": "no traffic sign" + }, + "source": { + "osmTags": { + "and": [ + { + "or": [ + "no:traffic_sign~*", + "not:traffic_sign~*" + ] + }, + "traffic_sign:issue~*" + ] + } + }, + "minzoom": 10, + "title": { + "render": { + "nl": "ontbrekend verkeersbord", + "en": "missing traffic sign" + } + }, + "tagRenderings": [ + "images", + { + "render": { + "nl": "ID ontbrekend verkeersbord: {no:traffic_sign}", + "en": "missing traffic sign ID: {no:traffic_sign}" + }, + "question": { + "nl": "Wat is het ID voor het ontbrekende verkeersbord?", + "en": "What is ID for the missing traffic sign?" + }, + "freeform": { + "key": "no:traffic_sign" + }, + "id": "notrafficsign-no:traffic_sign" + }, + { + "render": { + "nl": "Probleem bij deze situatie: {traffic_sign:issue}", + "en": "Issue with this situation: {traffic_sign:issue}" + }, + "question": { + "nl": "Wat is er mis met deze situatie?", + "en": "What is the issue with this situation?" + }, + "freeform": { + "key": "traffic_sign:issue" + }, + "id": "notrafficsign-traffic_sign:issue" + }, + { + "question": { + "nl": "Wanneer werd deze situatie laatst gesurveyed?", + "en": "When was this situation last surveyed?" + }, + "render": { + "nl": "Deze situatie werd laatst gesurveyed op {survey:date}", + "en": "This situation was last surveyed on {survey:date}" + }, + "freeform": { + "key": "survey:date", + "type": "date" + }, + "mappings": [ + { + "if": "survey:date:={_now:date}", + "then": "Vandaag gesurveyed!" + } + ], + "id": "notrafficsign-survey:date" + } + ], + "mapRendering": [ + { + "icon": "./TS_questionmark.svg", + "location": [ + "point", + "centroid" + ] } ] } - ] + ], + "defaultBackgroundId": "Stamen.TonerLite" } + constructor() { super([ ["Walking_node_theme", () => { @@ -439,8 +427,13 @@ export default class LegacyThemeLoaderSpec extends T { }, "test"); T.isTrue(r0.errors.length > 0, "Failing case is not detected") } - - ] + ], + ["Images are rewritten", () => { + const fixed = new FixImages(new Set()).convertStrict(LegacyThemeLoaderSpec.verkeerde_borden, "test") + const fixedValue = fixed.layers[0]["mapRendering"][0].icon + Assert.equal("https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/TS_bolt.svg", + fixedValue) + } ] ] ); } diff --git a/theme.html b/theme.html index 95f2e8935..4017bc3d4 100644 --- a/theme.html +++ b/theme.html @@ -37,14 +37,7 @@ - - - + @@ -70,13 +63,15 @@
-

Loading MapComplete, hang on...

-

Powered by OpenStreetMap

+

Loading MapComplete, hang on...

+

Powered by OpenStreetMap

Below
+ +