forked from MapComplete/MapComplete
		
	Refactoring: move special visualisations into multiple files
This commit is contained in:
		
							parent
							
								
									f057b0f358
								
							
						
					
					
						commit
						b9bc3f5980
					
				
					 14 changed files with 839 additions and 801 deletions
				
			
		|  | @ -136,6 +136,11 @@ export class GenerateDocs extends Script { | ||||||
|     async main(args: string[]) { |     async main(args: string[]) { | ||||||
|         console.log("Starting documentation generation...") |         console.log("Starting documentation generation...") | ||||||
|         ScriptUtils.fixUtils() |         ScriptUtils.fixUtils() | ||||||
|  | 
 | ||||||
|  |         this.WriteMarkdownFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage(), [ | ||||||
|  |             "src/UI/SpecialVisualizations.ts" | ||||||
|  |         ]) | ||||||
|  | 
 | ||||||
|         if (!existsSync("./Docs/Themes")) { |         if (!existsSync("./Docs/Themes")) { | ||||||
|             mkdirSync("./Docs/Themes") |             mkdirSync("./Docs/Themes") | ||||||
|         } |         } | ||||||
|  | @ -166,13 +171,12 @@ export class GenerateDocs extends Script { | ||||||
|             ScriptUtils.erasableLog("Written docs for theme", theme.id) |             ScriptUtils.erasableLog("Written docs for theme", theme.id) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         this.WriteMarkdownFile("./Docs/SpecialRenderings.md", SpecialVisualizations.HelpMessage(), [ | 
 | ||||||
|             "src/UI/SpecialVisualizations.ts", |  | ||||||
|         ]) |  | ||||||
|         this.WriteMarkdownFile( |         this.WriteMarkdownFile( | ||||||
|             "./Docs/CalculatedTags.md", |             "./Docs/CalculatedTags.md", | ||||||
|             ["# Metatags", SimpleMetaTaggers.HelpText(), ExtraFunctions.HelpText()].join("\n"), |             ["# Metatags", SimpleMetaTaggers.HelpText(), ExtraFunctions.HelpText()].join("\n"), | ||||||
|             ["src/Logic/SimpleMetaTagger.ts", "src/Logic/ExtraFunctions.ts"] |             ["src/Logic/SimpleMetaTagger.ts", "src/Logic/ExtraFunctions.ts"], | ||||||
|  |             { noTableOfContents: false } | ||||||
|         ) |         ) | ||||||
|         this.WriteMarkdownFile("./Docs/SpecialInputElements.md", Validators.HelpText(), [ |         this.WriteMarkdownFile("./Docs/SpecialInputElements.md", Validators.HelpText(), [ | ||||||
|             "src/UI/InputElement/Validators.ts", |             "src/UI/InputElement/Validators.ts", | ||||||
|  | @ -212,7 +216,7 @@ export class GenerateDocs extends Script { | ||||||
|         markdown: string, |         markdown: string, | ||||||
|         autogenSource: string[], |         autogenSource: string[], | ||||||
|         options?: { |         options?: { | ||||||
|             noTableOfContents: boolean |             noTableOfContents?: boolean | ||||||
|         } |         } | ||||||
|     ): void { |     ): void { | ||||||
|         for (const source of autogenSource) { |         for (const source of autogenSource) { | ||||||
|  |  | ||||||
|  | @ -1,42 +1,14 @@ | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ | /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ | ||||||
| export class ThemeMetaTagging { | export class ThemeMetaTagging { | ||||||
|     public static readonly themeName = "usersettings" |    public static readonly themeName = "usersettings" | ||||||
| 
 | 
 | ||||||
|     public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) { |    public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) { | ||||||
|         Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => |       Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )  | ||||||
|             feat.properties._description |       Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' )  | ||||||
|                 .match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) |       Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href   }) (feat)  )  | ||||||
|                 ?.at(1) |       Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat)  )  | ||||||
|         ) |       Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )  | ||||||
|         Utils.AddLazyProperty( |       feat.properties['__current_backgroun'] = 'initial_value' | ||||||
|             feat.properties, |    } | ||||||
|             "_d", | } | ||||||
|             () => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? "" |  | ||||||
|         ) |  | ||||||
|         Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () => |  | ||||||
|             ((feat) => { |  | ||||||
|                 const e = document.createElement("div") |  | ||||||
|                 e.innerHTML = feat.properties._d |  | ||||||
|                 return Array.from(e.getElementsByTagName("a")).filter( |  | ||||||
|                     (a) => a.href.match(/mastodon|en.osm.town/) !== null |  | ||||||
|                 )[0]?.href |  | ||||||
|             })(feat) |  | ||||||
|         ) |  | ||||||
|         Utils.AddLazyProperty(feat.properties, "_mastodon_link", () => |  | ||||||
|             ((feat) => { |  | ||||||
|                 const e = document.createElement("div") |  | ||||||
|                 e.innerHTML = feat.properties._d |  | ||||||
|                 return Array.from(e.getElementsByTagName("a")).filter( |  | ||||||
|                     (a) => a.getAttribute("rel")?.indexOf("me") >= 0 |  | ||||||
|                 )[0]?.href |  | ||||||
|             })(feat) |  | ||||||
|         ) |  | ||||||
|         Utils.AddLazyProperty( |  | ||||||
|             feat.properties, |  | ||||||
|             "_mastodon_candidate", |  | ||||||
|             () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a |  | ||||||
|         ) |  | ||||||
|         feat.properties["__current_backgroun"] = "initial_value" |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -24,6 +24,8 @@ export interface ConflateFlowArguments extends ImportFlowArguments { | ||||||
| export default class ConflateImportButtonViz implements SpecialVisualization, AutoAction { | export default class ConflateImportButtonViz implements SpecialVisualization, AutoAction { | ||||||
|     supportsAutoAction: boolean = true |     supportsAutoAction: boolean = true | ||||||
|     needsUrls = [] |     needsUrls = [] | ||||||
|  |     group = "data_import" | ||||||
|  | 
 | ||||||
|     public readonly funcName: string = "conflate_button" |     public readonly funcName: string = "conflate_button" | ||||||
|     public readonly args: { |     public readonly args: { | ||||||
|         name: string |         name: string | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ import { Feature, Point } from "geojson" | ||||||
| import { UIEventSource } from "../../../Logic/UIEventSource" | import { UIEventSource } from "../../../Logic/UIEventSource" | ||||||
| import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization" | ||||||
| import BaseUIElement from "../../BaseUIElement" | import BaseUIElement from "../../BaseUIElement" | ||||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" |  | ||||||
| import SvelteUIElement from "../../Base/SvelteUIElement" | import SvelteUIElement from "../../Base/SvelteUIElement" | ||||||
| import PointImportFlow from "./PointImportFlow.svelte" | import PointImportFlow from "./PointImportFlow.svelte" | ||||||
| import { PointImportFlowArguments, PointImportFlowState } from "./PointImportFlowState" | import { PointImportFlowArguments, PointImportFlowState } from "./PointImportFlowState" | ||||||
|  | @ -20,9 +19,11 @@ export class PointImportButtonViz implements SpecialVisualization { | ||||||
|     public readonly example?: string |     public readonly example?: string | ||||||
|     public readonly args: { name: string; defaultValue?: string; doc: string; split?: boolean }[] |     public readonly args: { name: string; defaultValue?: string; doc: string; split?: boolean }[] | ||||||
|     public needsUrls = [] |     public needsUrls = [] | ||||||
|  |     group = "data_import" | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
|         this.funcName = "import_button" |         this.funcName = "import_button" | ||||||
|  | 
 | ||||||
|         this.docs = |         this.docs = | ||||||
|             "This button will copy the point from an external dataset into OpenStreetMap" + |             "This button will copy the point from an external dataset into OpenStreetMap" + | ||||||
|             ImportFlowUtils.documentationGeneral |             ImportFlowUtils.documentationGeneral | ||||||
|  |  | ||||||
|  | @ -21,6 +21,8 @@ import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSou | ||||||
| export default class WayImportButtonViz implements AutoAction, SpecialVisualization { | export default class WayImportButtonViz implements AutoAction, SpecialVisualization { | ||||||
|     public readonly funcName: string = "import_way_button" |     public readonly funcName: string = "import_way_button" | ||||||
|     needsUrls = [] |     needsUrls = [] | ||||||
|  |     group = "data_import" | ||||||
|  | 
 | ||||||
|     public readonly docs: string = |     public readonly docs: string = | ||||||
|         "This button will copy the data from an external dataset into OpenStreetMap, copying the geometry and adding it as a 'line'" + |         "This button will copy the data from an external dataset into OpenStreetMap, copying the geometry and adding it as a 'line'" + | ||||||
|         ImportFlowUtils.documentationGeneral |         ImportFlowUtils.documentationGeneral | ||||||
|  |  | ||||||
|  | @ -1,13 +1,14 @@ | ||||||
| import { GeoOperations } from "../../Logic/GeoOperations" | import { GeoOperations } from "../../Logic/GeoOperations" | ||||||
| import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource" | import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | import { SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
| import BaseUIElement from "../BaseUIElement" |  | ||||||
| import SvelteUIElement from "../Base/SvelteUIElement" | import SvelteUIElement from "../Base/SvelteUIElement" | ||||||
| import MapillaryLink from "../BigComponents/MapillaryLink.svelte" | import MapillaryLink from "../BigComponents/MapillaryLink.svelte" | ||||||
| 
 | 
 | ||||||
| export class MapillaryLinkVis implements SpecialVisualization { | export class MapillaryLinkVis implements SpecialVisualizationSvelte { | ||||||
|     funcName = "mapillary_link" |     funcName = "mapillary_link" | ||||||
|  |     group = "web_and_communication" | ||||||
|  | 
 | ||||||
|     docs = "Adds a button to open mapillary on the specified location" |     docs = "Adds a button to open mapillary on the specified location" | ||||||
|     needsUrls = [] |     needsUrls = [] | ||||||
| 
 | 
 | ||||||
|  | @ -24,7 +25,7 @@ export class MapillaryLinkVis implements SpecialVisualization { | ||||||
|         tagsSource: UIEventSource<Record<string, string>>, |         tagsSource: UIEventSource<Record<string, string>>, | ||||||
|         args: string[], |         args: string[], | ||||||
|         feature: Feature |         feature: Feature | ||||||
|     ): BaseUIElement { |     ): SvelteUIElement { | ||||||
|         const [lon, lat] = GeoOperations.centerpointCoordinates(feature) |         const [lon, lat] = GeoOperations.centerpointCoordinates(feature) | ||||||
|         let zoom = Number(args[0]) |         let zoom = Number(args[0]) | ||||||
|         if (isNaN(zoom)) { |         if (isNaN(zoom)) { | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ import { default as PlantNetCode } from "../../Logic/Web/PlantNet" | ||||||
| export class PlantNetDetectionViz implements SpecialVisualization { | export class PlantNetDetectionViz implements SpecialVisualization { | ||||||
|     funcName = "plantnet_detection" |     funcName = "plantnet_detection" | ||||||
|     needsUrls = [PlantNetCode.baseUrl] |     needsUrls = [PlantNetCode.baseUrl] | ||||||
|  |     group = "data_import" | ||||||
| 
 | 
 | ||||||
|     docs = |     docs = | ||||||
|         "Sends the images linked to the current object to plantnet.org and asks it what plant species is shown on it. The user can then select the correct species; the corresponding wikidata-identifier will then be added to the object (together with `source:species:wikidata=plantnet.org AI`). " |         "Sends the images linked to the current object to plantnet.org and asks it what plant species is shown on it. The user can then select the correct species; the corresponding wikidata-identifier will then be added to the object (together with `source:species:wikidata=plantnet.org AI`). " | ||||||
|  |  | ||||||
|  | @ -20,6 +20,8 @@ import Icon from "../Map/Icon.svelte" | ||||||
| export default class TagApplyButton implements AutoAction, SpecialVisualization { | export default class TagApplyButton implements AutoAction, SpecialVisualization { | ||||||
|     public readonly funcName = "tag_apply" |     public readonly funcName = "tag_apply" | ||||||
|     needsUrls = [] |     needsUrls = [] | ||||||
|  |     group = "data_import" | ||||||
|  | 
 | ||||||
|     public readonly docs = |     public readonly docs = | ||||||
|         "Shows a big button; clicking this button will apply certain tags onto the feature.\n\nThe first argument takes a specification of which tags to add.\n" + |         "Shows a big button; clicking this button will apply certain tags onto the feature.\n\nThe first argument takes a specification of which tags to add.\n" + | ||||||
|         Utils.Special_visualizations_tagsToApplyHelpText |         Utils.Special_visualizations_tagsToApplyHelpText | ||||||
|  |  | ||||||
|  | @ -1,14 +1,33 @@ | ||||||
| import { SpecialVisualizationSvelte } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
| import Maproulette from "../../Logic/Maproulette" | import Maproulette from "../../Logic/Maproulette" | ||||||
| import SvelteUIElement from "../Base/SvelteUIElement" | import SvelteUIElement from "../Base/SvelteUIElement" | ||||||
| import MaprouletteSetStatus from "../MapRoulette/MaprouletteSetStatus.svelte" | import MaprouletteSetStatus from "../MapRoulette/MaprouletteSetStatus.svelte" | ||||||
|  | import TagApplyButton from "../Popup/TagApplyButton" | ||||||
|  | import { PointImportButtonViz } from "../Popup/ImportButtons/PointImportButtonViz" | ||||||
|  | import WayImportButtonViz from "../Popup/ImportButtons/WayImportButtonViz" | ||||||
|  | import ConflateImportButtonViz from "../Popup/ImportButtons/ConflateImportButtonViz" | ||||||
|  | import { PlantNetDetectionViz } from "../Popup/PlantNetDetectionViz" | ||||||
|  | import Constants from "../../Models/Constants" | ||||||
|  | import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource" | ||||||
|  | import { Feature, GeoJsonProperties } from "geojson" | ||||||
|  | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
|  | import BaseUIElement from "../BaseUIElement" | ||||||
|  | import LinkedDataLoader from "../../Logic/Web/LinkedDataLoader" | ||||||
|  | import Toggle from "../Input/Toggle" | ||||||
|  | import ComparisonTool from "../Comparison/ComparisonTool.svelte" | ||||||
|  | import { Utils } from "../../Utils" | ||||||
| 
 | 
 | ||||||
| export class MapRouletteSpecialVisualisations { | export class DataImportSpecialVisualisations { | ||||||
|   public static initList(): SpecialVisualizationSvelte[] { |     public static initList(): (SpecialVisualization & { group })[] { | ||||||
|     return [ |     return [ | ||||||
|       { |         new TagApplyButton(), | ||||||
|  |         new PointImportButtonViz(), | ||||||
|  |         new WayImportButtonViz(), | ||||||
|  |         new ConflateImportButtonViz(), | ||||||
|  |         new PlantNetDetectionViz(), | ||||||
|  |         { | ||||||
|         funcName: "maproulette_set_status", |         funcName: "maproulette_set_status", | ||||||
|         group: "maproulette", |             group: "data_import", | ||||||
|         docs: "Change the status of the given MapRoulette task", |         docs: "Change the status of the given MapRoulette task", | ||||||
|         needsUrls: [Maproulette.defaultEndpoint], |         needsUrls: [Maproulette.defaultEndpoint], | ||||||
|         example: |         example: | ||||||
|  | @ -87,6 +106,186 @@ export class MapRouletteSpecialVisualisations { | ||||||
|             askFeedback |             askFeedback | ||||||
|           }) |           }) | ||||||
|         } |         } | ||||||
|       }] |         }, | ||||||
|  |         { | ||||||
|  |             funcName: "linked_data_from_website", | ||||||
|  |             group: "data_import", | ||||||
|  |             docs: "Attempts to load (via a proxy) the specified website and parsed ld+json from there. Suitable data will be offered to import into OSM. Note: this element is added by default", | ||||||
|  |             args: [ | ||||||
|  |                 { | ||||||
|  |                     name: "key", | ||||||
|  |                     defaultValue: "website", | ||||||
|  |                     doc: "Attempt to load ld+json from the specified URL. This can be in an embedded <script type='ld+json'>" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: "useProxy", | ||||||
|  |                     defaultValue: "yes", | ||||||
|  |                     doc: "If 'yes', uses the provided proxy server. This proxy server will scrape HTML and search for a script with `lang='ld+json'`. If `no`, the data will be downloaded and expects a linked-data-json directly" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: "host", | ||||||
|  |                     doc: "If not using a proxy, define what host the website is allowed to connect to" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: "mode", | ||||||
|  |                     doc: "If `display`, only show the data in tabular and readonly form, ignoring already existing tags. This is used to explicitly show all the tags. If unset or anything else, allow to apply/import on OSM" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: "collapsed", | ||||||
|  |                     defaultValue: "yes", | ||||||
|  |                     doc: "If the containing accordion should be closed" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             needsUrls: [Constants.linkedDataProxy, "http://www.schema.org"], | ||||||
|  |             constr( | ||||||
|  |                 state: SpecialVisualizationState, | ||||||
|  |                 tags: UIEventSource<Record<string, string>>, | ||||||
|  |                 argument: string[], | ||||||
|  |                 feature: Feature, | ||||||
|  |                 layer: LayerConfig | ||||||
|  |             ): BaseUIElement { | ||||||
|  |                 const key = argument[0] ?? "website" | ||||||
|  |                 const useProxy = argument[1] !== "no" | ||||||
|  |                 const readonly = argument[3] === "readonly" | ||||||
|  |                 const isClosed = (argument[4] ?? "yes") === "yes" | ||||||
|  | 
 | ||||||
|  |                 const countryStore: Store<string | undefined> = tags.mapD( | ||||||
|  |                     (tags) => tags._country | ||||||
|  |                 ) | ||||||
|  |                 const sourceUrl: Store<string | undefined> = tags.mapD((tags) => { | ||||||
|  |                     if (!tags[key] || tags[key] === "undefined") { | ||||||
|  |                         return null | ||||||
|  |                     } | ||||||
|  |                     return tags[key] | ||||||
|  |                 }) | ||||||
|  |                 const externalData: Store<{ success: GeoJsonProperties } | { error }> = | ||||||
|  |                     sourceUrl.bindD( | ||||||
|  |                         (url) => { | ||||||
|  |                             const country = countryStore.data | ||||||
|  |                             if (url.startsWith("https://data.velopark.be/")) { | ||||||
|  |                                 return Stores.FromPromiseWithErr( | ||||||
|  |                                     (async () => { | ||||||
|  |                                         try { | ||||||
|  |                                             const loadAll = | ||||||
|  |                                                 layer.id.toLowerCase().indexOf("maproulette") >= | ||||||
|  |                                                 0 // Dirty hack
 | ||||||
|  |                                             const features = | ||||||
|  |                                                 await LinkedDataLoader.fetchVeloparkEntry( | ||||||
|  |                                                     url, | ||||||
|  |                                                     loadAll | ||||||
|  |                                                 ) | ||||||
|  |                                             const feature = | ||||||
|  |                                                 features.find( | ||||||
|  |                                                     (f) => f.properties["ref:velopark"] === url | ||||||
|  |                                                 ) ?? features[0] | ||||||
|  |                                             const properties = feature.properties | ||||||
|  |                                             properties["ref:velopark"] = url | ||||||
|  |                                             console.log( | ||||||
|  |                                                 "Got properties from velopark:", | ||||||
|  |                                                 properties | ||||||
|  |                                             ) | ||||||
|  |                                             return properties | ||||||
|  |                                         } catch (e) { | ||||||
|  |                                             console.error(e) | ||||||
|  |                                             throw e | ||||||
|  |                                         } | ||||||
|  |                                     })() | ||||||
|  |                                 ) | ||||||
|  |                             } | ||||||
|  |                             if (country === undefined) { | ||||||
|  |                                 return undefined | ||||||
|  |                             } | ||||||
|  |                             return Stores.FromPromiseWithErr( | ||||||
|  |                                 (async () => { | ||||||
|  |                                     try { | ||||||
|  |                                         return await LinkedDataLoader.fetchJsonLd( | ||||||
|  |                                             url, | ||||||
|  |                                             { country }, | ||||||
|  |                                             useProxy ? "proxy" : "fetch-lod" | ||||||
|  |                                         ) | ||||||
|  |                                     } catch (e) { | ||||||
|  |                                         console.log( | ||||||
|  |                                             "Could not get with proxy/download LOD, attempting to download directly. Error for ", | ||||||
|  |                                             url, | ||||||
|  |                                             "is", | ||||||
|  |                                             e | ||||||
|  |                                         ) | ||||||
|  |                                         return await LinkedDataLoader.fetchJsonLd( | ||||||
|  |                                             url, | ||||||
|  |                                             { country }, | ||||||
|  |                                             "fetch-raw" | ||||||
|  |                                         ) | ||||||
|  |                                     } | ||||||
|  |                                 })() | ||||||
|  |                             ) | ||||||
|  |                         }, | ||||||
|  |                         [countryStore] | ||||||
|  |                     ) | ||||||
|  | 
 | ||||||
|  |                 externalData.addCallbackAndRunD((lod) => | ||||||
|  |                     console.log("linked_data_from_website received the following data:", lod) | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |                 return new Toggle( | ||||||
|  |                     new SvelteUIElement(ComparisonTool, { | ||||||
|  |                         feature, | ||||||
|  |                         state, | ||||||
|  |                         tags, | ||||||
|  |                         layer, | ||||||
|  |                         externalData, | ||||||
|  |                         sourceUrl, | ||||||
|  |                         readonly, | ||||||
|  |                         collapsed: isClosed | ||||||
|  |                     }), | ||||||
|  |                     undefined, | ||||||
|  |                     sourceUrl.map((url) => !!url) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             funcName: "compare_data", | ||||||
|  |             group: "data_import", | ||||||
|  |             needsUrls: (args) => args[1].split(";"), | ||||||
|  |             args: [ | ||||||
|  |                 { | ||||||
|  |                     name: "url", | ||||||
|  |                     required: true, | ||||||
|  |                     doc: "The attribute containing the url where to fetch more data" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: "host", | ||||||
|  |                     required: true, | ||||||
|  |                     doc: "The domain name(s) where data might be fetched from - this is needed to set the CSP. A domain must include 'https', e.g. 'https://example.com'. For multiple domains, separate them with ';'. If you don't know the possible domains, use '*'. " | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: "readonly", | ||||||
|  |                     required: false, | ||||||
|  |                     doc: "If 'yes', will not show 'apply'-buttons" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             docs: "Gives an interactive element which shows a tag comparison between the OSM-object and the upstream object. This allows to copy some or all tags into OSM", | ||||||
|  |             constr( | ||||||
|  |                 state: SpecialVisualizationState, | ||||||
|  |                 tagSource: UIEventSource<Record<string, string>>, | ||||||
|  |                 args: string[], | ||||||
|  |                 feature: Feature, | ||||||
|  |                 layer: LayerConfig | ||||||
|  |             ): BaseUIElement { | ||||||
|  |                 const url = args[0] | ||||||
|  |                 const readonly = args[3] === "yes" | ||||||
|  |                 const externalData = Stores.FromPromiseWithErr(Utils.downloadJson(url)) | ||||||
|  |                 return new SvelteUIElement(ComparisonTool, { | ||||||
|  |                     url, | ||||||
|  |                     state, | ||||||
|  |                     tags: tagSource, | ||||||
|  |                     layer, | ||||||
|  |                     feature, | ||||||
|  |                     readonly, | ||||||
|  |                     externalData | ||||||
|  |                 }) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     ] | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization" | ||||||
| import Constants from "../../Models/Constants" | import Constants from "../../Models/Constants" | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" | import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
|  | @ -11,6 +11,9 @@ import Translations from "../i18n/Translations" | ||||||
| import AddNoteComment from "../Popup/Notes/AddNoteComment.svelte" | import AddNoteComment from "../Popup/Notes/AddNoteComment.svelte" | ||||||
| import { Imgur } from "../../Logic/ImageProviders/Imgur" | import { Imgur } from "../../Logic/ImageProviders/Imgur" | ||||||
| import UploadImage from "../Image/UploadImage.svelte" | import UploadImage from "../Image/UploadImage.svelte" | ||||||
|  | import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
|  | import Combine from "../Base/Combine" | ||||||
|  | import NoteCommentElement from "../Popup/Notes/NoteCommentElement.svelte" | ||||||
| 
 | 
 | ||||||
| class CloseNoteViz implements SpecialVisualizationSvelte { | class CloseNoteViz implements SpecialVisualizationSvelte { | ||||||
|     public readonly funcName = "close_note" |     public readonly funcName = "close_note" | ||||||
|  | @ -93,8 +96,9 @@ class AddNoteCommentViz implements SpecialVisualizationSvelte { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| export class NoteVisualisations { | export class NoteVisualisations { | ||||||
|     public static initList(): SpecialVisualizationSvelte[] { |     public static initList(): (SpecialVisualization & { group })[] { | ||||||
|         return [new AddNoteCommentViz(), |         return [new AddNoteCommentViz(), | ||||||
|  |             new CloseNoteViz(), | ||||||
|             { |             { | ||||||
|                 funcName: "open_note", |                 funcName: "open_note", | ||||||
|                 args: [], |                 args: [], | ||||||
|  | @ -133,7 +137,47 @@ export class NoteVisualisations { | ||||||
|                     return new SvelteUIElement(UploadImage, { state, tags, layer, feature }) |                     return new SvelteUIElement(UploadImage, { state, tags, layer, feature }) | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             new CloseNoteViz() |             { | ||||||
|  |                 funcName: "visualize_note_comments", | ||||||
|  |                 group: "notes", | ||||||
|  |                 docs: "Visualises the comments for notes", | ||||||
|  |                 args: [ | ||||||
|  |                     { | ||||||
|  |                         name: "commentsKey", | ||||||
|  |                         doc: "The property name of the comments, which should be stringified json", | ||||||
|  |                         defaultValue: "comments" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "start", | ||||||
|  |                         doc: "Drop the first 'start' comments", | ||||||
|  |                         defaultValue: "0" | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  |                 needsUrls: [Constants.osmAuthConfig.url], | ||||||
|  |                 constr: (state, tags, args) => | ||||||
|  |                     new VariableUiElement( | ||||||
|  |                         tags | ||||||
|  |                             .map((tags) => tags[args[0]]) | ||||||
|  |                             .map((commentsStr) => { | ||||||
|  |                                 const comments: { text: string }[] = JSON.parse(commentsStr) | ||||||
|  |                                 const startLoc = Number(args[1] ?? 0) | ||||||
|  |                                 if (!isNaN(startLoc) && startLoc > 0) { | ||||||
|  |                                     comments.splice(0, startLoc) | ||||||
|  |                                 } | ||||||
|  |                                 return new Combine( | ||||||
|  |                                     comments | ||||||
|  |                                         .filter((c) => c.text !== "") | ||||||
|  |                                         .map( | ||||||
|  |                                             (comment) => | ||||||
|  |                                                 new SvelteUIElement(NoteCommentElement, { | ||||||
|  |                                                     comment, | ||||||
|  |                                                     state | ||||||
|  |                                                 }) | ||||||
|  |                                         ) | ||||||
|  |                                 ).SetClass("flex flex-col") | ||||||
|  |                             }) | ||||||
|  |                     ) | ||||||
|  |             } | ||||||
|         ] |         ] | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
| import { MangroveReviews } from "mangrove-reviews-typescript" | import { MangroveReviews } from "mangrove-reviews-typescript" | ||||||
| import FeatureReviews from "../../Logic/Web/MangroveReviews" | import FeatureReviews from "../../Logic/Web/MangroveReviews" | ||||||
| import SvelteUIElement from "../Base/SvelteUIElement" | import SvelteUIElement from "../Base/SvelteUIElement" | ||||||
|  | @ -7,121 +7,128 @@ import ReviewForm from "../Reviews/ReviewForm.svelte" | ||||||
| import AllReviews from "../Reviews/AllReviews.svelte" | import AllReviews from "../Reviews/AllReviews.svelte" | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" | import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import ImportReviewIdentity from "../Reviews/ImportReviewIdentity.svelte" | import ImportReviewIdentity from "../Reviews/ImportReviewIdentity.svelte" | ||||||
|  | import { Feature } from "geojson" | ||||||
|  | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
|  | import BaseUIElement from "../BaseUIElement" | ||||||
|  | import Combine from "../Base/Combine" | ||||||
| 
 | 
 | ||||||
| export class ReviewSpecialVisualisations { | export class ReviewSpecialVisualisations { | ||||||
|     public static initList(): SpecialVisualizationSvelte[] { |     public static initList(): (SpecialVisualization & { group })[] { | ||||||
|         return [{ |         const createReview: SpecialVisualization & { group } = { | ||||||
|             funcName: "rating", |             funcName: "create_review", | ||||||
|             group: "reviews", |             group: "reviews", | ||||||
|             docs: "Shows stars which represent the average rating on mangrove.", | 
 | ||||||
|  |             docs: "Invites the contributor to leave a review. Somewhat small UI-element until interacted", | ||||||
|             needsUrls: [MangroveReviews.ORIGINAL_API], |             needsUrls: [MangroveReviews.ORIGINAL_API], | ||||||
|             args: [ |             args: [ | ||||||
|                 { |                 { | ||||||
|                     name: "subjectKey", |                     name: "subjectKey", | ||||||
|                     defaultValue: "name", |                     defaultValue: "name", | ||||||
|                     doc: "The key to use to determine the subject. If the value is specified, the subject will be <b>tags[subjectKey]</b> and will use this to filter the reviews." |                     doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>" | ||||||
|                 }, |                 }, | ||||||
|                 { |                 { | ||||||
|                     name: "fallback", |                     name: "fallback", | ||||||
|                     doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value" |                     doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: "question", | ||||||
|  |                     doc: "The question to ask during the review" | ||||||
|                 } |                 } | ||||||
|             ], |             ], | ||||||
|             constr: (state, tags, args, feature) => { |             constr: (state, tags, args, feature, layer) => { | ||||||
|                 const nameKey = args[0] ?? "name" |                 const nameKey = args[0] ?? "name" | ||||||
|                 const fallbackName = args[1] |                 const fallbackName = args[1] | ||||||
|  |                 const question = args[2] | ||||||
|                 const reviews = FeatureReviews.construct( |                 const reviews = FeatureReviews.construct( | ||||||
|                     feature, |                     feature, | ||||||
|                     tags, |                     tags, | ||||||
|                     state.userRelatedState.mangroveIdentity, |                     state.userRelatedState?.mangroveIdentity, | ||||||
|                     { |                     { | ||||||
|                         nameKey: nameKey, |                         nameKey: nameKey, | ||||||
|                         fallbackName |                         fallbackName | ||||||
|                     }, |                     }, | ||||||
|                     state.featureSwitchIsTesting |                     state.featureSwitchIsTesting | ||||||
|                 ) |                 ) | ||||||
|                 return new SvelteUIElement(StarsBarIcon, { |                 return new SvelteUIElement(ReviewForm, { | ||||||
|                     score: reviews.average |                     reviews, | ||||||
|  |                     state, | ||||||
|  |                     tags, | ||||||
|  |                     feature, | ||||||
|  |                     layer, | ||||||
|  |                     question | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
|         }, |         } | ||||||
|             { |         const listReviews: SpecialVisualization & { group } = { | ||||||
|                 funcName: "create_review", |             funcName: "list_reviews", | ||||||
|                 group: "reviews", |             group: "reviews", | ||||||
| 
 | 
 | ||||||
|                 docs: "Invites the contributor to leave a review. Somewhat small UI-element until interacted", |             docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten", | ||||||
|  |             needsUrls: [MangroveReviews.ORIGINAL_API], | ||||||
|  |             args: [ | ||||||
|  |                 { | ||||||
|  |                     name: "subjectKey", | ||||||
|  |                     defaultValue: "name", | ||||||
|  |                     doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     name: "fallback", | ||||||
|  |                     doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             constr: (state, tags, args, feature, layer) => { | ||||||
|  |                 const nameKey = args[0] ?? "name" | ||||||
|  |                 const fallbackName = args[1] | ||||||
|  |                 const reviews = FeatureReviews.construct( | ||||||
|  |                     feature, | ||||||
|  |                     tags, | ||||||
|  |                     state.userRelatedState?.mangroveIdentity, | ||||||
|  |                     { | ||||||
|  |                         nameKey: nameKey, | ||||||
|  |                         fallbackName | ||||||
|  |                     }, | ||||||
|  |                     state.featureSwitchIsTesting | ||||||
|  |                 ) | ||||||
|  |                 return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer }) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return [ | ||||||
|  |             { | ||||||
|  |                 funcName: "rating", | ||||||
|  |                 group: "reviews", | ||||||
|  |                 docs: "Shows stars which represent the average rating on mangrove.", | ||||||
|                 needsUrls: [MangroveReviews.ORIGINAL_API], |                 needsUrls: [MangroveReviews.ORIGINAL_API], | ||||||
|                 args: [ |                 args: [ | ||||||
|                     { |                     { | ||||||
|                         name: "subjectKey", |                         name: "subjectKey", | ||||||
|                         defaultValue: "name", |                         defaultValue: "name", | ||||||
|                         doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>" |                         doc: "The key to use to determine the subject. If the value is specified, the subject will be <b>tags[subjectKey]</b> and will use this to filter the reviews." | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         name: "fallback", |                         name: "fallback", | ||||||
|                         doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value" |                         doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value" | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "question", |  | ||||||
|                         doc: "The question to ask during the review" |  | ||||||
|                     } |                     } | ||||||
|                 ], |                 ], | ||||||
|                 constr: (state, tags, args, feature, layer) => { |                 constr: (state, tags, args, feature) => { | ||||||
|                     const nameKey = args[0] ?? "name" |                     const nameKey = args[0] ?? "name" | ||||||
|                     const fallbackName = args[1] |                     const fallbackName = args[1] | ||||||
|                     const question = args[2] |  | ||||||
|                     const reviews = FeatureReviews.construct( |                     const reviews = FeatureReviews.construct( | ||||||
|                         feature, |                         feature, | ||||||
|                         tags, |                         tags, | ||||||
|                         state.userRelatedState?.mangroveIdentity, |                         state.userRelatedState.mangroveIdentity, | ||||||
|                         { |                         { | ||||||
|                             nameKey: nameKey, |                             nameKey: nameKey, | ||||||
|                             fallbackName |                             fallbackName | ||||||
|                         }, |                         }, | ||||||
|                         state.featureSwitchIsTesting |                         state.featureSwitchIsTesting | ||||||
|                     ) |                     ) | ||||||
|                     return new SvelteUIElement(ReviewForm, { |                     return new SvelteUIElement(StarsBarIcon, { | ||||||
|                         reviews, |                         score: reviews.average | ||||||
|                         state, |  | ||||||
|                         tags, |  | ||||||
|                         feature, |  | ||||||
|                         layer, |  | ||||||
|                         question |  | ||||||
|                     }) |                     }) | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             { |             createReview, | ||||||
|                 funcName: "list_reviews", |             listReviews, | ||||||
|                 group: "reviews", |  | ||||||
| 
 |  | ||||||
|                 docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten", |  | ||||||
|                 needsUrls: [MangroveReviews.ORIGINAL_API], |  | ||||||
|                 args: [ |  | ||||||
|                     { |  | ||||||
|                         name: "subjectKey", |  | ||||||
|                         defaultValue: "name", |  | ||||||
|                         doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "fallback", |  | ||||||
|                         doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 constr: (state, tags, args, feature, layer) => { |  | ||||||
|                     const nameKey = args[0] ?? "name" |  | ||||||
|                     const fallbackName = args[1] |  | ||||||
|                     const reviews = FeatureReviews.construct( |  | ||||||
|                         feature, |  | ||||||
|                         tags, |  | ||||||
|                         state.userRelatedState?.mangroveIdentity, |  | ||||||
|                         { |  | ||||||
|                             nameKey: nameKey, |  | ||||||
|                             fallbackName |  | ||||||
|                         }, |  | ||||||
|                         state.featureSwitchIsTesting |  | ||||||
|                     ) |  | ||||||
|                     return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer }) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             { |             { | ||||||
|                 funcName: "import_mangrove_key", |                 funcName: "import_mangrove_key", | ||||||
|                 group: "settings", |                 group: "settings", | ||||||
|  | @ -142,6 +149,43 @@ export class ReviewSpecialVisualisations { | ||||||
|                     const [text] = argument |                     const [text] = argument | ||||||
|                     return new SvelteUIElement(ImportReviewIdentity, { state, text }) |                     return new SvelteUIElement(ImportReviewIdentity, { state, text }) | ||||||
|                 } |                 } | ||||||
|             }] |             }, | ||||||
|  |             { | ||||||
|  |                 funcName: "reviews", | ||||||
|  |                 group: "reviews", | ||||||
|  | 
 | ||||||
|  |                 example: | ||||||
|  |                     "`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used", | ||||||
|  |                 docs: "A pragmatic combination of `create_review` and `list_reviews`", | ||||||
|  |                 args: [ | ||||||
|  |                     { | ||||||
|  |                         name: "subjectKey", | ||||||
|  |                         defaultValue: "name", | ||||||
|  |                         doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "fallback", | ||||||
|  |                         doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "question", | ||||||
|  |                         doc: "The question to ask in the review form. Optional" | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  |                 constr( | ||||||
|  |                     state: SpecialVisualizationState, | ||||||
|  |                     tagSource: UIEventSource<Record<string, string>>, | ||||||
|  |                     args: string[], | ||||||
|  |                     feature: Feature, | ||||||
|  |                     layer: LayerConfig | ||||||
|  |                 ): BaseUIElement { | ||||||
|  |                     return new Combine([ | ||||||
|  |                         createReview.constr(state, tagSource, args, feature, layer), | ||||||
|  | 
 | ||||||
|  |                         listReviews.constr(state, tagSource, args, feature, layer) | ||||||
|  |                     ]) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ] | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,237 @@ | ||||||
|  | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  | import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource" | ||||||
|  | import { Feature } from "geojson" | ||||||
|  | import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||||
|  | import { Translation } from "../i18n/Translation" | ||||||
|  | import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
|  | import SvelteUIElement from "../Base/SvelteUIElement" | ||||||
|  | import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte" | ||||||
|  | import GroupedView from "../Popup/GroupedView.svelte" | ||||||
|  | import OpenIdEditor from "../BigComponents/OpenIdEditor.svelte" | ||||||
|  | import OpenJosm from "../Base/OpenJosm.svelte" | ||||||
|  | import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" | ||||||
|  | import BaseUIElement from "../BaseUIElement" | ||||||
|  | import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte" | ||||||
|  | import Combine from "../Base/Combine" | ||||||
|  | 
 | ||||||
|  | class StealViz implements SpecialVisualization { | ||||||
|  | 
 | ||||||
|  |     funcName = "steal" | ||||||
|  |     group = "tagrendering_manipulation" | ||||||
|  | 
 | ||||||
|  |     docs = "Shows a tagRendering from a different object as if this was the object itself" | ||||||
|  |     args = [ | ||||||
|  |         { | ||||||
|  |             name: "featureId", | ||||||
|  |             doc: "The key of the attribute which contains the id of the feature from which to use the tags", | ||||||
|  |             required: true | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name: "tagRenderingId", | ||||||
|  |             doc: "The layer-id and tagRenderingId to render. Can be multiple value if ';'-separated (in which case every value must also contain the layerId, e.g. `layerId.tagRendering0; layerId.tagRendering1`). Note: this can cause layer injection", | ||||||
|  |             required: true | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  |     needsUrls = [] | ||||||
|  |     svelteBased = true | ||||||
|  | 
 | ||||||
|  |     constr(state: SpecialVisualizationState, featureTags, args) { | ||||||
|  |         const [featureIdKey, layerAndtagRenderingIds] = args | ||||||
|  |         const tagRenderings: [LayerConfig, TagRenderingConfig][] = [] | ||||||
|  |         for (const layerAndTagRenderingId of layerAndtagRenderingIds.split(";")) { | ||||||
|  |             const [layerId, tagRenderingId] = layerAndTagRenderingId.trim().split(".") | ||||||
|  |             const layer = state.theme.layers.find((l) => l.id === layerId) | ||||||
|  |             const tagRendering = layer.tagRenderings.find((tr) => tr.id === tagRenderingId) | ||||||
|  |             tagRenderings.push([layer, tagRendering]) | ||||||
|  |         } | ||||||
|  |         if (tagRenderings.length === 0) { | ||||||
|  |             throw "Could not create stolen tagrenddering: tagRenderings not found" | ||||||
|  |         } | ||||||
|  |         return new VariableUiElement( | ||||||
|  |             featureTags.map( | ||||||
|  |                 (tags) => { | ||||||
|  |                     const featureId = tags[featureIdKey] | ||||||
|  |                     if (featureId === undefined) { | ||||||
|  |                         return undefined | ||||||
|  |                     } | ||||||
|  |                     const otherTags = state.featureProperties.getStore(featureId) | ||||||
|  |                     const otherFeature = state.indexedFeatures.featuresById.data.get(featureId) | ||||||
|  |                     const elements: BaseUIElement[] = [] | ||||||
|  |                     for (const [layer, tagRendering] of tagRenderings) { | ||||||
|  |                         elements.push( | ||||||
|  |                             new SvelteUIElement(TagRenderingEditable, { | ||||||
|  |                                 config: tagRendering, | ||||||
|  |                                 tags: otherTags, | ||||||
|  |                                 selectedElement: otherFeature, | ||||||
|  |                                 state, | ||||||
|  |                                 layer | ||||||
|  |                             }) | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                     if (elements.length === 1) { | ||||||
|  |                         return elements[0] | ||||||
|  |                     } | ||||||
|  |                     return new Combine(elements).SetClass("flex flex-col") | ||||||
|  |                 }, | ||||||
|  |                 [state.indexedFeatures.featuresById] | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getLayerDependencies(args): string[] { | ||||||
|  |         const [, tagRenderingId] = args | ||||||
|  |         if (tagRenderingId.indexOf(".") < 0) { | ||||||
|  |             throw "Error: argument 'layerId.tagRenderingId' of special visualisation 'steal' should contain a dot" | ||||||
|  |         } | ||||||
|  |         const [layerId] = tagRenderingId.split(".") | ||||||
|  |         return [layerId] | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default class TagrenderingManipulationSpecialVisualisations { | ||||||
|  | 
 | ||||||
|  |     public static initList(): (SpecialVisualization & { group })[] { | ||||||
|  |         return [ | ||||||
|  |             new StealViz(), | ||||||
|  |             { | ||||||
|  |                 funcName: "multi", | ||||||
|  |                 group: "tagrendering_manipulation", | ||||||
|  |                 docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering", | ||||||
|  |                 example: | ||||||
|  |                     "```json\n" + | ||||||
|  |                     JSON.stringify( | ||||||
|  |                         { | ||||||
|  |                             render: { | ||||||
|  |                                 special: { | ||||||
|  |                                     type: "multi", | ||||||
|  |                                     key: "_doors_from_building_properties", | ||||||
|  |                                     tagrendering: { | ||||||
|  |                                         en: "The building containing this feature has a <a href='#{id}'>door</a> of width {entrance:width}" | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         null, | ||||||
|  |                         "  " | ||||||
|  |                     ) + | ||||||
|  |                     "\n```", | ||||||
|  |                 args: [ | ||||||
|  |                     { | ||||||
|  |                         name: "key", | ||||||
|  |                         doc: "The property to read and to interpret as a list of properties", | ||||||
|  |                         required: true | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "tagrendering", | ||||||
|  |                         doc: "An entire tagRenderingConfig", | ||||||
|  |                         required: true | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "classes", | ||||||
|  |                         doc: "CSS-classes to apply on every individual item. Seperated by `space`" | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  |                 constr( | ||||||
|  |                     state: SpecialVisualizationState, | ||||||
|  |                     featureTags: UIEventSource<Record<string, string>>, | ||||||
|  |                     args: string[], | ||||||
|  |                     feature: Feature, | ||||||
|  |                     layer: LayerConfig | ||||||
|  |                 ) { | ||||||
|  |                     const [key, tr, classesRaw] = args | ||||||
|  |                     const classes = classesRaw ?? "" | ||||||
|  |                     const translation = new Translation({ "*": tr }) | ||||||
|  |                     return new VariableUiElement( | ||||||
|  |                         featureTags.map((tags) => { | ||||||
|  |                             let properties: object[] | ||||||
|  |                             if (typeof tags[key] === "string") { | ||||||
|  |                                 properties = JSON.parse(tags[key]) | ||||||
|  |                             } else { | ||||||
|  |                                 properties = <object[]><unknown>tags[key] | ||||||
|  |                             } | ||||||
|  |                             if (!properties) { | ||||||
|  |                                 console.debug( | ||||||
|  |                                     "Could not create a special visualization for multi(", | ||||||
|  |                                     args.join(", ") + ")", | ||||||
|  |                                     "no properties found for object", | ||||||
|  |                                     feature.properties.id | ||||||
|  |                                 ) | ||||||
|  |                                 return undefined | ||||||
|  |                             } | ||||||
|  |                             const elements = [] | ||||||
|  |                             for (const property of properties) { | ||||||
|  |                                 const subsTr = new SvelteUIElement(SpecialTranslation, { | ||||||
|  |                                     t: translation, | ||||||
|  |                                     tags: new ImmutableStore(property), | ||||||
|  |                                     state, | ||||||
|  |                                     feature, | ||||||
|  |                                     layer | ||||||
|  |                                     // clss: classes ?? "",
 | ||||||
|  |                                 }).SetClass(classes) | ||||||
|  |                                 elements.push(subsTr) | ||||||
|  |                             } | ||||||
|  |                             return elements | ||||||
|  |                         }) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 funcName: "group", | ||||||
|  |                 group: "tagrendering_manipulation", | ||||||
|  |                 docs: "A collapsable group (accordion)", | ||||||
|  |                 args: [ | ||||||
|  |                     { | ||||||
|  |                         name: "header", | ||||||
|  |                         doc: "The _identifier_ of a single tagRendering. This will be used as header" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "labels", | ||||||
|  |                         doc: "A `;`-separated list of either identifiers or label names. All tagRenderings matching this value will be shown in the accordion" | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  |                 constr( | ||||||
|  |                     state: SpecialVisualizationState, | ||||||
|  |                     tags: UIEventSource<Record<string, string>>, | ||||||
|  |                     argument: string[], | ||||||
|  |                     selectedElement: Feature, | ||||||
|  |                     layer: LayerConfig | ||||||
|  |                 ): SvelteUIElement { | ||||||
|  |                     const [header, labelsStr] = argument | ||||||
|  |                     const labels = labelsStr.split(";").map((x) => x.trim()) | ||||||
|  |                     return new SvelteUIElement(GroupedView, { | ||||||
|  |                         state, | ||||||
|  |                         tags, | ||||||
|  |                         selectedElement, | ||||||
|  |                         layer, | ||||||
|  |                         header, | ||||||
|  |                         labels | ||||||
|  |                     }) | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 funcName: "open_in_iD", | ||||||
|  |                 docs: "Opens the current view in the iD-editor", | ||||||
|  |                 args: [], | ||||||
|  |                 group: "tagrendering_manipulation", | ||||||
|  |                 constr: (state, feature): SvelteUIElement => { | ||||||
|  |                     return new SvelteUIElement(OpenIdEditor, { | ||||||
|  |                         mapProperties: state.mapProperties, | ||||||
|  |                         objectId: feature.data.id | ||||||
|  |                     }) | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 funcName: "open_in_josm", | ||||||
|  |                 group: "tagrendering_manipulation", | ||||||
|  |                 docs: "Opens the current view in the JOSM-editor", | ||||||
|  |                 args: [], | ||||||
|  |                 needsUrls: ["http://127.0.0.1:8111/load_and_zoom"], | ||||||
|  | 
 | ||||||
|  |                 constr: (state): SvelteUIElement => { | ||||||
|  |                     return new SvelteUIElement(OpenJosm, { state }) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -1,7 +1,194 @@ | ||||||
| import { SpecialVisualization } from "../SpecialVisualization" | import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|  | import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
|  | import BaseUIElement from "../BaseUIElement" | ||||||
|  | import SvelteUIElement from "../Base/SvelteUIElement" | ||||||
|  | import FediverseLink from "../Popup/FediverseLink.svelte" | ||||||
|  | import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata" | ||||||
|  | import Wikipedia from "../../Logic/Web/Wikipedia" | ||||||
|  | import WikipediaPanel from "../Wikipedia/WikipediaPanel.svelte" | ||||||
|  | import { VariableUiElement } from "../Base/VariableUIElement" | ||||||
|  | import { Utils } from "../../Utils" | ||||||
|  | import { Translation } from "../i18n/Translation" | ||||||
|  | import { MapillaryLinkVis } from "../Popup/MapillaryLinkVis" | ||||||
|  | import SendEmail from "../Popup/SendEmail.svelte" | ||||||
|  | import DynLink from "../Base/DynLink.svelte" | ||||||
| 
 | 
 | ||||||
| export class WebAndDataSpecialVisualisations { | export class WebAndCommunicationSpecialVisualisations { | ||||||
|     public static initList(): (SpecialVisualization & { group }) [] { |     public static initList(): (SpecialVisualization & { group }) [] { | ||||||
|         return [] |         return [ | ||||||
|  | 
 | ||||||
|  |             { | ||||||
|  |                 funcName: "fediverse_link", | ||||||
|  |                 group: "web_and_communication", | ||||||
|  |                 docs: "Converts a fediverse username or link into a clickable link", | ||||||
|  |                 args: [ | ||||||
|  |                     { | ||||||
|  |                         name: "key", | ||||||
|  |                         doc: "The attribute-name containing the link", | ||||||
|  |                         required: true | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  | 
 | ||||||
|  |                 constr( | ||||||
|  |                     state: SpecialVisualizationState, | ||||||
|  |                     tags: UIEventSource<Record<string, string>>, | ||||||
|  |                     argument: string[] | ||||||
|  |                 ): BaseUIElement { | ||||||
|  |                     const key = argument[0] | ||||||
|  |                     return new SvelteUIElement(FediverseLink, { key, tags, state }) | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 funcName: "wikipedia", | ||||||
|  |                 group: "web_and_communication", | ||||||
|  |                 docs: "A box showing the corresponding wikipedia article(s) - based on the **wikidata** tag.", | ||||||
|  |                 args: [ | ||||||
|  |                     { | ||||||
|  |                         name: "keyToShowWikipediaFor", | ||||||
|  |                         doc: "Use the wikidata entry from this key to show the wikipedia article for. Multiple keys can be given (separated by ';'), in which case the first matching value is used", | ||||||
|  |                         defaultValue: "wikidata;wikipedia" | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  |                 needsUrls: [...Wikidata.neededUrls, ...Wikipedia.neededUrls], | ||||||
|  | 
 | ||||||
|  |                 example: | ||||||
|  |                     "`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height", | ||||||
|  |                 constr: (_, tagsSource, args) => { | ||||||
|  |                     const keys = args[0].split(";").map((k) => k.trim()) | ||||||
|  |                     const wikiIds: Store<string[]> = tagsSource.map((tags) => { | ||||||
|  |                         const key = keys.find((k) => tags[k] !== undefined && tags[k] !== "") | ||||||
|  |                         return tags[key]?.split(";")?.map((id) => id.trim()) ?? [] | ||||||
|  |                     }) | ||||||
|  |                     return new SvelteUIElement(WikipediaPanel, { | ||||||
|  |                         wikiIds | ||||||
|  |                     }) | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 funcName: "wikidata_label", | ||||||
|  |                 group: "web_and_communication", | ||||||
|  | 
 | ||||||
|  |                 docs: "Shows the label of the corresponding wikidata-item", | ||||||
|  |                 args: [ | ||||||
|  |                     { | ||||||
|  |                         name: "keyToShowWikidataFor", | ||||||
|  |                         doc: "Use the wikidata entry from this key to show the label", | ||||||
|  |                         defaultValue: "wikidata" | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  |                 needsUrls: Wikidata.neededUrls, | ||||||
|  |                 example: | ||||||
|  |                     "`{wikidata_label()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the label itself", | ||||||
|  |                 constr: (_, tagsSource, args) => | ||||||
|  |                     new VariableUiElement( | ||||||
|  |                         tagsSource | ||||||
|  |                             .map((tags) => tags[args[0]]) | ||||||
|  |                             .map((wikidata) => { | ||||||
|  |                                 wikidata = Utils.NoEmpty( | ||||||
|  |                                     wikidata?.split(";")?.map((wd) => wd.trim()) ?? [] | ||||||
|  |                                 )[0] | ||||||
|  |                                 const entry = Wikidata.LoadWikidataEntry(wikidata) | ||||||
|  |                                 return new VariableUiElement( | ||||||
|  |                                     entry.map((e) => { | ||||||
|  |                                         if (e === undefined || e["success"] === undefined) { | ||||||
|  |                                             return wikidata | ||||||
|  |                                         } | ||||||
|  |                                         const response = <WikidataResponse>e["success"] | ||||||
|  |                                         return Translation.fromMap(response.labels) | ||||||
|  |                                     }) | ||||||
|  |                                 ) | ||||||
|  |                             }) | ||||||
|  |                     ) | ||||||
|  |             }, | ||||||
|  |             new MapillaryLinkVis(), | ||||||
|  |             { | ||||||
|  |                 funcName: "send_email", | ||||||
|  |                 group: "web_and_communication", | ||||||
|  |                 docs: "Creates a `mailto`-link where some fields are already set and correctly escaped. The user will be promted to send the email", | ||||||
|  |                 args: [ | ||||||
|  |                     { | ||||||
|  |                         name: "to", | ||||||
|  |                         doc: "Who to send the email to?", | ||||||
|  |                         required: true | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "subject", | ||||||
|  |                         doc: "The subject of the email", | ||||||
|  |                         required: true | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "body", | ||||||
|  |                         doc: "The text in the email", | ||||||
|  |                         required: true | ||||||
|  |                     }, | ||||||
|  | 
 | ||||||
|  |                     { | ||||||
|  |                         name: "button_text", | ||||||
|  |                         doc: "The text shown on the button in the UI", | ||||||
|  |                         required: true | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  | 
 | ||||||
|  |                 constr(__, tags, args) { | ||||||
|  |                     return new SvelteUIElement(SendEmail, { args, tags }) | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 funcName: "link", | ||||||
|  |                 group: "web_and_communication", | ||||||
|  |                 docs: "Construct a link. By using the 'special' visualisation notation, translations should be easier", | ||||||
|  |                 args: [ | ||||||
|  |                     { | ||||||
|  |                         name: "text", | ||||||
|  |                         doc: "Text to be shown", | ||||||
|  |                         required: true | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "href", | ||||||
|  |                         doc: "The URL to link to. Note that this will be URI-encoded before ", | ||||||
|  |                         required: true | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "class", | ||||||
|  |                         doc: "CSS-classes to add to the element" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "download", | ||||||
|  |                         doc: "Expects a string which denotes the filename to download the contents of `href` into. If set, this link will act as a download-button." | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "arialabel", | ||||||
|  |                         doc: "If set, this text will be used as aria-label" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         name: "icon", | ||||||
|  |                         doc: "If set, show this icon next to the link. You might want to combine this with `class: button`" | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  | 
 | ||||||
|  |                 constr( | ||||||
|  |                     state: SpecialVisualizationState, | ||||||
|  |                     tagSource: UIEventSource<Record<string, string>>, | ||||||
|  |                     args: string[] | ||||||
|  |                 ): SvelteUIElement { | ||||||
|  |                     let [text, href, classnames, download, ariaLabel, icon] = args | ||||||
|  |                     if (download === "") { | ||||||
|  |                         download = undefined | ||||||
|  |                     } | ||||||
|  |                     const newTab = download === undefined && !href.startsWith("#") | ||||||
|  |                     const textStore = tagSource.map((tags) => Utils.SubstituteKeys(text, tags)) | ||||||
|  |                     const hrefStore = tagSource.map((tags) => Utils.SubstituteKeys(href, tags)) | ||||||
|  |                     return new SvelteUIElement(DynLink, { | ||||||
|  |                         text: textStore, | ||||||
|  |                         href: hrefStore, | ||||||
|  |                         classnames: new ImmutableStore(classnames), | ||||||
|  |                         download: tagSource.map((tags) => Utils.SubstituteKeys(download, tags)), | ||||||
|  |                         ariaLabel: tagSource.map((tags) => Utils.SubstituteKeys(ariaLabel, tags)), | ||||||
|  |                         newTab: new ImmutableStore(newTab), | ||||||
|  |                         icon: tagSource.map((tags) => Utils.SubstituteKeys(icon, tags)) | ||||||
|  |                     }).setSpan() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ] | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,14 +6,9 @@ import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState | ||||||
| import { HistogramViz } from "./Popup/HistogramViz" | import { HistogramViz } from "./Popup/HistogramViz" | ||||||
| import { UploadToOsmViz } from "./Popup/UploadToOsmViz" | import { UploadToOsmViz } from "./Popup/UploadToOsmViz" | ||||||
| import { MultiApplyViz } from "./Popup/MultiApplyViz" | import { MultiApplyViz } from "./Popup/MultiApplyViz" | ||||||
| import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz" | import { UIEventSource } from "../Logic/UIEventSource" | ||||||
| import TagApplyButton from "./Popup/TagApplyButton" |  | ||||||
| import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis" |  | ||||||
| import { ImmutableStore, Store, Stores, UIEventSource } from "../Logic/UIEventSource" |  | ||||||
| import AllTagsPanel from "./Popup/AllTagsPanel.svelte" | import AllTagsPanel from "./Popup/AllTagsPanel.svelte" | ||||||
| import { VariableUiElement } from "./Base/VariableUIElement" | import { VariableUiElement } from "./Base/VariableUIElement" | ||||||
| import { Utils } from "../Utils" |  | ||||||
| import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata" |  | ||||||
| import { Translation } from "./i18n/Translation" | import { Translation } from "./i18n/Translation" | ||||||
| import Translations from "./i18n/Translations" | import Translations from "./i18n/Translations" | ||||||
| import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization" | import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization" | ||||||
|  | @ -23,119 +18,34 @@ import AutoApplyButton from "./Popup/AutoApplyButton" | ||||||
| import { LanguageElement } from "./Popup/LanguageElement/LanguageElement" | import { LanguageElement } from "./Popup/LanguageElement/LanguageElement" | ||||||
| import SvelteUIElement from "./Base/SvelteUIElement" | import SvelteUIElement from "./Base/SvelteUIElement" | ||||||
| import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" | import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" | ||||||
| import { Feature, GeoJsonProperties, LineString } from "geojson" | import { Feature, LineString } from "geojson" | ||||||
| import { GeoOperations } from "../Logic/GeoOperations" | import { GeoOperations } from "../Logic/GeoOperations" | ||||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig" | import LayerConfig from "../Models/ThemeConfig/LayerConfig" | ||||||
| import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" | import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" | ||||||
| import ExportFeatureButton from "./Popup/ExportFeatureButton.svelte" | import ExportFeatureButton from "./Popup/ExportFeatureButton.svelte" | ||||||
| import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte" |  | ||||||
| import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte" | import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte" | ||||||
| import { PointImportButtonViz } from "./Popup/ImportButtons/PointImportButtonViz" |  | ||||||
| import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz" |  | ||||||
| import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz" |  | ||||||
| import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte" |  | ||||||
| import SendEmail from "./Popup/SendEmail.svelte" |  | ||||||
| import Constants from "../Models/Constants" | import Constants from "../Models/Constants" | ||||||
| import Wikipedia from "../Logic/Web/Wikipedia" |  | ||||||
| import { TagUtils } from "../Logic/Tags/TagUtils" | import { TagUtils } from "../Logic/Tags/TagUtils" | ||||||
| import OpenJosm from "./Base/OpenJosm.svelte" |  | ||||||
| import NextChangeViz from "./OpeningHours/NextChangeViz.svelte" | import NextChangeViz from "./OpeningHours/NextChangeViz.svelte" | ||||||
| import { Unit } from "../Models/Unit" | import { Unit } from "../Models/Unit" | ||||||
| import DirectionIndicator from "./Base/DirectionIndicator.svelte" | import DirectionIndicator from "./Base/DirectionIndicator.svelte" | ||||||
| import ComparisonTool from "./Comparison/ComparisonTool.svelte" |  | ||||||
| import SpecialTranslation from "./Popup/TagRendering/SpecialTranslation.svelte" |  | ||||||
| import SpecialVisualisationUtils from "./SpecialVisualisationUtils" | import SpecialVisualisationUtils from "./SpecialVisualisationUtils" | ||||||
| import Toggle from "./Input/Toggle" |  | ||||||
| import LinkedDataLoader from "../Logic/Web/LinkedDataLoader" |  | ||||||
| import DynLink from "./Base/DynLink.svelte" |  | ||||||
| import MarkdownUtils from "../Utils/MarkdownUtils" | import MarkdownUtils from "../Utils/MarkdownUtils" | ||||||
| import Trash from "@babeard/svelte-heroicons/mini/Trash" | import Trash from "@babeard/svelte-heroicons/mini/Trash" | ||||||
| import { And } from "../Logic/Tags/And" | import { And } from "../Logic/Tags/And" | ||||||
| import GroupedView from "./Popup/GroupedView.svelte" |  | ||||||
| import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" | import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" | ||||||
| import NoteCommentElement from "./Popup/Notes/NoteCommentElement.svelte" |  | ||||||
| import FediverseLink from "./Popup/FediverseLink.svelte" |  | ||||||
| import { ImageVisualisations } from "./SpecialVisualisations/ImageVisualisations" | import { ImageVisualisations } from "./SpecialVisualisations/ImageVisualisations" | ||||||
| import { NoteVisualisations } from "./SpecialVisualisations/NoteVisualisations" | import { NoteVisualisations } from "./SpecialVisualisations/NoteVisualisations" | ||||||
| import { FavouriteVisualisations } from "./SpecialVisualisations/FavouriteVisualisations" | import { FavouriteVisualisations } from "./SpecialVisualisations/FavouriteVisualisations" | ||||||
| import { UISpecialVisualisations } from "./SpecialVisualisations/UISpecialVisualisations" | import { UISpecialVisualisations } from "./SpecialVisualisations/UISpecialVisualisations" | ||||||
| import { SettingsVisualisations } from "./SpecialVisualisations/SettingsVisualisations" | import { SettingsVisualisations } from "./SpecialVisualisations/SettingsVisualisations" | ||||||
| import { ReviewSpecialVisualisations } from "./SpecialVisualisations/ReviewSpecialVisualisations" | import { ReviewSpecialVisualisations } from "./SpecialVisualisations/ReviewSpecialVisualisations" | ||||||
| import { MapRouletteSpecialVisualisations } from "./SpecialVisualisations/MapRouletteSpecialVisualisations" | import { DataImportSpecialVisualisations } from "./SpecialVisualisations/DataImportSpecialVisualisations" | ||||||
| 
 | import TagrenderingManipulationSpecialVisualisations | ||||||
| 
 |     from "./SpecialVisualisations/TagrenderingManipulationSpecialVisualisations" | ||||||
| class StealViz implements SpecialVisualization { | import { | ||||||
|     // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
 |     WebAndCommunicationSpecialVisualisations | ||||||
| 
 | } from "./SpecialVisualisations/WebAndCommunicationSpecialVisualisations" | ||||||
|     funcName = "steal" |  | ||||||
|     docs = "Shows a tagRendering from a different object as if this was the object itself" |  | ||||||
|     args = [ |  | ||||||
|         { |  | ||||||
|             name: "featureId", |  | ||||||
|             doc: "The key of the attribute which contains the id of the feature from which to use the tags", |  | ||||||
|             required: true |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             name: "tagRenderingId", |  | ||||||
|             doc: "The layer-id and tagRenderingId to render. Can be multiple value if ';'-separated (in which case every value must also contain the layerId, e.g. `layerId.tagRendering0; layerId.tagRendering1`). Note: this can cause layer injection", |  | ||||||
|             required: true |  | ||||||
|         } |  | ||||||
|     ] |  | ||||||
|     needsUrls = [] |  | ||||||
|     svelteBased = true |  | ||||||
| 
 |  | ||||||
|     constr(state: SpecialVisualizationState, featureTags, args) { |  | ||||||
|         const [featureIdKey, layerAndtagRenderingIds] = args |  | ||||||
|         const tagRenderings: [LayerConfig, TagRenderingConfig][] = [] |  | ||||||
|         for (const layerAndTagRenderingId of layerAndtagRenderingIds.split(";")) { |  | ||||||
|             const [layerId, tagRenderingId] = layerAndTagRenderingId.trim().split(".") |  | ||||||
|             const layer = state.theme.layers.find((l) => l.id === layerId) |  | ||||||
|             const tagRendering = layer.tagRenderings.find((tr) => tr.id === tagRenderingId) |  | ||||||
|             tagRenderings.push([layer, tagRendering]) |  | ||||||
|         } |  | ||||||
|         if (tagRenderings.length === 0) { |  | ||||||
|             throw "Could not create stolen tagrenddering: tagRenderings not found" |  | ||||||
|         } |  | ||||||
|         return new VariableUiElement( |  | ||||||
|             featureTags.map( |  | ||||||
|                 (tags) => { |  | ||||||
|                     const featureId = tags[featureIdKey] |  | ||||||
|                     if (featureId === undefined) { |  | ||||||
|                         return undefined |  | ||||||
|                     } |  | ||||||
|                     const otherTags = state.featureProperties.getStore(featureId) |  | ||||||
|                     const otherFeature = state.indexedFeatures.featuresById.data.get(featureId) |  | ||||||
|                     const elements: BaseUIElement[] = [] |  | ||||||
|                     for (const [layer, tagRendering] of tagRenderings) { |  | ||||||
|                         elements.push( |  | ||||||
|                             new SvelteUIElement(TagRenderingEditable, { |  | ||||||
|                                 config: tagRendering, |  | ||||||
|                                 tags: otherTags, |  | ||||||
|                                 selectedElement: otherFeature, |  | ||||||
|                                 state, |  | ||||||
|                                 layer |  | ||||||
|                             }) |  | ||||||
|                         ) |  | ||||||
|                     } |  | ||||||
|                     if (elements.length === 1) { |  | ||||||
|                         return elements[0] |  | ||||||
|                     } |  | ||||||
|                     return new Combine(elements).SetClass("flex flex-col") |  | ||||||
|                 }, |  | ||||||
|                 [state.indexedFeatures.featuresById] |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     getLayerDependencies(args): string[] { |  | ||||||
|         const [, tagRenderingId] = args |  | ||||||
|         if (tagRenderingId.indexOf(".") < 0) { |  | ||||||
|             throw "Error: argument 'layerId.tagRenderingId' of special visualisation 'steal' should contain a dot" |  | ||||||
|         } |  | ||||||
|         const [layerId] = tagRenderingId.split(".") |  | ||||||
|         return [layerId] |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| export default class SpecialVisualizations { | export default class SpecialVisualizations { | ||||||
|  | @ -180,7 +90,7 @@ export default class SpecialVisualizations { | ||||||
|                 ) |                 ) | ||||||
|                 : undefined, |                 : undefined, | ||||||
|             "#### Example usage of " + viz.funcName, |             "#### Example usage of " + viz.funcName, | ||||||
|             "<code>" + example + "</code>" |             example | ||||||
|         ].join("\n\n") |         ].join("\n\n") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -215,7 +125,9 @@ export default class SpecialVisualizations { | ||||||
|             "images": "Elements related to adding or manipulating images. Normally also added by default, but in some cases a tweaked version is needed", |             "images": "Elements related to adding or manipulating images. Normally also added by default, but in some cases a tweaked version is needed", | ||||||
|             "notes": "Elements relating to OpenStreetMap-notes, e.g. the component to close and/or add a comment", |             "notes": "Elements relating to OpenStreetMap-notes, e.g. the component to close and/or add a comment", | ||||||
|             "reviews": "Elements relating to seeing and adding ratings and reviews with Mangrove.reviews", |             "reviews": "Elements relating to seeing and adding ratings and reviews with Mangrove.reviews", | ||||||
|             "maproulette": "Elements to close a maproulette task" |             "data_import": "Elements to help with importing data to OSM. For example: buttons to import a feature, apply tags on an element, apply multiple tags on an element or to work with maproulette", | ||||||
|  |             "tagrendering_manipulation": "Special visualisations which reuse other tagRenderings to show data, but with a twist.", | ||||||
|  |             "web_and_communication": "Tools to show data from external websites, which link to external websites or which link to external profiles" | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const helpTexts: string[] = [] |         const helpTexts: string[] = [] | ||||||
|  | @ -224,8 +136,6 @@ export default class SpecialVisualizations { | ||||||
|             if (viz.group !== lastGroup) { |             if (viz.group !== lastGroup) { | ||||||
|                 lastGroup = viz.group |                 lastGroup = viz.group | ||||||
|                 if (viz.group === undefined) { |                 if (viz.group === undefined) { | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                     helpTexts.push("## Unclassified elements\n\nVarious elements") |                     helpTexts.push("## Unclassified elements\n\nVarious elements") | ||||||
|                 } else { |                 } else { | ||||||
| 
 | 
 | ||||||
|  | @ -268,10 +178,11 @@ export default class SpecialVisualizations { | ||||||
|             "# Special tag renderings", |             "# Special tag renderings", | ||||||
|             "In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.", |             "In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.", | ||||||
|             "General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssClasses}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args", |             "General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssClasses}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args", | ||||||
|             "#### Using expanded syntax", |             "# Using expanded syntax", | ||||||
|             `Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}\`, one can also write`, |             `Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}\`, one can also write`, | ||||||
|             "```\n" + example + "\n```\n", |             "```\n" + example + "\n```\n", | ||||||
|             "In other words: use `{ \"before\": ..., \"after\": ..., \"special\": {\"type\": ..., \"argname\": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)" |             "In other words: use `{ \"before\": ..., \"after\": ..., \"special\": {\"type\": ..., \"argname\": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)", | ||||||
|  |             "# Overview of all special components" | ||||||
|         ].join("\n\n") |         ].join("\n\n") | ||||||
|         return firstPart + "\n\n" + helpTexts.join("\n\n") |         return firstPart + "\n\n" + helpTexts.join("\n\n") | ||||||
|     } |     } | ||||||
|  | @ -284,12 +195,10 @@ export default class SpecialVisualizations { | ||||||
|             ...UISpecialVisualisations.initList(), |             ...UISpecialVisualisations.initList(), | ||||||
|             ...SettingsVisualisations.initList(), |             ...SettingsVisualisations.initList(), | ||||||
|             ...ReviewSpecialVisualisations.initList(), |             ...ReviewSpecialVisualisations.initList(), | ||||||
|             ...MapRouletteSpecialVisualisations.initList(), |             ...DataImportSpecialVisualisations.initList(), | ||||||
| 
 |             ...TagrenderingManipulationSpecialVisualisations.initList(), | ||||||
|  |             ...WebAndCommunicationSpecialVisualisations.initList(), | ||||||
|             new HistogramViz(), |             new HistogramViz(), | ||||||
|             new StealViz(), |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             { |             { | ||||||
|                 funcName: "export_as_gpx", |                 funcName: "export_as_gpx", | ||||||
|                 docs: "Exports the selected feature as GPX-file", |                 docs: "Exports the selected feature as GPX-file", | ||||||
|  | @ -324,74 +233,6 @@ export default class SpecialVisualizations { | ||||||
|             new UploadToOsmViz(), |             new UploadToOsmViz(), | ||||||
|             new MultiApplyViz(), |             new MultiApplyViz(), | ||||||
| 
 | 
 | ||||||
|             new PlantNetDetectionViz(), |  | ||||||
| 
 |  | ||||||
|             new TagApplyButton(), |  | ||||||
| 
 |  | ||||||
|             new PointImportButtonViz(), |  | ||||||
|             new WayImportButtonViz(), |  | ||||||
|             new ConflateImportButtonViz(), |  | ||||||
| 
 |  | ||||||
|             { |  | ||||||
|                 funcName: "wikipedia", |  | ||||||
|                 docs: "A box showing the corresponding wikipedia article(s) - based on the **wikidata** tag.", |  | ||||||
|                 args: [ |  | ||||||
|                     { |  | ||||||
|                         name: "keyToShowWikipediaFor", |  | ||||||
|                         doc: "Use the wikidata entry from this key to show the wikipedia article for. Multiple keys can be given (separated by ';'), in which case the first matching value is used", |  | ||||||
|                         defaultValue: "wikidata;wikipedia" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 needsUrls: [...Wikidata.neededUrls, ...Wikipedia.neededUrls], |  | ||||||
| 
 |  | ||||||
|                 example: |  | ||||||
|                     "`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height", |  | ||||||
|                 constr: (_, tagsSource, args) => { |  | ||||||
|                     const keys = args[0].split(";").map((k) => k.trim()) |  | ||||||
|                     const wikiIds: Store<string[]> = tagsSource.map((tags) => { |  | ||||||
|                         const key = keys.find((k) => tags[k] !== undefined && tags[k] !== "") |  | ||||||
|                         return tags[key]?.split(";")?.map((id) => id.trim()) ?? [] |  | ||||||
|                     }) |  | ||||||
|                     return new SvelteUIElement(WikipediaPanel, { |  | ||||||
|                         wikiIds |  | ||||||
|                     }) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 funcName: "wikidata_label", |  | ||||||
|                 docs: "Shows the label of the corresponding wikidata-item", |  | ||||||
|                 args: [ |  | ||||||
|                     { |  | ||||||
|                         name: "keyToShowWikidataFor", |  | ||||||
|                         doc: "Use the wikidata entry from this key to show the label", |  | ||||||
|                         defaultValue: "wikidata" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 needsUrls: Wikidata.neededUrls, |  | ||||||
|                 example: |  | ||||||
|                     "`{wikidata_label()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the label itself", |  | ||||||
|                 constr: (_, tagsSource, args) => |  | ||||||
|                     new VariableUiElement( |  | ||||||
|                         tagsSource |  | ||||||
|                             .map((tags) => tags[args[0]]) |  | ||||||
|                             .map((wikidata) => { |  | ||||||
|                                 wikidata = Utils.NoEmpty( |  | ||||||
|                                     wikidata?.split(";")?.map((wd) => wd.trim()) ?? [] |  | ||||||
|                                 )[0] |  | ||||||
|                                 const entry = Wikidata.LoadWikidataEntry(wikidata) |  | ||||||
|                                 return new VariableUiElement( |  | ||||||
|                                     entry.map((e) => { |  | ||||||
|                                         if (e === undefined || e["success"] === undefined) { |  | ||||||
|                                             return wikidata |  | ||||||
|                                         } |  | ||||||
|                                         const response = <WikidataResponse>e["success"] |  | ||||||
|                                         return Translation.fromMap(response.labels) |  | ||||||
|                                     }) |  | ||||||
|                                 ) |  | ||||||
|                             }) |  | ||||||
|                     ) |  | ||||||
|             }, |  | ||||||
|             new MapillaryLinkVis(), |  | ||||||
|             new LanguageElement(), |             new LanguageElement(), | ||||||
|             { |             { | ||||||
|                 funcName: "all_tags", |                 funcName: "all_tags", | ||||||
|  | @ -405,47 +246,6 @@ export default class SpecialVisualizations { | ||||||
|                     layer: LayerConfig |                     layer: LayerConfig | ||||||
|                 ) => new SvelteUIElement(AllTagsPanel, { tags, layer }) |                 ) => new SvelteUIElement(AllTagsPanel, { tags, layer }) | ||||||
|             }, |             }, | ||||||
|             { |  | ||||||
|                 funcName: "reviews", |  | ||||||
|                 group: "reviews", |  | ||||||
| 
 |  | ||||||
|                 example: |  | ||||||
|                     "`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used", |  | ||||||
|                 docs: "A pragmatic combination of `create_review` and `list_reviews`", |  | ||||||
|                 args: [ |  | ||||||
|                     { |  | ||||||
|                         name: "subjectKey", |  | ||||||
|                         defaultValue: "name", |  | ||||||
|                         doc: "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "fallback", |  | ||||||
|                         doc: "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "question", |  | ||||||
|                         doc: "The question to ask in the review form. Optional" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 constr( |  | ||||||
|                     state: SpecialVisualizationState, |  | ||||||
|                     tagSource: UIEventSource<Record<string, string>>, |  | ||||||
|                     args: string[], |  | ||||||
|                     feature: Feature, |  | ||||||
|                     layer: LayerConfig |  | ||||||
|                 ): BaseUIElement { |  | ||||||
|                     return new Combine([ |  | ||||||
|                         SpecialVisualizations.specialVisualisationsDict |  | ||||||
|                             .get("create_review") |  | ||||||
|                             .constr(state, tagSource, args, feature, layer), |  | ||||||
|                         SpecialVisualizations.specialVisualisationsDict |  | ||||||
|                             .get("list_reviews") |  | ||||||
|                             .constr(state, tagSource, args, feature, layer) |  | ||||||
|                     ]) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|             { |             { | ||||||
|                 funcName: "opening_hours_table", |                 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'.", |                 docs: "Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.", | ||||||
|  | @ -568,28 +368,7 @@ export default class SpecialVisualizations { | ||||||
|                     }) |                     }) | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             { |  | ||||||
|                 funcName: "open_in_iD", |  | ||||||
|                 docs: "Opens the current view in the iD-editor", |  | ||||||
|                 args: [], |  | ||||||
| 
 | 
 | ||||||
|                 constr: (state, feature) => { |  | ||||||
|                     return new SvelteUIElement(OpenIdEditor, { |  | ||||||
|                         mapProperties: state.mapProperties, |  | ||||||
|                         objectId: feature.data.id |  | ||||||
|                     }) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 funcName: "open_in_josm", |  | ||||||
|                 docs: "Opens the current view in the JOSM-editor", |  | ||||||
|                 args: [], |  | ||||||
|                 needsUrls: ["http://127.0.0.1:8111/load_and_zoom"], |  | ||||||
| 
 |  | ||||||
|                 constr: (state) => { |  | ||||||
|                     return new SvelteUIElement(OpenJosm, { state }) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             { |             { | ||||||
|                 funcName: "clear_location_history", |                 funcName: "clear_location_history", | ||||||
|                 docs: "A button to remove the travelled track information from the device", |                 docs: "A button to remove the travelled track information from the device", | ||||||
|  | @ -605,46 +384,7 @@ export default class SpecialVisualizations { | ||||||
|                     }) |                     }) | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             { | 
 | ||||||
|                 funcName: "visualize_note_comments", |  | ||||||
|                 docs: "Visualises the comments for notes", |  | ||||||
|                 args: [ |  | ||||||
|                     { |  | ||||||
|                         name: "commentsKey", |  | ||||||
|                         doc: "The property name of the comments, which should be stringified json", |  | ||||||
|                         defaultValue: "comments" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "start", |  | ||||||
|                         doc: "Drop the first 'start' comments", |  | ||||||
|                         defaultValue: "0" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 needsUrls: [Constants.osmAuthConfig.url], |  | ||||||
|                 constr: (state, tags, args) => |  | ||||||
|                     new VariableUiElement( |  | ||||||
|                         tags |  | ||||||
|                             .map((tags) => tags[args[0]]) |  | ||||||
|                             .map((commentsStr) => { |  | ||||||
|                                 const comments: { text: string }[] = JSON.parse(commentsStr) |  | ||||||
|                                 const startLoc = Number(args[1] ?? 0) |  | ||||||
|                                 if (!isNaN(startLoc) && startLoc > 0) { |  | ||||||
|                                     comments.splice(0, startLoc) |  | ||||||
|                                 } |  | ||||||
|                                 return new Combine( |  | ||||||
|                                     comments |  | ||||||
|                                         .filter((c) => c.text !== "") |  | ||||||
|                                         .map( |  | ||||||
|                                             (comment) => |  | ||||||
|                                                 new SvelteUIElement(NoteCommentElement, { |  | ||||||
|                                                     comment, |  | ||||||
|                                                     state |  | ||||||
|                                                 }) |  | ||||||
|                                         ) |  | ||||||
|                                 ).SetClass("flex flex-col") |  | ||||||
|                             }) |  | ||||||
|                     ) |  | ||||||
|             }, |  | ||||||
|             { |             { | ||||||
|                 funcName: "title", |                 funcName: "title", | ||||||
|                 args: [], |                 args: [], | ||||||
|  | @ -689,173 +429,8 @@ export default class SpecialVisualizations { | ||||||
|                     ) |                     ) | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             { |  | ||||||
|                 funcName: "send_email", |  | ||||||
|                 docs: "Creates a `mailto`-link where some fields are already set and correctly escaped. The user will be promted to send the email", |  | ||||||
|                 args: [ |  | ||||||
|                     { |  | ||||||
|                         name: "to", |  | ||||||
|                         doc: "Who to send the email to?", |  | ||||||
|                         required: true |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "subject", |  | ||||||
|                         doc: "The subject of the email", |  | ||||||
|                         required: true |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "body", |  | ||||||
|                         doc: "The text in the email", |  | ||||||
|                         required: true |  | ||||||
|                     }, |  | ||||||
| 
 | 
 | ||||||
|                     { |  | ||||||
|                         name: "button_text", |  | ||||||
|                         doc: "The text shown on the button in the UI", |  | ||||||
|                         required: true |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
| 
 | 
 | ||||||
|                 constr(__, tags, args) { |  | ||||||
|                     return new SvelteUIElement(SendEmail, { args, tags }) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 funcName: "link", |  | ||||||
|                 docs: "Construct a link. By using the 'special' visualisation notation, translations should be easier", |  | ||||||
|                 args: [ |  | ||||||
|                     { |  | ||||||
|                         name: "text", |  | ||||||
|                         doc: "Text to be shown", |  | ||||||
|                         required: true |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "href", |  | ||||||
|                         doc: "The URL to link to. Note that this will be URI-encoded before ", |  | ||||||
|                         required: true |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "class", |  | ||||||
|                         doc: "CSS-classes to add to the element" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "download", |  | ||||||
|                         doc: "Expects a string which denotes the filename to download the contents of `href` into. If set, this link will act as a download-button." |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "arialabel", |  | ||||||
|                         doc: "If set, this text will be used as aria-label" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "icon", |  | ||||||
|                         doc: "If set, show this icon next to the link. You might want to combine this with `class: button`" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
| 
 |  | ||||||
|                 constr( |  | ||||||
|                     state: SpecialVisualizationState, |  | ||||||
|                     tagSource: UIEventSource<Record<string, string>>, |  | ||||||
|                     args: string[] |  | ||||||
|                 ): SvelteUIElement { |  | ||||||
|                     let [text, href, classnames, download, ariaLabel, icon] = args |  | ||||||
|                     if (download === "") { |  | ||||||
|                         download = undefined |  | ||||||
|                     } |  | ||||||
|                     const newTab = download === undefined && !href.startsWith("#") |  | ||||||
|                     const textStore = tagSource.map((tags) => Utils.SubstituteKeys(text, tags)) |  | ||||||
|                     const hrefStore = tagSource.map((tags) => Utils.SubstituteKeys(href, tags)) |  | ||||||
|                     return new SvelteUIElement(DynLink, { |  | ||||||
|                         text: textStore, |  | ||||||
|                         href: hrefStore, |  | ||||||
|                         classnames: new ImmutableStore(classnames), |  | ||||||
|                         download: tagSource.map((tags) => Utils.SubstituteKeys(download, tags)), |  | ||||||
|                         ariaLabel: tagSource.map((tags) => Utils.SubstituteKeys(ariaLabel, tags)), |  | ||||||
|                         newTab: new ImmutableStore(newTab), |  | ||||||
|                         icon: tagSource.map((tags) => Utils.SubstituteKeys(icon, tags)) |  | ||||||
|                     }).setSpan() |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 funcName: "multi", |  | ||||||
|                 docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering", |  | ||||||
|                 example: |  | ||||||
|                     "```json\n" + |  | ||||||
|                     JSON.stringify( |  | ||||||
|                         { |  | ||||||
|                             render: { |  | ||||||
|                                 special: { |  | ||||||
|                                     type: "multi", |  | ||||||
|                                     key: "_doors_from_building_properties", |  | ||||||
|                                     tagrendering: { |  | ||||||
|                                         en: "The building containing this feature has a <a href='#{id}'>door</a> of width {entrance:width}" |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         }, |  | ||||||
|                         null, |  | ||||||
|                         "  " |  | ||||||
|                     ) + |  | ||||||
|                     "\n```", |  | ||||||
|                 args: [ |  | ||||||
|                     { |  | ||||||
|                         name: "key", |  | ||||||
|                         doc: "The property to read and to interpret as a list of properties", |  | ||||||
|                         required: true |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "tagrendering", |  | ||||||
|                         doc: "An entire tagRenderingConfig", |  | ||||||
|                         required: true |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "classes", |  | ||||||
|                         doc: "CSS-classes to apply on every individual item. Seperated by `space`" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 constr( |  | ||||||
|                     state: SpecialVisualizationState, |  | ||||||
|                     featureTags: UIEventSource<Record<string, string>>, |  | ||||||
|                     args: string[], |  | ||||||
|                     feature: Feature, |  | ||||||
|                     layer: LayerConfig |  | ||||||
|                 ) { |  | ||||||
|                     const [key, tr, classesRaw] = args |  | ||||||
|                     let classes = classesRaw ?? "" |  | ||||||
|                     const translation = new Translation({ "*": tr }) |  | ||||||
|                     return new VariableUiElement( |  | ||||||
|                         featureTags.map((tags) => { |  | ||||||
|                             let properties: object[] |  | ||||||
|                             if (typeof tags[key] === "string") { |  | ||||||
|                                 properties = JSON.parse(tags[key]) |  | ||||||
|                             } else { |  | ||||||
|                                 properties = <any>tags[key] |  | ||||||
|                             } |  | ||||||
|                             if (!properties) { |  | ||||||
|                                 console.debug( |  | ||||||
|                                     "Could not create a special visualization for multi(", |  | ||||||
|                                     args.join(", ") + ")", |  | ||||||
|                                     "no properties found for object", |  | ||||||
|                                     feature.properties.id |  | ||||||
|                                 ) |  | ||||||
|                                 return undefined |  | ||||||
|                             } |  | ||||||
|                             const elements = [] |  | ||||||
|                             for (const property of properties) { |  | ||||||
|                                 const subsTr = new SvelteUIElement(SpecialTranslation, { |  | ||||||
|                                     t: translation, |  | ||||||
|                                     tags: new ImmutableStore(property), |  | ||||||
|                                     state, |  | ||||||
|                                     feature, |  | ||||||
|                                     layer |  | ||||||
|                                     // clss: classes ?? "",
 |  | ||||||
|                                 }).SetClass(classes) |  | ||||||
|                                 elements.push(subsTr) |  | ||||||
|                             } |  | ||||||
|                             return elements |  | ||||||
|                         }) |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             { |             { | ||||||
|                 funcName: "translated", |                 funcName: "translated", | ||||||
|                 docs: "If the given key can be interpreted as a JSON, only show the key containing the current language (or 'en'). This specialRendering is meant to be used by MapComplete studio and is not useful in map themes", |                 docs: "If the given key can be interpreted as a JSON, only show the key containing the current language (or 'en'). This specialRendering is meant to be used by MapComplete studio and is not useful in map themes", | ||||||
|  | @ -886,28 +461,6 @@ export default class SpecialVisualizations { | ||||||
|                     ) |                     ) | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             { |  | ||||||
|                 funcName: "fediverse_link", |  | ||||||
|                 docs: "Converts a fediverse username or link into a clickable link", |  | ||||||
|                 args: [ |  | ||||||
|                     { |  | ||||||
|                         name: "key", |  | ||||||
|                         doc: "The attribute-name containing the link", |  | ||||||
|                         required: true |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
| 
 |  | ||||||
|                 constr( |  | ||||||
|                     state: SpecialVisualizationState, |  | ||||||
|                     tags: UIEventSource<Record<string, string>>, |  | ||||||
|                     argument: string[], |  | ||||||
|                     feature: Feature, |  | ||||||
|                     layer: LayerConfig |  | ||||||
|                 ): BaseUIElement { |  | ||||||
|                     const key = argument[0] |  | ||||||
|                     return new SvelteUIElement(FediverseLink, { key, tags, state }) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             { |             { | ||||||
|                 funcName: "braced", |                 funcName: "braced", | ||||||
|                 docs: "Show a literal text within braces", |                 docs: "Show a literal text within braces", | ||||||
|  | @ -1022,182 +575,6 @@ export default class SpecialVisualizations { | ||||||
|                     ) |                     ) | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             { |  | ||||||
|                 funcName: "compare_data", |  | ||||||
|                 needsUrls: (args) => args[1].split(";"), |  | ||||||
|                 args: [ |  | ||||||
|                     { |  | ||||||
|                         name: "url", |  | ||||||
|                         required: true, |  | ||||||
|                         doc: "The attribute containing the url where to fetch more data" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "host", |  | ||||||
|                         required: true, |  | ||||||
|                         doc: "The domain name(s) where data might be fetched from - this is needed to set the CSP. A domain must include 'https', e.g. 'https://example.com'. For multiple domains, separate them with ';'. If you don't know the possible domains, use '*'. " |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "readonly", |  | ||||||
|                         required: false, |  | ||||||
|                         doc: "If 'yes', will not show 'apply'-buttons" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 docs: "Gives an interactive element which shows a tag comparison between the OSM-object and the upstream object. This allows to copy some or all tags into OSM", |  | ||||||
|                 constr( |  | ||||||
|                     state: SpecialVisualizationState, |  | ||||||
|                     tagSource: UIEventSource<Record<string, string>>, |  | ||||||
|                     args: string[], |  | ||||||
|                     feature: Feature, |  | ||||||
|                     layer: LayerConfig |  | ||||||
|                 ): BaseUIElement { |  | ||||||
|                     const url = args[0] |  | ||||||
|                     const readonly = args[3] === "yes" |  | ||||||
|                     const externalData = Stores.FromPromiseWithErr(Utils.downloadJson(url)) |  | ||||||
|                     return new SvelteUIElement(ComparisonTool, { |  | ||||||
|                         url, |  | ||||||
|                         state, |  | ||||||
|                         tags: tagSource, |  | ||||||
|                         layer, |  | ||||||
|                         feature, |  | ||||||
|                         readonly, |  | ||||||
|                         externalData |  | ||||||
|                     }) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|                 funcName: "linked_data_from_website", |  | ||||||
|                 docs: "Attempts to load (via a proxy) the specified website and parsed ld+json from there. Suitable data will be offered to import into OSM", |  | ||||||
|                 args: [ |  | ||||||
|                     { |  | ||||||
|                         name: "key", |  | ||||||
|                         defaultValue: "website", |  | ||||||
|                         doc: "Attempt to load ld+json from the specified URL. This can be in an embedded <script type='ld+json'>" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "useProxy", |  | ||||||
|                         defaultValue: "yes", |  | ||||||
|                         doc: "If 'yes', uses the provided proxy server. This proxy server will scrape HTML and search for a script with `lang='ld+json'`. If `no`, the data will be downloaded and expects a linked-data-json directly" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "host", |  | ||||||
|                         doc: "If not using a proxy, define what host the website is allowed to connect to" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "mode", |  | ||||||
|                         doc: "If `display`, only show the data in tabular and readonly form, ignoring already existing tags. This is used to explicitly show all the tags. If unset or anything else, allow to apply/import on OSM" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "collapsed", |  | ||||||
|                         defaultValue: "yes", |  | ||||||
|                         doc: "If the containing accordion should be closed" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 needsUrls: [Constants.linkedDataProxy, "http://www.schema.org"], |  | ||||||
|                 constr( |  | ||||||
|                     state: SpecialVisualizationState, |  | ||||||
|                     tags: UIEventSource<Record<string, string>>, |  | ||||||
|                     argument: string[], |  | ||||||
|                     feature: Feature, |  | ||||||
|                     layer: LayerConfig |  | ||||||
|                 ): BaseUIElement { |  | ||||||
|                     const key = argument[0] ?? "website" |  | ||||||
|                     const useProxy = argument[1] !== "no" |  | ||||||
|                     const readonly = argument[3] === "readonly" |  | ||||||
|                     const isClosed = (argument[4] ?? "yes") === "yes" |  | ||||||
| 
 |  | ||||||
|                     const countryStore: Store<string | undefined> = tags.mapD( |  | ||||||
|                         (tags) => tags._country |  | ||||||
|                     ) |  | ||||||
|                     const sourceUrl: Store<string | undefined> = tags.mapD((tags) => { |  | ||||||
|                         if (!tags[key] || tags[key] === "undefined") { |  | ||||||
|                             return null |  | ||||||
|                         } |  | ||||||
|                         return tags[key] |  | ||||||
|                     }) |  | ||||||
|                     const externalData: Store<{ success: GeoJsonProperties } | { error: any }> = |  | ||||||
|                         sourceUrl.bindD( |  | ||||||
|                             (url) => { |  | ||||||
|                                 const country = countryStore.data |  | ||||||
|                                 if (url.startsWith("https://data.velopark.be/")) { |  | ||||||
|                                     return Stores.FromPromiseWithErr( |  | ||||||
|                                         (async () => { |  | ||||||
|                                             try { |  | ||||||
|                                                 const loadAll = |  | ||||||
|                                                     layer.id.toLowerCase().indexOf("maproulette") >= |  | ||||||
|                                                     0 // Dirty hack
 |  | ||||||
|                                                 const features = |  | ||||||
|                                                     await LinkedDataLoader.fetchVeloparkEntry( |  | ||||||
|                                                         url, |  | ||||||
|                                                         loadAll |  | ||||||
|                                                     ) |  | ||||||
|                                                 const feature = |  | ||||||
|                                                     features.find( |  | ||||||
|                                                         (f) => f.properties["ref:velopark"] === url |  | ||||||
|                                                     ) ?? features[0] |  | ||||||
|                                                 const properties = feature.properties |  | ||||||
|                                                 properties["ref:velopark"] = url |  | ||||||
|                                                 console.log( |  | ||||||
|                                                     "Got properties from velopark:", |  | ||||||
|                                                     properties |  | ||||||
|                                                 ) |  | ||||||
|                                                 return properties |  | ||||||
|                                             } catch (e) { |  | ||||||
|                                                 console.error(e) |  | ||||||
|                                                 throw e |  | ||||||
|                                             } |  | ||||||
|                                         })() |  | ||||||
|                                     ) |  | ||||||
|                                 } |  | ||||||
|                                 if (country === undefined) { |  | ||||||
|                                     return undefined |  | ||||||
|                                 } |  | ||||||
|                                 return Stores.FromPromiseWithErr( |  | ||||||
|                                     (async () => { |  | ||||||
|                                         try { |  | ||||||
|                                             return await LinkedDataLoader.fetchJsonLd( |  | ||||||
|                                                 url, |  | ||||||
|                                                 { country }, |  | ||||||
|                                                 useProxy ? "proxy" : "fetch-lod" |  | ||||||
|                                             ) |  | ||||||
|                                         } catch (e) { |  | ||||||
|                                             console.log( |  | ||||||
|                                                 "Could not get with proxy/download LOD, attempting to download directly. Error for ", |  | ||||||
|                                                 url, |  | ||||||
|                                                 "is", |  | ||||||
|                                                 e |  | ||||||
|                                             ) |  | ||||||
|                                             return await LinkedDataLoader.fetchJsonLd( |  | ||||||
|                                                 url, |  | ||||||
|                                                 { country }, |  | ||||||
|                                                 "fetch-raw" |  | ||||||
|                                             ) |  | ||||||
|                                         } |  | ||||||
|                                     })() |  | ||||||
|                                 ) |  | ||||||
|                             }, |  | ||||||
|                             [countryStore] |  | ||||||
|                         ) |  | ||||||
| 
 |  | ||||||
|                     externalData.addCallbackAndRunD((lod) => |  | ||||||
|                         console.log("linked_data_from_website received the following data:", lod) |  | ||||||
|                     ) |  | ||||||
| 
 |  | ||||||
|                     return new Toggle( |  | ||||||
|                         new SvelteUIElement(ComparisonTool, { |  | ||||||
|                             feature, |  | ||||||
|                             state, |  | ||||||
|                             tags, |  | ||||||
|                             layer, |  | ||||||
|                             externalData, |  | ||||||
|                             sourceUrl, |  | ||||||
|                             readonly, |  | ||||||
|                             collapsed: isClosed |  | ||||||
|                         }), |  | ||||||
|                         undefined, |  | ||||||
|                         sourceUrl.map((url) => !!url) |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
| 
 | 
 | ||||||
|             { |             { | ||||||
|                 funcName: "preset_description", |                 funcName: "preset_description", | ||||||
|  | @ -1206,9 +583,6 @@ export default class SpecialVisualizations { | ||||||
|                 constr( |                 constr( | ||||||
|                     state: SpecialVisualizationState, |                     state: SpecialVisualizationState, | ||||||
|                     tagSource: UIEventSource<Record<string, string>>, |                     tagSource: UIEventSource<Record<string, string>>, | ||||||
|                     argument: string[], |  | ||||||
|                     feature: Feature, |  | ||||||
|                     layer: LayerConfig |  | ||||||
|                 ): BaseUIElement { |                 ): BaseUIElement { | ||||||
|                     const translation = tagSource.map((tags) => { |                     const translation = tagSource.map((tags) => { | ||||||
|                         const layer = state.theme.getMatchingLayer(tags) |                         const layer = state.theme.getMatchingLayer(tags) | ||||||
|  | @ -1219,38 +593,6 @@ export default class SpecialVisualizations { | ||||||
|             }, |             }, | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|             { |  | ||||||
|                 funcName: "group", |  | ||||||
|                 docs: "A collapsable group (accordion)", |  | ||||||
|                 args: [ |  | ||||||
|                     { |  | ||||||
|                         name: "header", |  | ||||||
|                         doc: "The _identifier_ of a single tagRendering. This will be used as header" |  | ||||||
|                     }, |  | ||||||
|                     { |  | ||||||
|                         name: "labels", |  | ||||||
|                         doc: "A `;`-separated list of either identifiers or label names. All tagRenderings matching this value will be shown in the accordion" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 constr( |  | ||||||
|                     state: SpecialVisualizationState, |  | ||||||
|                     tags: UIEventSource<Record<string, string>>, |  | ||||||
|                     argument: string[], |  | ||||||
|                     selectedElement: Feature, |  | ||||||
|                     layer: LayerConfig |  | ||||||
|                 ): SvelteUIElement { |  | ||||||
|                     const [header, labelsStr] = argument |  | ||||||
|                     const labels = labelsStr.split(";").map((x) => x.trim()) |  | ||||||
|                     return new SvelteUIElement<any, any, any>(GroupedView, { |  | ||||||
|                         state, |  | ||||||
|                         tags, |  | ||||||
|                         selectedElement, |  | ||||||
|                         layer, |  | ||||||
|                         header, |  | ||||||
|                         labels |  | ||||||
|                     }) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             { |             { | ||||||
|                 funcName: "preset_type_select", |                 funcName: "preset_type_select", | ||||||
|                 docs: "An editable tag rendering which allows to change the type", |                 docs: "An editable tag rendering which allows to change the type", | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue