From 8bcc8820acf8cc6d3e311f3ee53d04db2b2df187 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 17 Jan 2024 18:08:14 +0100 Subject: [PATCH] Furhter improvements to velopark: better icons, improvements to loading --- assets/themes/velopark/velopark.json | 293 ++++++++++-------- scripts/generateIncludedImages.ts | 1 + scripts/velopark/veloParkToGeojson.ts | 49 +-- src/Logic/Web/VeloparkLoader.ts | 234 +++++++++----- src/Models/Constants.ts | 4 +- src/UI/Image/NearbyImages.svelte | 2 +- src/UI/Map/Icon.svelte | 91 +++--- .../MapRoulette/MaprouletteSetStatus.svelte | 2 +- src/UI/SpecialVisualizations.ts | 261 +++++++++------- src/assets/svg/Square_rounded.svelte | 4 + 10 files changed, 560 insertions(+), 381 deletions(-) create mode 100644 src/assets/svg/Square_rounded.svelte diff --git a/assets/themes/velopark/velopark.json b/assets/themes/velopark/velopark.json index 7050dd042..be17bb702 100644 --- a/assets/themes/velopark/velopark.json +++ b/assets/themes/velopark/velopark.json @@ -25,135 +25,186 @@ "defaultBackgroundId": "photo", "layers": [ { - "builtin": "maproulette_challenge", - "override": { - "=name": { - "en": "Velopark data", - "nl": "Velopark data" - }, - "=filter": [ - { - "id": "created-only", - "options": [ - { - "question": { - "en": "Only unfinished tasks", - "nl": "Enkel onafgewerkte taken" - }, - "osmTags": "mr_taskStatus=Created", - "default": true - } - ] + "id": "velopark_maproulette", + "description": "Maproulette challenge containing velopark data", + "source": { + "osmTags": "mr_taskId~*", + "geoJson": "https://maproulette.org/api/v2/challenge/view/43282", + "isOsmCache": false + }, + "title": { + "render": "Velopark parking {mr_velopark_id}" + }, + "name": { + "en": "Velopark data", + "nl": "Velopark data" + }, + "titleIcons": [ + { + "id": "maproulette", + "render": "" + } + ], + "tagRenderings": [ + { + "id": "velopark-id-display", + "render": { + "*": "{ref:velopark}" } - ], - "calculatedTags+": [ - "mr_velopark_id=feat.properties['ref:velopark']?.split('/')?.at(-1)", - "_nearby_bicycle_parkings=closestn(feat)(['bike_parking','bike_parking_with_velopark_ref'], 100, undefined, 25)", - "_nearby_bicycle_parkings:count=get(feat)('_nearby_bicycle_parkings').length", - "_nearby_bicycle_parkings:props=get(feat)('_nearby_bicycle_parkings').map(f => ({_distance: Math.round(f.distance), _ref: feat.properties['ref:velopark'], _mr_id: feat.properties.id, '_velopark:id': (f.feat.properties['_velopark:id'] ?? 'unlinked') /*Explicit copy to trigger lazy loading*/, ...f.feat.properties}))" - ], - "=title": { - "render": "Velopark parking {mr_velopark_id}" }, - "source": { - "geoJson": "https://maproulette.org/api/v2/challenge/view/43282" + { + "id": "velopark-link", + "render": { + "special": { + "type": "link", + "href": "https://www.velopark.be/static/data/{mr_velopark_id}", + "text": { + "en": "See on velopark (webpage)", + "nl": "Bekijk op Velopark (webpagina)" + } + } + } }, - "=tagRenderings": [ - { - "id": "velopark-link", - "render": { - "special": { - "type": "link", - "href": "https://www.velopark.be/static/data/{mr_velopark_id}", - "text": { - "en": "See on velopark (webpage)", - "nl": "Bekijk op Velopark (webpagina)" - } + { + "id": "velopark-data-link", + "render": { + "special": { + "type": "link", + "href": "{ref:velopark}", + "text": "Inspect raw data on velopark.be" + } + } + }, + { + "id": "show-data-velopark", + "render": { + "special": { + "type": "compare_data", + "url": "ref:velopark", + "host": "https://data.velopark.be", + "postprocessing": "velopark", + "readonly": "yes" + } + } + }, + { + "id": "closest_parkings", + "render": { + "*": "There are {_nearby_bicycle_parkings:count} bicycle parkings within 25m known in OpenStreetMap. " + } + }, + { + "id": "list_nearby_bike_parkings", + "render": { + "special": { + "type": "multi", + "key": "_nearby_bicycle_parkings:props", + "tagrendering": "{id} ({_distance}m, {_velopark:id}) {minimap(20)} {tag_apply(ref:velopark=$_ref,Link,link,id,_mr_id)}" + } + } + }, + { + "id": "import_point", + "render": { + "special": { + "type": "import_button", + "targetLayer": "bike_parking_with_velopark_ref bike_parking", + "tags": "amenity=bicycle_parking;ref:velopark=$ref:velopark", + "text": { + "en": "Create a new bicycle parking in OSM", + "nl": "Maak een nieuwe parking aan in OSM" + }, + "maproulette_id": "mr_taskId" + } + } + }, + { + "id": "close_mr", + "render": { + "special": { + "type": "maproulette_set_status", + "message": { + "en": "Mark this item as linked", + "nl": "Markeer als gelinkt" } } - }, - { - "id": "velopark-data-link", - "render": { - "special": { - "type": "link", - "href": "{ref:velopark}", - "text": "Inspect raw data on velopark.be" - } + } + }, + { + "id": "close_mr_incorrect", + "render": { + "special": { + "type": "maproulette_set_status", + "message": { + "en": "Mark this item as incorrect (duplicate, does not exist anymore, contradictory data)", + "nl": "Markeer dit object als incorrect (duplicaatin, incorrect of tegenstrijdige data, ...)" + }, + "image": "invalid", + "status": 6 } - }, - { - "id": "show-data-velopark", - "render": { - "special": { - "type": "compare_data", - "url": "ref:velopark", - "host": "https://data.velopark.be", - "postprocessing": "velopark", - "readonly": "yes" - } + } + }, + "{nearby_images(open,readonly)}" + ], + "lineRendering": [], + "filter": [ + { + "id": "created-only", + "options": [ + { + "question": { + "en": "Only unfinished tasks", + "nl": "Enkel onafgewerkte taken" + }, + "osmTags": "mr_taskStatus=Created", + "default": true } - }, - { - "id": "closest_parkings", - "render": { - "*": "There are {_nearby_bicycle_parkings:count} bicycle parkings within 25m known in OpenStreetMap. " + ] + }, + { + "id": "too-hard-only", + "options": [ + { + "question": { + "en": "Only too-hard tasks", + "nl": "Enkel foutieve taken" + }, + "osmTags": "mr_taskStatus=Too_hard" } + ] + } + ], + "calculatedTags": [ + "mr_velopark_id=feat.properties['ref:velopark']?.split('/')?.at(-1)", + "_nearby_bicycle_parkings=closestn(feat)(['bike_parking','bike_parking_with_velopark_ref'], 100, undefined, 25)", + "_nearby_bicycle_parkings:count=get(feat)('_nearby_bicycle_parkings').length", + "_nearby_bicycle_parkings:props=get(feat)('_nearby_bicycle_parkings').map(f => ({_distance: Math.round(f.distance), _ref: feat.properties['ref:velopark'], _mr_id: feat.properties.id, '_velopark:id': (f.feat.properties['_velopark:id'] ?? 'unlinked') /*Explicit copy to trigger lazy loading*/, ...f.feat.properties}))" + ], + "pointRendering": [ + { + "location": [ + "point", + "centroid" + ], + "marker": [ + { + "icon": "square_rounded", + "color": "#ffffff88" + }, + { + "icon": "./assets/themes/velopark/velopark.svg" } - }, - { - "id": "list_nearby_bike_parkings", - "render": { - "special": { - "type": "multi", - "key": "_nearby_bicycle_parkings:props", - "tagrendering": "{id} ({_distance}m, {_velopark:id}) {minimap(20)} {tag_apply(ref:velopark=$_ref,Link,link,id,_mr_id)}" - } - } - }, - { - "id": "import_point", - "render": { - "special": { - "type": "import_button", - "targetLayer": "bike_parking_with_velopark_ref bike_parking", - "tags": "amenity=bicycle_parking;ref:velopark=$ref:velopark", - "text": { - "en": "Create a new bicycle parking in OSM", - "nl": "Maak een nieuwe parking aan in OSM" - }, - "maproulette_id": "mr_taskId" - } - } - }, - { - "id": "close_mr", - "render": { - "special": { - "type": "maproulette_set_status", - "message": { - "en": "Mark this item as linked", - "nl": "Markeer als gelinkt" - } - } - } - }, - { - "id": "close_mr_incorrect", - "render": { - "special": { - "type": "maproulette_set_status", - "message": { - "en": "Mark this item as incorrect (duplicate, does not exist anymore, contradictory data)", - "nl": "Markeer dit object als incorrect (duplicaatin, incorrect of tegenstrijdige data, ...)" - }, - "image": "bug", - "status": 6 - } - } - }, - "{nearby_images()}" - ] - } + ], + "iconSize": "40,40", + "anchor": "bottom", + "iconBadges": [{ + "if": "mr_taskStatus=Too_Hard", + "then": "invalid" + },{ + "if": "mr_taskStatus=Fixed", + "then": "confirm" + }] + } + ] }, { "builtin": [ diff --git a/scripts/generateIncludedImages.ts b/scripts/generateIncludedImages.ts index 0798c7fe3..08044203b 100644 --- a/scripts/generateIncludedImages.ts +++ b/scripts/generateIncludedImages.ts @@ -82,6 +82,7 @@ function genImages(dryrun = false) { "SocialImageForeground", "speech_bubble_black_outline", "square", + "square_rounded", "star", "star_half", "star_outline", diff --git a/scripts/velopark/veloParkToGeojson.ts b/scripts/velopark/veloParkToGeojson.ts index a3226cf39..0157e9b1f 100644 --- a/scripts/velopark/veloParkToGeojson.ts +++ b/scripts/velopark/veloParkToGeojson.ts @@ -2,15 +2,17 @@ import Script from "../Script" import { Utils } from "../../src/Utils" import VeloparkLoader, { VeloparkData } from "../../src/Logic/Web/VeloparkLoader" import fs from "fs" -import OverpassFeatureSource from "../../src/Logic/FeatureSource/Sources/OverpassFeatureSource" import { Overpass } from "../../src/Logic/Osm/Overpass" import { RegexTag } from "../../src/Logic/Tags/RegexTag" import Constants from "../../src/Models/Constants" import { ImmutableStore } from "../../src/Logic/UIEventSource" import { BBox } from "../../src/Logic/BBox" + class VeloParkToGeojson extends Script { constructor() { - super("Downloads the latest Velopark data and converts it to a geojson, which will be saved at the current directory") + super( + "Downloads the latest Velopark data and converts it to a geojson, which will be saved at the current directory" + ) } async main(args: string[]): Promise { @@ -19,37 +21,48 @@ class VeloParkToGeojson extends Script { const url = "https://www.velopark.be/api/parkings/1000" const data = await Utils.downloadJson(url) - const bboxBelgium = new BBox([[2.51357303225, 49.5294835476],[ 6.15665815596, 51.4750237087]]) - const alreadyLinkedQuery = new Overpass(new RegexTag("ref:velopark", /.+/), + const bboxBelgium = new BBox([ + [2.51357303225, 49.5294835476], + [6.15665815596, 51.4750237087], + ]) + const alreadyLinkedQuery = new Overpass( + new RegexTag("ref:velopark", /.+/), [], Constants.defaultOverpassUrls[0], - new ImmutableStore(60*5), + new ImmutableStore(60 * 5), false - ) + ) const alreadyLinkedFeatures = await alreadyLinkedQuery.queryGeoJson(bboxBelgium) - const seenIds = new Set(alreadyLinkedFeatures[0].features.map(f => f.properties["ref:velopark"])) - const features = data.map(f => VeloparkLoader.convert(f)) - .filter(f => !seenIds.has(f.properties["ref:velopark"])) + const seenIds = new Set( + alreadyLinkedFeatures[0].features.map((f) => f.properties["ref:velopark"]) + ) + console.log("OpenStreetMap contains", seenIds.size, "bicycle parkings with a velopark ref") + const allVelopark = data.map((f) => VeloparkLoader.convert(f)) + const features = allVelopark.filter((f) => !seenIds.has(f.properties["ref:velopark"])) const allProperties = new Set() for (const feature of features) { - Object.keys(feature.properties).forEach(k => allProperties.add(k)) + Object.keys(feature.properties).forEach((k) => allProperties.add(k)) } allProperties.delete("ref:velopark") for (const feature of features) { - allProperties.forEach(k => { + allProperties.forEach((k) => { delete feature.properties[k] }) } - fs.writeFileSync("velopark_id_only_export_" + new Date().toISOString() + ".geojson", JSON.stringify({ - "type": "FeatureCollection", - features, - }, null, " ")) - + fs.writeFileSync( + "velopark_id_only_export_" + new Date().toISOString() + ".geojson", + JSON.stringify( + { + type: "FeatureCollection", + features, + }, + null, + " " + ) + ) } - - } new VeloParkToGeojson().run() diff --git a/src/Logic/Web/VeloparkLoader.ts b/src/Logic/Web/VeloparkLoader.ts index 1101aaaaf..4b0cf8ef0 100644 --- a/src/Logic/Web/VeloparkLoader.ts +++ b/src/Logic/Web/VeloparkLoader.ts @@ -1,4 +1,4 @@ -import { Feature, Geometry, Point } from "geojson" +import { Feature, Geometry } from "geojson" import { OH } from "../../UI/OpeningHours/OpeningHours" import EmailValidator from "../../UI/InputElement/Validators/EmailValidator" import PhoneValidator from "../../UI/InputElement/Validators/PhoneValidator" @@ -12,39 +12,49 @@ import { Utils } from "../../Utils" * Reads a velopark-json, converts it to a geojson */ export default class VeloparkLoader { - private static readonly emailReformatting = new EmailValidator() private static readonly phoneValidator = new PhoneValidator() private static readonly coder = new CountryCoder( Constants.countryCoderEndpoint, - Utils.downloadJson, + Utils.downloadJson ) public static convert(veloparkData: VeloparkData): Feature { - + console.log("Converting", veloparkData) const properties: { - "ref:velopark":string, - "operator:email"?: string, - "operator:phone"?: string, - fee?: string, + "ref:velopark": string + "operator:email"?: string + "operator:phone"?: string + fee?: string opening_hours?: string access?: string maxstay?: string operator?: string } = { - "ref:velopark": veloparkData["id"] ?? veloparkData["@id"] + "ref:velopark": veloparkData["id"] ?? veloparkData["@id"], } + for (const k of ["_id", "url", "dateModified", "name", "address"]) { + delete veloparkData[k] + } + + VeloparkLoader.cleanup(veloparkData["properties"]) + VeloparkLoader.cleanupEmtpy(veloparkData) + properties.operator = veloparkData.operatedBy?.companyName if (veloparkData.contactPoint?.email) { - properties["operator:email"] = VeloparkLoader.emailReformatting.reformat(veloparkData.contactPoint?.email) + properties["operator:email"] = VeloparkLoader.emailReformatting.reformat( + veloparkData.contactPoint?.email + ) } - if (veloparkData.contactPoint?.telephone) { - properties["operator:phone"] = VeloparkLoader.phoneValidator.reformat(veloparkData.contactPoint?.telephone, () => "be") + properties["operator:phone"] = VeloparkLoader.phoneValidator.reformat( + veloparkData.contactPoint?.telephone, + () => "be" + ) } veloparkData.photos?.forEach((p, i) => { @@ -52,130 +62,198 @@ export default class VeloparkLoader { properties["image"] = p.image } else { properties["image:" + i] = p.image - } }) let geometry = veloparkData.geometry for (const g of veloparkData["@graph"]) { + VeloparkLoader.cleanup(g) + VeloparkLoader.cleanupEmtpy(g) if (g.geo[0]) { geometry = { type: "Point", coordinates: [g.geo[0].longitude, g.geo[0].latitude] } } - if (g.maximumParkingDuration?.endsWith("D") && g.maximumParkingDuration?.startsWith("P")) { - const duration = g.maximumParkingDuration.substring(1, g.maximumParkingDuration.length - 1) + if ( + g.maximumParkingDuration?.endsWith("D") && + g.maximumParkingDuration?.startsWith("P") + ) { + const duration = g.maximumParkingDuration.substring( + 1, + g.maximumParkingDuration.length - 1 + ) properties.maxstay = duration + " days" } properties.access = g.publicAccess ? "yes" : "no" const prefix = "http://schema.org/" if (g.openingHoursSpecification) { - const oh = OH.simplify(g.openingHoursSpecification.map(spec => { - const dayOfWeek = spec.dayOfWeek.substring(prefix.length, prefix.length + 2).toLowerCase() - const startHour = spec.opens - const endHour = spec.closes === "23:59" ? "24:00" : spec.closes - const merged = OH.MergeTimes(OH.ParseRule(dayOfWeek + " " + startHour + "-" + endHour)) - return OH.ToString(merged) - }).join("; ")) + const oh = OH.simplify( + g.openingHoursSpecification + .map((spec) => { + const dayOfWeek = spec.dayOfWeek + .substring(prefix.length, prefix.length + 2) + .toLowerCase() + const startHour = spec.opens + const endHour = spec.closes === "23:59" ? "24:00" : spec.closes + const merged = OH.MergeTimes( + OH.ParseRule(dayOfWeek + " " + startHour + "-" + endHour) + ) + return OH.ToString(merged) + }) + .join("; ") + ) properties.opening_hours = oh } if (g.priceSpecification?.[0]) { properties.fee = g.priceSpecification[0].freeOfCharge ? "no" : "yes" } + const types = { + "https://data.velopark.be/openvelopark/terms#RegularBicycle": "_", + "https://data.velopark.be/openvelopark/terms#ElectricBicycle": + "capacity:electric_bicycle", + "https://data.velopark.be/openvelopark/terms#CargoBicycle": "capacity:cargo_bike", + } + let totalCapacity = 0 + for (let i = (g.allows ?? []).length - 1; i >= 0; i--) { + const capacity = g.allows[i] + const type: string = capacity["@type"] + if (type === undefined) { + console.warn("No type found for", capacity.bicycleType) + continue + } + const count = capacity["amount"] + if (!isNaN(count)) { + totalCapacity += Number(count) + } else { + console.warn("Not a valid number while loading velopark data:", count) + } + if (type !== "_") { + // properties[type] = count + } + g.allows.splice(i, 1) + } + if (totalCapacity > 0) { + properties["capacity"] = totalCapacity + } } + console.log(JSON.stringify(properties, null, " ")) + return { type: "Feature", properties, geometry } } + private static cleanup(data: any) { + if (!data?.attributes) { + return + } + for (const k of ["NIS_CODE", "name_NL", "name_DE", "name_EN", "name_FR"]) { + delete data.attributes[k] + } + VeloparkLoader.cleanupEmtpy(data) + } + + private static cleanupEmtpy(data: any) { + for (const key in data) { + if (data[key] === null) { + delete data[key] + continue + } + if (Object.keys(data[key]).length === 0) { + delete data[key] + } + } + } } export interface VeloparkData { geometry?: Geometry - "@context": any, + "@context": any "@id": string // "https://data.velopark.be/data/NMBS_541", - "@type": "BicycleParkingStation", - "dateModified": string, - "identifier": number, - "name": [ + "@type": "BicycleParkingStation" + dateModified: string + identifier: number + name: [ { - "@value": string, + "@value": string "@language": "nl" } - ], - "ownedBy": { - "@id": string, - "@type": "BusinessEntity", - "companyName": string - }, - "operatedBy": { - "@type": "BusinessEntity", - "companyName": string - }, - "address": any, - "hasMap": any, - "contactPoint": { - "@type": "ContactPoint", - "email": string, - "telephone": string - }, - "photos": { - "@type": "Photograph", - "image": string - }[], - "interactionService": { - "@type": "WebSite", - "url": string - }, + ] + ownedBy: { + "@id": string + "@type": "BusinessEntity" + companyName: string + } + operatedBy: { + "@type": "BusinessEntity" + companyName: string + } + address: any + hasMap: any + contactPoint: { + "@type": "ContactPoint" + email: string + telephone: string + } + photos: { + "@type": "Photograph" + image: string + }[] + interactionService: { + "@type": "WebSite" + url: string + } /** * Contains various extra pieces of data, e.g. services or opening hours */ "@graph": [ { - "@type": "https://data.velopark.be/openvelopark/terms#PublicBicycleParking", - "openingHoursSpecification": { - "@type": "OpeningHoursSpecification", + "@type": "https://data.velopark.be/openvelopark/terms#PublicBicycleParking" + openingHoursSpecification: { + "@type": "OpeningHoursSpecification" /** * Ends with 'Monday', 'Tuesday', ... */ - "dayOfWeek": "http://schema.org/Monday" + dayOfWeek: + | "http://schema.org/Monday" | "http://schema.org/Tuesday" | "http://schema.org/Wednesday" | "http://schema.org/Thursday" | "http://schema.org/Friday" | "http://schema.org/Saturday" - | "http://schema.org/Sunday", + | "http://schema.org/Sunday" /** * opens: 00:00 and closes 23:59 for the entire day */ - "opens": string, - "closes": string - }[], + opens: string + closes: string + }[] /** * P30D = 30 days */ - "maximumParkingDuration": "P30D", - "publicAccess": true, - "totalCapacity": 110, - "allows": [ + maximumParkingDuration: "P30D" + publicAccess: true + totalCapacity: 110 + allows: [ { - "@type": "AllowedBicycle", + "@type": "AllowedBicycle" /* TODO is cargo bikes etc also available?*/ - "bicycleType": "https://data.velopark.be/openvelopark/terms#RegularBicycle", - "bicyclesAmount": number + bicycleType: + | string + | "https://data.velopark.be/openvelopark/terms#RegularBicycle" + bicyclesAmount: number } - ], - "geo": [ + ] + geo: [ { - "@type": "GeoCoordinates", - "latitude": number, - "longitude": number + "@type": "GeoCoordinates" + latitude: number + longitude: number } - ], - "priceSpecification": [ + ] + priceSpecification: [ { - "@type": "PriceSpecification", - "freeOfCharge": boolean + "@type": "PriceSpecification" + freeOfCharge: boolean } ] } ] - } diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts index f2c058c66..1b3b71937 100644 --- a/src/Models/Constants.ts +++ b/src/Models/Constants.ts @@ -36,7 +36,7 @@ export default class Constants { "import_candidate", "usersettings", "icons", - "filters" + "filters", ] as const /** * Layer IDs of layers which have special properties through built-in hooks @@ -117,7 +117,9 @@ export default class Constants { */ private static readonly _defaultPinIcons = [ "pin", + "bug", "square", + "square_rounded", "circle", "checkmark", "clock", diff --git a/src/UI/Image/NearbyImages.svelte b/src/UI/Image/NearbyImages.svelte index 375b1b449..11e5c4d0c 100644 --- a/src/UI/Image/NearbyImages.svelte +++ b/src/UI/Image/NearbyImages.svelte @@ -55,7 +55,7 @@
{#each $images as image (image.pictureUrl)} - + {/each}
diff --git a/src/UI/Map/Icon.svelte b/src/UI/Map/Icon.svelte index 3265ec7de..e1c386ddb 100644 --- a/src/UI/Map/Icon.svelte +++ b/src/UI/Map/Icon.svelte @@ -1,47 +1,49 @@ @@ -50,6 +52,11 @@ {:else if icon === "square"} + {:else if icon === "square_rounded"} + + {:else if icon === "bug"} + + {:else if icon === "circle"} {:else if icon === "checkmark"} @@ -117,7 +124,7 @@ {:else if icon === "addSmall"} {:else if icon === "link"} - + {:else} {/if} diff --git a/src/UI/MapRoulette/MaprouletteSetStatus.svelte b/src/UI/MapRoulette/MaprouletteSetStatus.svelte index 18bccf608..908ec8b93 100644 --- a/src/UI/MapRoulette/MaprouletteSetStatus.svelte +++ b/src/UI/MapRoulette/MaprouletteSetStatus.svelte @@ -54,7 +54,7 @@ {:else if $status === Maproulette.STATUS_OPEN} - diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index 5bfc47024..8f7a7e001 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -3,7 +3,11 @@ import { FixedUiElement } from "./Base/FixedUiElement" import BaseUIElement from "./BaseUIElement" import Title from "./Base/Title" import Table from "./Base/Table" -import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization" +import { + RenderingSpecification, + SpecialVisualization, + SpecialVisualizationState, +} from "./SpecialVisualization" import { HistogramViz } from "./Popup/HistogramViz" import { MinimapViz } from "./Popup/MinimapViz" import { ShareLinkViz } from "./Popup/ShareLinkViz" @@ -94,6 +98,11 @@ class NearbyImageVis implements SpecialVisualization { defaultValue: "closed", doc: "Either `open` or `closed`. If `open`, then the image carousel will always be shown", }, + { + name: "readonly", + required: false, + doc: "If 'readonly', will not show the 'link'-button", + }, ] docs = "A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature" @@ -106,9 +115,10 @@ class NearbyImageVis implements SpecialVisualization { tags: UIEventSource>, args: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { const isOpen = args[0] === "open" + const readonly = args[1] === "readonly" const [lon, lat] = GeoOperations.centerpointCoordinates(feature) return new SvelteUIElement(isOpen ? NearbyImages : NearbyImagesCollapsed, { tags, @@ -117,6 +127,7 @@ class NearbyImageVis implements SpecialVisualization { lat, feature, layer, + linkable: !readonly, }) } } @@ -171,7 +182,7 @@ class StealViz implements SpecialVisualization { selectedElement: otherFeature, state, layer, - }), + }) ) } if (elements.length === 1) { @@ -179,8 +190,8 @@ class StealViz implements SpecialVisualization { } return new Combine(elements).SetClass("flex flex-col") }, - [state.indexedFeatures.featuresById], - ), + [state.indexedFeatures.featuresById] + ) ) } @@ -219,7 +230,7 @@ export class QuestionViz implements SpecialVisualization { tags: UIEventSource>, args: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { const labels = args[0] ?.split(";") @@ -273,8 +284,10 @@ export default class SpecialVisualizations { * templ.args[0] = "{email}" */ public static constructSpecification( - template: string | { special: Record> & { type: string } }, - extraMappings: SpecialVisualization[] = [], + template: + | string + | { special: Record> & { type: string } }, + extraMappings: SpecialVisualization[] = [] ): RenderingSpecification[] { if (template === "") { return [] @@ -283,7 +296,7 @@ export default class SpecialVisualizations { if (typeof template !== "string") { console.trace( "Got a non-expanded template while constructing the specification, it still has a 'special-key':", - template, + template ) throw "Got a non-expanded template while constructing the specification" } @@ -291,20 +304,20 @@ export default class SpecialVisualizations { for (const knownSpecial of allKnownSpecials) { // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way' const matched = template.match( - new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s"), + new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s") ) if (matched != null) { // We found a special component that should be brought to live const partBefore = SpecialVisualizations.constructSpecification( matched[1], - extraMappings, + extraMappings ) const argument = matched[2] /* .trim() // We don't trim, as spaces might be relevant, e.g. "what is ... of {title()}"*/ const style = matched[3]?.substring(1) ?? "" const partAfter = SpecialVisualizations.constructSpecification( matched[4], - extraMappings, + extraMappings ) const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "") if (argument.length > 0) { @@ -343,31 +356,31 @@ export default class SpecialVisualizations { viz.docs, viz.args.length > 0 ? new Table( - ["name", "default", "description"], - viz.args.map((arg) => { - let defaultArg = arg.defaultValue ?? "_undefined_" - if (defaultArg == "") { - defaultArg = "_empty string_" - } - return [arg.name, defaultArg, arg.doc] - }), - ) + ["name", "default", "description"], + viz.args.map((arg) => { + let defaultArg = arg.defaultValue ?? "_undefined_" + if (defaultArg == "") { + defaultArg = "_empty string_" + } + return [arg.name, defaultArg, arg.doc] + }) + ) : undefined, new Title("Example usage of " + viz.funcName, 4), new FixedUiElement( viz.example ?? - "`{" + - viz.funcName + - "(" + - viz.args.map((arg) => arg.defaultValue).join(",") + - ")}`", + "`{" + + viz.funcName + + "(" + + viz.args.map((arg) => arg.defaultValue).join(",") + + ")}`" ).SetClass("literal-code"), ]) } public static HelpMessage() { const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) => - SpecialVisualizations.DocumentationFor(viz), + SpecialVisualizations.DocumentationFor(viz) ) return new Combine([ @@ -401,10 +414,10 @@ export default class SpecialVisualizations { }, }, null, - " ", - ), + " " + ) ).SetClass("code"), - "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)', ]).SetClass("flex flex-col"), ...helpTexts, ]).SetClass("flex flex-col") @@ -413,20 +426,20 @@ export default class SpecialVisualizations { // noinspection JSUnusedGlobalSymbols public static renderExampleOfSpecial( state: SpecialVisualizationState, - s: SpecialVisualization, + s: SpecialVisualization ): BaseUIElement { const examples = s.structuredExamples === undefined ? [] : s.structuredExamples().map((e) => { - return s.constr( - state, - new UIEventSource>(e.feature.properties), - e.args, - e.feature, - undefined, - ) - }) + return s.constr( + state, + new UIEventSource>(e.feature.properties), + e.args, + e.feature, + undefined + ) + }) return new Combine([new Title(s.funcName), s.docs, ...examples]) } @@ -466,7 +479,7 @@ export default class SpecialVisualizations { assignTo: state.userRelatedState.language, availableLanguages: state.layout.language, preferredLanguages: state.osmConnection.userDetails.map( - (ud) => ud.languages, + (ud) => ud.languages ), }) }, @@ -491,7 +504,7 @@ export default class SpecialVisualizations { constr( state: SpecialVisualizationState, - tagSource: UIEventSource>, + tagSource: UIEventSource> ): BaseUIElement { return new VariableUiElement( tagSource @@ -501,7 +514,7 @@ export default class SpecialVisualizations { return new SplitRoadWizard(id, state) } return undefined - }), + }) ) }, }, @@ -515,7 +528,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { if (feature.geometry.type !== "Point") { return undefined @@ -538,7 +551,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { if (!layer.deletion) { return undefined @@ -566,7 +579,7 @@ export default class SpecialVisualizations { state: SpecialVisualizationState, tagSource: UIEventSource>, argument: string[], - feature: Feature, + feature: Feature ): BaseUIElement { const [lon, lat] = GeoOperations.centerpointCoordinates(feature) return new SvelteUIElement(CreateNewNote, { @@ -630,7 +643,7 @@ export default class SpecialVisualizations { .map((tags) => tags[args[0]]) .map((wikidata) => { wikidata = Utils.NoEmpty( - wikidata?.split(";")?.map((wd) => wd.trim()) ?? [], + wikidata?.split(";")?.map((wd) => wd.trim()) ?? [] )[0] const entry = Wikidata.LoadWikidataEntry(wikidata) return new VariableUiElement( @@ -640,9 +653,9 @@ export default class SpecialVisualizations { } const response = e["success"] return Translation.fromMap(response.labels) - }), + }) ) - }), + }) ), }, new MapillaryLinkVis(), @@ -674,7 +687,7 @@ export default class SpecialVisualizations { AllImageProviders.LoadImagesFor(tags, imagePrefixes), tags, state, - feature, + feature ) }, }, @@ -730,7 +743,7 @@ export default class SpecialVisualizations { { nameKey: nameKey, fallbackName, - }, + } ) return new SvelteUIElement(StarsBarIcon, { score: reviews.average, @@ -763,7 +776,7 @@ export default class SpecialVisualizations { { nameKey: nameKey, fallbackName, - }, + } ) return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer }) }, @@ -795,7 +808,7 @@ export default class SpecialVisualizations { { nameKey: nameKey, fallbackName, - }, + } ) return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer }) }, @@ -853,7 +866,7 @@ export default class SpecialVisualizations { tags: UIEventSource>, args: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): SvelteUIElement { const keyToUse = args[0] const prefix = args[1] @@ -890,10 +903,10 @@ export default class SpecialVisualizations { return undefined } const allUnits: Unit[] = [].concat( - ...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []), + ...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []) ) const unit = allUnits.filter((unit) => - unit.isApplicableToKey(key), + unit.isApplicableToKey(key) )[0] if (unit === undefined) { return value @@ -901,7 +914,7 @@ export default class SpecialVisualizations { const getCountry = () => tagSource.data._country const [v, denom] = unit.findDenomination(value, getCountry) return unit.asHumanLongValue(v, getCountry) - }), + }) ) }, }, @@ -918,7 +931,7 @@ export default class SpecialVisualizations { new Combine([ t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"), t.downloadGeoJsonHelper.SetClass("subtle"), - ]).SetClass("flex flex-col"), + ]).SetClass("flex flex-col") ) .onClick(() => { console.log("Exporting as Geojson") @@ -931,7 +944,7 @@ export default class SpecialVisualizations { title + "_mapcomplete_export.geojson", { mimetype: "application/vnd.geo+json", - }, + } ) }) .SetClass("w-full") @@ -967,7 +980,7 @@ export default class SpecialVisualizations { constr: (state) => { return new SubtleButton( Svg.delete_icon_svg().SetStyle("height: 1.5rem"), - Translations.t.general.removeLocationHistory, + Translations.t.general.removeLocationHistory ).onClick(() => { state.historicalUserLocations.features.setData([]) state.selectedElement.setData(undefined) @@ -1005,10 +1018,10 @@ export default class SpecialVisualizations { .filter((c) => c.text !== "") .map( (c, i) => - new NoteCommentElement(c, state, i, comments.length), - ), + new NoteCommentElement(c, state, i, comments.length) + ) ).SetClass("flex flex-col") - }), + }) ), }, { @@ -1049,9 +1062,9 @@ export default class SpecialVisualizations { return undefined } return new SubstitutedTranslation(title, tagsSource, state).SetClass( - "px-1", + "px-1" ) - }), + }) ), }, { @@ -1067,8 +1080,8 @@ export default class SpecialVisualizations { let challenge = Stores.FromPromise( Utils.downloadJsonCached( `${Maproulette.defaultEndpoint}/challenge/${parentId}`, - 24 * 60 * 60 * 1000, - ), + 24 * 60 * 60 * 1000 + ) ) return new VariableUiElement( @@ -1093,7 +1106,7 @@ export default class SpecialVisualizations { } else { return [title, new List(listItems)] } - }), + }) ) }, docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.", @@ -1107,15 +1120,15 @@ export default class SpecialVisualizations { "\n" + "```json\n" + "{\n" + - " \"id\": \"mark_duplicate\",\n" + - " \"render\": {\n" + - " \"special\": {\n" + - " \"type\": \"maproulette_set_status\",\n" + - " \"message\": {\n" + - " \"en\": \"Mark as not found or false positive\"\n" + + ' "id": "mark_duplicate",\n' + + ' "render": {\n' + + ' "special": {\n' + + ' "type": "maproulette_set_status",\n' + + ' "message": {\n' + + ' "en": "Mark as not found or false positive"\n' + " },\n" + - " \"status\": \"2\",\n" + - " \"image\": \"close\"\n" + + ' "status": "2",\n' + + ' "image": "close"\n' + " }\n" + " }\n" + "}\n" + @@ -1181,8 +1194,8 @@ export default class SpecialVisualizations { const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox) return new StatisticsPanel(fsBboxed) }, - [state.mapProperties.bounds], - ), + [state.mapProperties.bounds] + ) ) }, }, @@ -1248,7 +1261,7 @@ export default class SpecialVisualizations { constr( state: SpecialVisualizationState, tagSource: UIEventSource>, - args: string[], + args: string[] ): BaseUIElement { let [text, href, classnames, download, ariaLabel] = args if (download === "") { @@ -1265,8 +1278,8 @@ export default class SpecialVisualizations { download: Utils.SubstituteKeys(download, tags), ariaLabel: Utils.SubstituteKeys(ariaLabel, tags), newTab, - }), - ), + }) + ) ) }, }, @@ -1288,7 +1301,7 @@ export default class SpecialVisualizations { }, }, null, - " ", + " " ) + "\n```", args: [ @@ -1310,26 +1323,30 @@ export default class SpecialVisualizations { featureTags.map((tags) => { try { const data = tags[key] - const properties: object[] = typeof data === "string" ? JSON.parse(tags[key]) : data + const properties: object[] = + typeof data === "string" ? JSON.parse(tags[key]) : data const elements = [] for (const property of properties) { const subsTr = new SubstitutedTranslation( translation, new UIEventSource(property), - state, + state ) elements.push(subsTr) } return new List(elements) } catch (e) { - console.log("Something went wrong while generating the elements for a multi", { - e, - tags, - key, - loaded: tags[key], - }) + console.log( + "Something went wrong while generating the elements for a multi", + { + e, + tags, + key, + loaded: tags[key], + } + ) } - }), + }) ) }, }, @@ -1349,7 +1366,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { return new VariableUiElement( tagSource.map((tags) => { @@ -1361,7 +1378,7 @@ export default class SpecialVisualizations { console.error("Cannot create a translation for", v, "due to", e) return JSON.stringify(v) } - }), + }) ) }, }, @@ -1381,7 +1398,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { const key = argument[0] const validator = new FediverseValidator() @@ -1391,14 +1408,14 @@ export default class SpecialVisualizations { .map((fediAccount) => { fediAccount = validator.reformat(fediAccount) const [_, username, host] = fediAccount.match( - FediverseValidator.usernameAtServer, + FediverseValidator.usernameAtServer ) return new SvelteUIElement(Link, { text: fediAccount, url: "https://" + host + "/@" + username, newTab: true, }) - }), + }) ) }, }, @@ -1418,7 +1435,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, args: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { return new FixedUiElement("{" + args[0] + "}") }, @@ -1439,7 +1456,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { const key = argument[0] ?? "value" return new VariableUiElement( @@ -1457,12 +1474,12 @@ export default class SpecialVisualizations { } catch (e) { return new FixedUiElement( "Could not parse this tag: " + - JSON.stringify(value) + - " due to " + - e, + JSON.stringify(value) + + " due to " + + e ).SetClass("alert") } - }), + }) ) }, }, @@ -1483,7 +1500,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { const giggityUrl = argument[0] return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl }) @@ -1499,12 +1516,12 @@ export default class SpecialVisualizations { _: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { const tags = (( state )).geolocation.currentUserLocation.features.map( - (features) => features[0]?.properties, + (features) => features[0]?.properties ) return new Combine([ new SvelteUIElement(OrientationDebugPanel, {}), @@ -1526,7 +1543,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { return new SvelteUIElement(MarkAsFavourite, { tags: tagSource, @@ -1546,7 +1563,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { return new SvelteUIElement(MarkAsFavouriteMini, { tags: tagSource, @@ -1566,7 +1583,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { return new SvelteUIElement(DirectionIndicator, { state, feature }) }, @@ -1581,7 +1598,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { return new VariableUiElement( tagSource @@ -1603,9 +1620,9 @@ export default class SpecialVisualizations { `${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` + `#${id}` return new Img(new Qr(url).toImageElement(75)).SetStyle( - "width: 75px", + "width: 75px" ) - }), + }) ) }, }, @@ -1625,7 +1642,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, args: string[], feature: Feature, - layer: LayerConfig, + layer: LayerConfig ): BaseUIElement { const key = args[0] === "" ? "_direction:centerpoint" : args[0] return new VariableUiElement( @@ -1636,11 +1653,11 @@ export default class SpecialVisualizations { }) .mapD((value) => { const dir = GeoOperations.bearingToHuman( - GeoOperations.parseBearing(value), + GeoOperations.parseBearing(value) ) console.log("Human dir", dir) return Translations.t.general.visualFeedback.directionsAbsolute[dir] - }), + }) ) }, }, @@ -1664,13 +1681,19 @@ export default class SpecialVisualizations { doc: "Apply some postprocessing. Currently, only 'velopark' is allowed as value", }, { - name:"readonly", + name: "readonly", required: false, - doc: "If 'yes', will not show 'apply'-buttons" - } + 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>, args: string[], feature: Feature, layer: LayerConfig): BaseUIElement { + constr( + state: SpecialVisualizationState, + tagSource: UIEventSource>, + args: string[], + feature: Feature, + layer: LayerConfig + ): BaseUIElement { const url = args[0] const postprocessVelopark = args[2] === "velopark" const readonly = args[3] === "yes" @@ -1681,7 +1704,7 @@ export default class SpecialVisualizations { tags: tagSource, layer, feature, - readonly + readonly, }) }, }, @@ -1696,7 +1719,7 @@ export default class SpecialVisualizations { throw ( "Invalid special visualisation found: funcName is undefined for " + invalid.map((sp) => sp.i).join(", ") + - ". Did you perhaps type \n funcName: \"funcname\" // type declaration uses COLON\ninstead of:\n funcName = \"funcName\" // value definition uses EQUAL" + '. Did you perhaps type \n funcName: "funcname" // type declaration uses COLON\ninstead of:\n funcName = "funcName" // value definition uses EQUAL' ) } diff --git a/src/assets/svg/Square_rounded.svelte b/src/assets/svg/Square_rounded.svelte new file mode 100644 index 000000000..135e304ed --- /dev/null +++ b/src/assets/svg/Square_rounded.svelte @@ -0,0 +1,4 @@ + + \ No newline at end of file