Furhter improvements to velopark: better icons, improvements to loading

This commit is contained in:
Pieter Vander Vennet 2024-01-17 18:08:14 +01:00
parent 5a48a2e19c
commit 8bcc8820ac
10 changed files with 560 additions and 381 deletions

View file

@ -25,40 +25,33 @@
"defaultBackgroundId": "photo", "defaultBackgroundId": "photo",
"layers": [ "layers": [
{ {
"builtin": "maproulette_challenge", "id": "velopark_maproulette",
"override": { "description": "Maproulette challenge containing velopark data",
"=name": { "source": {
"osmTags": "mr_taskId~*",
"geoJson": "https://maproulette.org/api/v2/challenge/view/43282",
"isOsmCache": false
},
"title": {
"render": "Velopark parking <b>{mr_velopark_id}</b>"
},
"name": {
"en": "Velopark data", "en": "Velopark data",
"nl": "Velopark data" "nl": "Velopark data"
}, },
"=filter": [ "titleIcons": [
{ {
"id": "created-only", "id": "maproulette",
"options": [ "render": "<a href='https://maproulette.org/challenge/{mr_challengeId}/task/{mr_taskId}' target='_blank'><img src='./assets/layers/maproulette/logomark.svg'/></a>"
{
"question": {
"en": "Only unfinished tasks",
"nl": "Enkel onafgewerkte taken"
},
"osmTags": "mr_taskStatus=Created",
"default": true
}
]
} }
], ],
"calculatedTags+": [ "tagRenderings": [
"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)", "id": "velopark-id-display",
"_nearby_bicycle_parkings:count=get(feat)('_nearby_bicycle_parkings').length", "render": {
"_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}))" "*": "<span class='literal-code'>{ref:velopark}</span>"
], }
"=title": {
"render": "Velopark parking <b>{mr_velopark_id}</b>"
}, },
"source": {
"geoJson": "https://maproulette.org/api/v2/challenge/view/43282"
},
"=tagRenderings": [
{ {
"id": "velopark-link", "id": "velopark-link",
"render": { "render": {
@ -146,14 +139,72 @@
"en": "Mark this item as incorrect (duplicate, does not exist anymore, contradictory data)", "en": "Mark this item as incorrect (duplicate, does not exist anymore, contradictory data)",
"nl": "Markeer dit object als incorrect (duplicaatin, incorrect of tegenstrijdige data, ...)" "nl": "Markeer dit object als incorrect (duplicaatin, incorrect of tegenstrijdige data, ...)"
}, },
"image": "bug", "image": "invalid",
"status": 6 "status": 6
} }
} }
}, },
"{nearby_images()}" "{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": "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"
}
],
"iconSize": "40,40",
"anchor": "bottom",
"iconBadges": [{
"if": "mr_taskStatus=Too_Hard",
"then": "invalid"
},{
"if": "mr_taskStatus=Fixed",
"then": "confirm"
}]
}
]
}, },
{ {
"builtin": [ "builtin": [

View file

@ -82,6 +82,7 @@ function genImages(dryrun = false) {
"SocialImageForeground", "SocialImageForeground",
"speech_bubble_black_outline", "speech_bubble_black_outline",
"square", "square",
"square_rounded",
"star", "star",
"star_half", "star_half",
"star_outline", "star_outline",

View file

@ -2,15 +2,17 @@ import Script from "../Script"
import { Utils } from "../../src/Utils" import { Utils } from "../../src/Utils"
import VeloparkLoader, { VeloparkData } from "../../src/Logic/Web/VeloparkLoader" import VeloparkLoader, { VeloparkData } from "../../src/Logic/Web/VeloparkLoader"
import fs from "fs" import fs from "fs"
import OverpassFeatureSource from "../../src/Logic/FeatureSource/Sources/OverpassFeatureSource"
import { Overpass } from "../../src/Logic/Osm/Overpass" import { Overpass } from "../../src/Logic/Osm/Overpass"
import { RegexTag } from "../../src/Logic/Tags/RegexTag" import { RegexTag } from "../../src/Logic/Tags/RegexTag"
import Constants from "../../src/Models/Constants" import Constants from "../../src/Models/Constants"
import { ImmutableStore } from "../../src/Logic/UIEventSource" import { ImmutableStore } from "../../src/Logic/UIEventSource"
import { BBox } from "../../src/Logic/BBox" import { BBox } from "../../src/Logic/BBox"
class VeloParkToGeojson extends Script { class VeloParkToGeojson extends Script {
constructor() { 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<void> { async main(args: string[]): Promise<void> {
@ -19,37 +21,48 @@ class VeloParkToGeojson extends Script {
const url = "https://www.velopark.be/api/parkings/1000" const url = "https://www.velopark.be/api/parkings/1000"
const data = <VeloparkData[]>await Utils.downloadJson(url) const data = <VeloparkData[]>await Utils.downloadJson(url)
const bboxBelgium = new BBox([[2.51357303225, 49.5294835476],[ 6.15665815596, 51.4750237087]]) const bboxBelgium = new BBox([
const alreadyLinkedQuery = new Overpass(new RegexTag("ref:velopark", /.+/), [2.51357303225, 49.5294835476],
[6.15665815596, 51.4750237087],
])
const alreadyLinkedQuery = new Overpass(
new RegexTag("ref:velopark", /.+/),
[], [],
Constants.defaultOverpassUrls[0], Constants.defaultOverpassUrls[0],
new ImmutableStore(60 * 5), new ImmutableStore(60 * 5),
false false
) )
const alreadyLinkedFeatures = await alreadyLinkedQuery.queryGeoJson(bboxBelgium) const alreadyLinkedFeatures = await alreadyLinkedQuery.queryGeoJson(bboxBelgium)
const seenIds = new Set<string>(alreadyLinkedFeatures[0].features.map(f => f.properties["ref:velopark"])) const seenIds = new Set<string>(
const features = data.map(f => VeloparkLoader.convert(f)) alreadyLinkedFeatures[0].features.map((f) => f.properties["ref:velopark"])
.filter(f => !seenIds.has(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<string>() const allProperties = new Set<string>()
for (const feature of features) { 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") allProperties.delete("ref:velopark")
for (const feature of features) { for (const feature of features) {
allProperties.forEach(k => { allProperties.forEach((k) => {
delete feature.properties[k] delete feature.properties[k]
}) })
} }
fs.writeFileSync("velopark_id_only_export_" + new Date().toISOString() + ".geojson", JSON.stringify({ fs.writeFileSync(
"type": "FeatureCollection", "velopark_id_only_export_" + new Date().toISOString() + ".geojson",
JSON.stringify(
{
type: "FeatureCollection",
features, features,
}, null, " ")) },
null,
" "
)
)
} }
} }
new VeloParkToGeojson().run() new VeloParkToGeojson().run()

View file

@ -1,4 +1,4 @@
import { Feature, Geometry, Point } from "geojson" import { Feature, Geometry } from "geojson"
import { OH } from "../../UI/OpeningHours/OpeningHours" import { OH } from "../../UI/OpeningHours/OpeningHours"
import EmailValidator from "../../UI/InputElement/Validators/EmailValidator" import EmailValidator from "../../UI/InputElement/Validators/EmailValidator"
import PhoneValidator from "../../UI/InputElement/Validators/PhoneValidator" import PhoneValidator from "../../UI/InputElement/Validators/PhoneValidator"
@ -12,39 +12,49 @@ import { Utils } from "../../Utils"
* Reads a velopark-json, converts it to a geojson * Reads a velopark-json, converts it to a geojson
*/ */
export default class VeloparkLoader { export default class VeloparkLoader {
private static readonly emailReformatting = new EmailValidator() private static readonly emailReformatting = new EmailValidator()
private static readonly phoneValidator = new PhoneValidator() private static readonly phoneValidator = new PhoneValidator()
private static readonly coder = new CountryCoder( private static readonly coder = new CountryCoder(
Constants.countryCoderEndpoint, Constants.countryCoderEndpoint,
Utils.downloadJson, Utils.downloadJson
) )
public static convert(veloparkData: VeloparkData): Feature { public static convert(veloparkData: VeloparkData): Feature {
console.log("Converting", veloparkData)
const properties: { const properties: {
"ref:velopark":string, "ref:velopark": string
"operator:email"?: string, "operator:email"?: string
"operator:phone"?: string, "operator:phone"?: string
fee?: string, fee?: string
opening_hours?: string opening_hours?: string
access?: string access?: string
maxstay?: string maxstay?: string
operator?: 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 properties.operator = veloparkData.operatedBy?.companyName
if (veloparkData.contactPoint?.email) { 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) { 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) => { veloparkData.photos?.forEach((p, i) => {
@ -52,130 +62,198 @@ export default class VeloparkLoader {
properties["image"] = p.image properties["image"] = p.image
} else { } else {
properties["image:" + i] = p.image properties["image:" + i] = p.image
} }
}) })
let geometry = veloparkData.geometry let geometry = veloparkData.geometry
for (const g of veloparkData["@graph"]) { for (const g of veloparkData["@graph"]) {
VeloparkLoader.cleanup(g)
VeloparkLoader.cleanupEmtpy(g)
if (g.geo[0]) { if (g.geo[0]) {
geometry = { type: "Point", coordinates: [g.geo[0].longitude, g.geo[0].latitude] } geometry = { type: "Point", coordinates: [g.geo[0].longitude, g.geo[0].latitude] }
} }
if (g.maximumParkingDuration?.endsWith("D") && g.maximumParkingDuration?.startsWith("P")) { if (
const duration = g.maximumParkingDuration.substring(1, g.maximumParkingDuration.length - 1) g.maximumParkingDuration?.endsWith("D") &&
g.maximumParkingDuration?.startsWith("P")
) {
const duration = g.maximumParkingDuration.substring(
1,
g.maximumParkingDuration.length - 1
)
properties.maxstay = duration + " days" properties.maxstay = duration + " days"
} }
properties.access = g.publicAccess ? "yes" : "no" properties.access = g.publicAccess ? "yes" : "no"
const prefix = "http://schema.org/" const prefix = "http://schema.org/"
if (g.openingHoursSpecification) { if (g.openingHoursSpecification) {
const oh = OH.simplify(g.openingHoursSpecification.map(spec => { const oh = OH.simplify(
const dayOfWeek = spec.dayOfWeek.substring(prefix.length, prefix.length + 2).toLowerCase() g.openingHoursSpecification
.map((spec) => {
const dayOfWeek = spec.dayOfWeek
.substring(prefix.length, prefix.length + 2)
.toLowerCase()
const startHour = spec.opens const startHour = spec.opens
const endHour = spec.closes === "23:59" ? "24:00" : spec.closes const endHour = spec.closes === "23:59" ? "24:00" : spec.closes
const merged = OH.MergeTimes(OH.ParseRule(dayOfWeek + " " + startHour + "-" + endHour)) const merged = OH.MergeTimes(
OH.ParseRule(dayOfWeek + " " + startHour + "-" + endHour)
)
return OH.ToString(merged) return OH.ToString(merged)
}).join("; ")) })
.join("; ")
)
properties.opening_hours = oh properties.opening_hours = oh
} }
if (g.priceSpecification?.[0]) { if (g.priceSpecification?.[0]) {
properties.fee = g.priceSpecification[0].freeOfCharge ? "no" : "yes" 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 } 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 { export interface VeloparkData {
geometry?: Geometry geometry?: Geometry
"@context": any, "@context": any
"@id": string // "https://data.velopark.be/data/NMBS_541", "@id": string // "https://data.velopark.be/data/NMBS_541",
"@type": "BicycleParkingStation", "@type": "BicycleParkingStation"
"dateModified": string, dateModified: string
"identifier": number, identifier: number
"name": [ name: [
{ {
"@value": string, "@value": string
"@language": "nl" "@language": "nl"
} }
], ]
"ownedBy": { ownedBy: {
"@id": string, "@id": string
"@type": "BusinessEntity", "@type": "BusinessEntity"
"companyName": string companyName: string
}, }
"operatedBy": { operatedBy: {
"@type": "BusinessEntity", "@type": "BusinessEntity"
"companyName": string companyName: string
}, }
"address": any, address: any
"hasMap": any, hasMap: any
"contactPoint": { contactPoint: {
"@type": "ContactPoint", "@type": "ContactPoint"
"email": string, email: string
"telephone": string telephone: string
}, }
"photos": { photos: {
"@type": "Photograph", "@type": "Photograph"
"image": string image: string
}[], }[]
"interactionService": { interactionService: {
"@type": "WebSite", "@type": "WebSite"
"url": string url: string
}, }
/** /**
* Contains various extra pieces of data, e.g. services or opening hours * Contains various extra pieces of data, e.g. services or opening hours
*/ */
"@graph": [ "@graph": [
{ {
"@type": "https://data.velopark.be/openvelopark/terms#PublicBicycleParking", "@type": "https://data.velopark.be/openvelopark/terms#PublicBicycleParking"
"openingHoursSpecification": { openingHoursSpecification: {
"@type": "OpeningHoursSpecification", "@type": "OpeningHoursSpecification"
/** /**
* Ends with 'Monday', 'Tuesday', ... * Ends with 'Monday', 'Tuesday', ...
*/ */
"dayOfWeek": "http://schema.org/Monday" dayOfWeek:
| "http://schema.org/Monday"
| "http://schema.org/Tuesday" | "http://schema.org/Tuesday"
| "http://schema.org/Wednesday" | "http://schema.org/Wednesday"
| "http://schema.org/Thursday" | "http://schema.org/Thursday"
| "http://schema.org/Friday" | "http://schema.org/Friday"
| "http://schema.org/Saturday" | "http://schema.org/Saturday"
| "http://schema.org/Sunday", | "http://schema.org/Sunday"
/** /**
* opens: 00:00 and closes 23:59 for the entire day * opens: 00:00 and closes 23:59 for the entire day
*/ */
"opens": string, opens: string
"closes": string closes: string
}[], }[]
/** /**
* P30D = 30 days * P30D = 30 days
*/ */
"maximumParkingDuration": "P30D", maximumParkingDuration: "P30D"
"publicAccess": true, publicAccess: true
"totalCapacity": 110, totalCapacity: 110
"allows": [ allows: [
{ {
"@type": "AllowedBicycle", "@type": "AllowedBicycle"
/* TODO is cargo bikes etc also available?*/ /* TODO is cargo bikes etc also available?*/
"bicycleType": "https://data.velopark.be/openvelopark/terms#RegularBicycle", bicycleType:
"bicyclesAmount": number | string
| "https://data.velopark.be/openvelopark/terms#RegularBicycle"
bicyclesAmount: number
} }
], ]
"geo": [ geo: [
{ {
"@type": "GeoCoordinates", "@type": "GeoCoordinates"
"latitude": number, latitude: number
"longitude": number longitude: number
} }
], ]
"priceSpecification": [ priceSpecification: [
{ {
"@type": "PriceSpecification", "@type": "PriceSpecification"
"freeOfCharge": boolean freeOfCharge: boolean
} }
] ]
} }
] ]
} }

View file

@ -36,7 +36,7 @@ export default class Constants {
"import_candidate", "import_candidate",
"usersettings", "usersettings",
"icons", "icons",
"filters" "filters",
] as const ] as const
/** /**
* Layer IDs of layers which have special properties through built-in hooks * Layer IDs of layers which have special properties through built-in hooks
@ -117,7 +117,9 @@ export default class Constants {
*/ */
private static readonly _defaultPinIcons = [ private static readonly _defaultPinIcons = [
"pin", "pin",
"bug",
"square", "square",
"square_rounded",
"circle", "circle",
"checkmark", "checkmark",
"clock", "clock",

View file

@ -55,7 +55,7 @@
<div class="flex w-full space-x-1 overflow-x-auto" style="scroll-snap-type: x proximity"> <div class="flex w-full space-x-1 overflow-x-auto" style="scroll-snap-type: x proximity">
{#each $images as image (image.pictureUrl)} {#each $images as image (image.pictureUrl)}
<span class="w-fit shrink-0" style="scroll-snap-align: start"> <span class="w-fit shrink-0" style="scroll-snap-align: start">
<LinkableImage {tags} {image} {state} {lon} {lat} {feature} {layer} {linkable} /> <LinkableImage {tags} {image} {state} {feature} {layer} {linkable} />
</span> </span>
{/each} {/each}
</div> </div>

View file

@ -32,6 +32,8 @@
import Party from "../../assets/svg/Party.svelte" import Party from "../../assets/svg/Party.svelte"
import AddSmall from "../../assets/svg/AddSmall.svelte" import AddSmall from "../../assets/svg/AddSmall.svelte"
import { LinkIcon } from "@babeard/svelte-heroicons/mini" import { LinkIcon } from "@babeard/svelte-heroicons/mini"
import Square_rounded from "../../assets/svg/Square_rounded.svelte"
import Bug from "../../assets/svg/Bug.svelte"
/** /**
* Renders a single icon. * Renders a single icon.
@ -50,6 +52,11 @@
<Pin {color} class={clss} /> <Pin {color} class={clss} />
{:else if icon === "square"} {:else if icon === "square"}
<Square {color} class={clss} /> <Square {color} class={clss} />
{:else if icon === "square_rounded"}
<Square_rounded {color} class={clss} />
{:else if icon === "bug"}
<Bug {color} class={clss} />
{:else if icon === "circle"} {:else if icon === "circle"}
<Circle {color} class={clss} /> <Circle {color} class={clss} />
{:else if icon === "checkmark"} {:else if icon === "checkmark"}

View file

@ -54,7 +54,7 @@
<Tr t={Translations.t.general.loading} /> <Tr t={Translations.t.general.loading} />
</Loading> </Loading>
{:else if $status === Maproulette.STATUS_OPEN} {:else if $status === Maproulette.STATUS_OPEN}
<button class="no-image-background w-full p-4" on:click={() => apply()}> <button class="no-image-background w-full p-4 m-0" on:click={() => apply()}>
<Icon clss="w-8 h-8 mr-2" icon={image} /> <Icon clss="w-8 h-8 mr-2" icon={image} />
{message} {message}
</button> </button>

View file

@ -3,7 +3,11 @@ import { FixedUiElement } from "./Base/FixedUiElement"
import BaseUIElement from "./BaseUIElement" import BaseUIElement from "./BaseUIElement"
import Title from "./Base/Title" import Title from "./Base/Title"
import Table from "./Base/Table" import Table from "./Base/Table"
import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization" import {
RenderingSpecification,
SpecialVisualization,
SpecialVisualizationState,
} from "./SpecialVisualization"
import { HistogramViz } from "./Popup/HistogramViz" import { HistogramViz } from "./Popup/HistogramViz"
import { MinimapViz } from "./Popup/MinimapViz" import { MinimapViz } from "./Popup/MinimapViz"
import { ShareLinkViz } from "./Popup/ShareLinkViz" import { ShareLinkViz } from "./Popup/ShareLinkViz"
@ -94,6 +98,11 @@ class NearbyImageVis implements SpecialVisualization {
defaultValue: "closed", defaultValue: "closed",
doc: "Either `open` or `closed`. If `open`, then the image carousel will always be shown", 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 = 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" "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<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
const isOpen = args[0] === "open" const isOpen = args[0] === "open"
const readonly = args[1] === "readonly"
const [lon, lat] = GeoOperations.centerpointCoordinates(feature) const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(isOpen ? NearbyImages : NearbyImagesCollapsed, { return new SvelteUIElement(isOpen ? NearbyImages : NearbyImagesCollapsed, {
tags, tags,
@ -117,6 +127,7 @@ class NearbyImageVis implements SpecialVisualization {
lat, lat,
feature, feature,
layer, layer,
linkable: !readonly,
}) })
} }
} }
@ -171,7 +182,7 @@ class StealViz implements SpecialVisualization {
selectedElement: otherFeature, selectedElement: otherFeature,
state, state,
layer, layer,
}), })
) )
} }
if (elements.length === 1) { if (elements.length === 1) {
@ -179,8 +190,8 @@ class StealViz implements SpecialVisualization {
} }
return new Combine(elements).SetClass("flex flex-col") 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<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
const labels = args[0] const labels = args[0]
?.split(";") ?.split(";")
@ -273,8 +284,10 @@ export default class SpecialVisualizations {
* templ.args[0] = "{email}" * templ.args[0] = "{email}"
*/ */
public static constructSpecification( public static constructSpecification(
template: string | { special: Record<string, string | Record<string, string>> & { type: string } }, template:
extraMappings: SpecialVisualization[] = [], | string
| { special: Record<string, string | Record<string, string>> & { type: string } },
extraMappings: SpecialVisualization[] = []
): RenderingSpecification[] { ): RenderingSpecification[] {
if (template === "") { if (template === "") {
return [] return []
@ -283,7 +296,7 @@ export default class SpecialVisualizations {
if (typeof template !== "string") { if (typeof template !== "string") {
console.trace( console.trace(
"Got a non-expanded template while constructing the specification, it still has a 'special-key':", "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" throw "Got a non-expanded template while constructing the specification"
} }
@ -291,20 +304,20 @@ export default class SpecialVisualizations {
for (const knownSpecial of allKnownSpecials) { for (const knownSpecial of allKnownSpecials) {
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way' // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
const matched = template.match( const matched = template.match(
new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s"), new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s")
) )
if (matched != null) { if (matched != null) {
// We found a special component that should be brought to live // We found a special component that should be brought to live
const partBefore = SpecialVisualizations.constructSpecification( const partBefore = SpecialVisualizations.constructSpecification(
matched[1], matched[1],
extraMappings, extraMappings
) )
const argument = const argument =
matched[2] /* .trim() // We don't trim, as spaces might be relevant, e.g. "what is ... of {title()}"*/ 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 style = matched[3]?.substring(1) ?? ""
const partAfter = SpecialVisualizations.constructSpecification( const partAfter = SpecialVisualizations.constructSpecification(
matched[4], matched[4],
extraMappings, extraMappings
) )
const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "") const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "")
if (argument.length > 0) { if (argument.length > 0) {
@ -350,7 +363,7 @@ export default class SpecialVisualizations {
defaultArg = "_empty string_" defaultArg = "_empty string_"
} }
return [arg.name, defaultArg, arg.doc] return [arg.name, defaultArg, arg.doc]
}), })
) )
: undefined, : undefined,
new Title("Example usage of " + viz.funcName, 4), new Title("Example usage of " + viz.funcName, 4),
@ -360,14 +373,14 @@ export default class SpecialVisualizations {
viz.funcName + viz.funcName +
"(" + "(" +
viz.args.map((arg) => arg.defaultValue).join(",") + viz.args.map((arg) => arg.defaultValue).join(",") +
")}`", ")}`"
).SetClass("literal-code"), ).SetClass("literal-code"),
]) ])
} }
public static HelpMessage() { public static HelpMessage() {
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) => const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
SpecialVisualizations.DocumentationFor(viz), SpecialVisualizations.DocumentationFor(viz)
) )
return new Combine([ return new Combine([
@ -401,10 +414,10 @@ export default class SpecialVisualizations {
}, },
}, },
null, null,
" ", " "
), )
).SetClass("code"), ).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"), ]).SetClass("flex flex-col"),
...helpTexts, ...helpTexts,
]).SetClass("flex flex-col") ]).SetClass("flex flex-col")
@ -413,7 +426,7 @@ export default class SpecialVisualizations {
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
public static renderExampleOfSpecial( public static renderExampleOfSpecial(
state: SpecialVisualizationState, state: SpecialVisualizationState,
s: SpecialVisualization, s: SpecialVisualization
): BaseUIElement { ): BaseUIElement {
const examples = const examples =
s.structuredExamples === undefined s.structuredExamples === undefined
@ -424,7 +437,7 @@ export default class SpecialVisualizations {
new UIEventSource<Record<string, string>>(e.feature.properties), new UIEventSource<Record<string, string>>(e.feature.properties),
e.args, e.args,
e.feature, e.feature,
undefined, undefined
) )
}) })
return new Combine([new Title(s.funcName), s.docs, ...examples]) return new Combine([new Title(s.funcName), s.docs, ...examples])
@ -466,7 +479,7 @@ export default class SpecialVisualizations {
assignTo: state.userRelatedState.language, assignTo: state.userRelatedState.language,
availableLanguages: state.layout.language, availableLanguages: state.layout.language,
preferredLanguages: state.osmConnection.userDetails.map( preferredLanguages: state.osmConnection.userDetails.map(
(ud) => ud.languages, (ud) => ud.languages
), ),
}) })
}, },
@ -491,7 +504,7 @@ export default class SpecialVisualizations {
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>
): BaseUIElement { ): BaseUIElement {
return new VariableUiElement( return new VariableUiElement(
tagSource tagSource
@ -501,7 +514,7 @@ export default class SpecialVisualizations {
return new SplitRoadWizard(<WayId>id, state) return new SplitRoadWizard(<WayId>id, state)
} }
return undefined return undefined
}), })
) )
}, },
}, },
@ -515,7 +528,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
if (feature.geometry.type !== "Point") { if (feature.geometry.type !== "Point") {
return undefined return undefined
@ -538,7 +551,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
if (!layer.deletion) { if (!layer.deletion) {
return undefined return undefined
@ -566,7 +579,7 @@ export default class SpecialVisualizations {
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature
): BaseUIElement { ): BaseUIElement {
const [lon, lat] = GeoOperations.centerpointCoordinates(feature) const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(CreateNewNote, { return new SvelteUIElement(CreateNewNote, {
@ -630,7 +643,7 @@ export default class SpecialVisualizations {
.map((tags) => tags[args[0]]) .map((tags) => tags[args[0]])
.map((wikidata) => { .map((wikidata) => {
wikidata = Utils.NoEmpty( wikidata = Utils.NoEmpty(
wikidata?.split(";")?.map((wd) => wd.trim()) ?? [], wikidata?.split(";")?.map((wd) => wd.trim()) ?? []
)[0] )[0]
const entry = Wikidata.LoadWikidataEntry(wikidata) const entry = Wikidata.LoadWikidataEntry(wikidata)
return new VariableUiElement( return new VariableUiElement(
@ -640,9 +653,9 @@ export default class SpecialVisualizations {
} }
const response = <WikidataResponse>e["success"] const response = <WikidataResponse>e["success"]
return Translation.fromMap(response.labels) return Translation.fromMap(response.labels)
}), })
) )
}), })
), ),
}, },
new MapillaryLinkVis(), new MapillaryLinkVis(),
@ -674,7 +687,7 @@ export default class SpecialVisualizations {
AllImageProviders.LoadImagesFor(tags, imagePrefixes), AllImageProviders.LoadImagesFor(tags, imagePrefixes),
tags, tags,
state, state,
feature, feature
) )
}, },
}, },
@ -730,7 +743,7 @@ export default class SpecialVisualizations {
{ {
nameKey: nameKey, nameKey: nameKey,
fallbackName, fallbackName,
}, }
) )
return new SvelteUIElement(StarsBarIcon, { return new SvelteUIElement(StarsBarIcon, {
score: reviews.average, score: reviews.average,
@ -763,7 +776,7 @@ export default class SpecialVisualizations {
{ {
nameKey: nameKey, nameKey: nameKey,
fallbackName, fallbackName,
}, }
) )
return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer }) return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer })
}, },
@ -795,7 +808,7 @@ export default class SpecialVisualizations {
{ {
nameKey: nameKey, nameKey: nameKey,
fallbackName, fallbackName,
}, }
) )
return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer }) return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
}, },
@ -853,7 +866,7 @@ export default class SpecialVisualizations {
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): SvelteUIElement { ): SvelteUIElement {
const keyToUse = args[0] const keyToUse = args[0]
const prefix = args[1] const prefix = args[1]
@ -890,10 +903,10 @@ export default class SpecialVisualizations {
return undefined return undefined
} }
const allUnits: Unit[] = [].concat( const allUnits: Unit[] = [].concat(
...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []), ...(state?.layout?.layers?.map((lyr) => lyr.units) ?? [])
) )
const unit = allUnits.filter((unit) => const unit = allUnits.filter((unit) =>
unit.isApplicableToKey(key), unit.isApplicableToKey(key)
)[0] )[0]
if (unit === undefined) { if (unit === undefined) {
return value return value
@ -901,7 +914,7 @@ export default class SpecialVisualizations {
const getCountry = () => tagSource.data._country const getCountry = () => tagSource.data._country
const [v, denom] = unit.findDenomination(value, getCountry) const [v, denom] = unit.findDenomination(value, getCountry)
return unit.asHumanLongValue(v, getCountry) return unit.asHumanLongValue(v, getCountry)
}), })
) )
}, },
}, },
@ -918,7 +931,7 @@ export default class SpecialVisualizations {
new Combine([ new Combine([
t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"), t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"),
t.downloadGeoJsonHelper.SetClass("subtle"), t.downloadGeoJsonHelper.SetClass("subtle"),
]).SetClass("flex flex-col"), ]).SetClass("flex flex-col")
) )
.onClick(() => { .onClick(() => {
console.log("Exporting as Geojson") console.log("Exporting as Geojson")
@ -931,7 +944,7 @@ export default class SpecialVisualizations {
title + "_mapcomplete_export.geojson", title + "_mapcomplete_export.geojson",
{ {
mimetype: "application/vnd.geo+json", mimetype: "application/vnd.geo+json",
}, }
) )
}) })
.SetClass("w-full") .SetClass("w-full")
@ -967,7 +980,7 @@ export default class SpecialVisualizations {
constr: (state) => { constr: (state) => {
return new SubtleButton( return new SubtleButton(
Svg.delete_icon_svg().SetStyle("height: 1.5rem"), Svg.delete_icon_svg().SetStyle("height: 1.5rem"),
Translations.t.general.removeLocationHistory, Translations.t.general.removeLocationHistory
).onClick(() => { ).onClick(() => {
state.historicalUserLocations.features.setData([]) state.historicalUserLocations.features.setData([])
state.selectedElement.setData(undefined) state.selectedElement.setData(undefined)
@ -1005,10 +1018,10 @@ export default class SpecialVisualizations {
.filter((c) => c.text !== "") .filter((c) => c.text !== "")
.map( .map(
(c, i) => (c, i) =>
new NoteCommentElement(c, state, i, comments.length), new NoteCommentElement(c, state, i, comments.length)
), )
).SetClass("flex flex-col") ).SetClass("flex flex-col")
}), })
), ),
}, },
{ {
@ -1049,9 +1062,9 @@ export default class SpecialVisualizations {
return undefined return undefined
} }
return new SubstitutedTranslation(title, tagsSource, state).SetClass( return new SubstitutedTranslation(title, tagsSource, state).SetClass(
"px-1", "px-1"
) )
}), })
), ),
}, },
{ {
@ -1067,8 +1080,8 @@ export default class SpecialVisualizations {
let challenge = Stores.FromPromise( let challenge = Stores.FromPromise(
Utils.downloadJsonCached( Utils.downloadJsonCached(
`${Maproulette.defaultEndpoint}/challenge/${parentId}`, `${Maproulette.defaultEndpoint}/challenge/${parentId}`,
24 * 60 * 60 * 1000, 24 * 60 * 60 * 1000
), )
) )
return new VariableUiElement( return new VariableUiElement(
@ -1093,7 +1106,7 @@ export default class SpecialVisualizations {
} else { } else {
return [title, new List(listItems)] 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.", 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" + "\n" +
"```json\n" + "```json\n" +
"{\n" + "{\n" +
" \"id\": \"mark_duplicate\",\n" + ' "id": "mark_duplicate",\n' +
" \"render\": {\n" + ' "render": {\n' +
" \"special\": {\n" + ' "special": {\n' +
" \"type\": \"maproulette_set_status\",\n" + ' "type": "maproulette_set_status",\n' +
" \"message\": {\n" + ' "message": {\n' +
" \"en\": \"Mark as not found or false positive\"\n" + ' "en": "Mark as not found or false positive"\n' +
" },\n" + " },\n" +
" \"status\": \"2\",\n" + ' "status": "2",\n' +
" \"image\": \"close\"\n" + ' "image": "close"\n' +
" }\n" + " }\n" +
" }\n" + " }\n" +
"}\n" + "}\n" +
@ -1181,8 +1194,8 @@ export default class SpecialVisualizations {
const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox) const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
return new StatisticsPanel(fsBboxed) return new StatisticsPanel(fsBboxed)
}, },
[state.mapProperties.bounds], [state.mapProperties.bounds]
), )
) )
}, },
}, },
@ -1248,7 +1261,7 @@ export default class SpecialVisualizations {
constr( constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
args: string[], args: string[]
): BaseUIElement { ): BaseUIElement {
let [text, href, classnames, download, ariaLabel] = args let [text, href, classnames, download, ariaLabel] = args
if (download === "") { if (download === "") {
@ -1265,8 +1278,8 @@ export default class SpecialVisualizations {
download: Utils.SubstituteKeys(download, tags), download: Utils.SubstituteKeys(download, tags),
ariaLabel: Utils.SubstituteKeys(ariaLabel, tags), ariaLabel: Utils.SubstituteKeys(ariaLabel, tags),
newTab, newTab,
}), })
), )
) )
}, },
}, },
@ -1288,7 +1301,7 @@ export default class SpecialVisualizations {
}, },
}, },
null, null,
" ", " "
) + ) +
"\n```", "\n```",
args: [ args: [
@ -1310,26 +1323,30 @@ export default class SpecialVisualizations {
featureTags.map((tags) => { featureTags.map((tags) => {
try { try {
const data = tags[key] 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 = [] const elements = []
for (const property of properties) { for (const property of properties) {
const subsTr = new SubstitutedTranslation( const subsTr = new SubstitutedTranslation(
translation, translation,
new UIEventSource<any>(property), new UIEventSource<any>(property),
state, state
) )
elements.push(subsTr) elements.push(subsTr)
} }
return new List(elements) return new List(elements)
} catch (e) { } catch (e) {
console.log("Something went wrong while generating the elements for a multi", { console.log(
"Something went wrong while generating the elements for a multi",
{
e, e,
tags, tags,
key, key,
loaded: tags[key], loaded: tags[key],
})
} }
}), )
}
})
) )
}, },
}, },
@ -1349,7 +1366,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
return new VariableUiElement( return new VariableUiElement(
tagSource.map((tags) => { tagSource.map((tags) => {
@ -1361,7 +1378,7 @@ export default class SpecialVisualizations {
console.error("Cannot create a translation for", v, "due to", e) console.error("Cannot create a translation for", v, "due to", e)
return JSON.stringify(v) return JSON.stringify(v)
} }
}), })
) )
}, },
}, },
@ -1381,7 +1398,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
const key = argument[0] const key = argument[0]
const validator = new FediverseValidator() const validator = new FediverseValidator()
@ -1391,14 +1408,14 @@ export default class SpecialVisualizations {
.map((fediAccount) => { .map((fediAccount) => {
fediAccount = validator.reformat(fediAccount) fediAccount = validator.reformat(fediAccount)
const [_, username, host] = fediAccount.match( const [_, username, host] = fediAccount.match(
FediverseValidator.usernameAtServer, FediverseValidator.usernameAtServer
) )
return new SvelteUIElement(Link, { return new SvelteUIElement(Link, {
text: fediAccount, text: fediAccount,
url: "https://" + host + "/@" + username, url: "https://" + host + "/@" + username,
newTab: true, newTab: true,
}) })
}), })
) )
}, },
}, },
@ -1418,7 +1435,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
return new FixedUiElement("{" + args[0] + "}") return new FixedUiElement("{" + args[0] + "}")
}, },
@ -1439,7 +1456,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
const key = argument[0] ?? "value" const key = argument[0] ?? "value"
return new VariableUiElement( return new VariableUiElement(
@ -1459,10 +1476,10 @@ export default class SpecialVisualizations {
"Could not parse this tag: " + "Could not parse this tag: " +
JSON.stringify(value) + JSON.stringify(value) +
" due to " + " due to " +
e, e
).SetClass("alert") ).SetClass("alert")
} }
}), })
) )
}, },
}, },
@ -1483,7 +1500,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
const giggityUrl = argument[0] const giggityUrl = argument[0]
return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl }) return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl })
@ -1499,12 +1516,12 @@ export default class SpecialVisualizations {
_: UIEventSource<Record<string, string>>, _: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
const tags = (<ThemeViewState>( const tags = (<ThemeViewState>(
state state
)).geolocation.currentUserLocation.features.map( )).geolocation.currentUserLocation.features.map(
(features) => features[0]?.properties, (features) => features[0]?.properties
) )
return new Combine([ return new Combine([
new SvelteUIElement(OrientationDebugPanel, {}), new SvelteUIElement(OrientationDebugPanel, {}),
@ -1526,7 +1543,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
return new SvelteUIElement(MarkAsFavourite, { return new SvelteUIElement(MarkAsFavourite, {
tags: tagSource, tags: tagSource,
@ -1546,7 +1563,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
return new SvelteUIElement(MarkAsFavouriteMini, { return new SvelteUIElement(MarkAsFavouriteMini, {
tags: tagSource, tags: tagSource,
@ -1566,7 +1583,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
return new SvelteUIElement(DirectionIndicator, { state, feature }) return new SvelteUIElement(DirectionIndicator, { state, feature })
}, },
@ -1581,7 +1598,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
argument: string[], argument: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
return new VariableUiElement( return new VariableUiElement(
tagSource 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` + `${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` +
`#${id}` `#${id}`
return new Img(new Qr(url).toImageElement(75)).SetStyle( return new Img(new Qr(url).toImageElement(75)).SetStyle(
"width: 75px", "width: 75px"
) )
}), })
) )
}, },
}, },
@ -1625,7 +1642,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>, tagSource: UIEventSource<Record<string, string>>,
args: string[], args: string[],
feature: Feature, feature: Feature,
layer: LayerConfig, layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
const key = args[0] === "" ? "_direction:centerpoint" : args[0] const key = args[0] === "" ? "_direction:centerpoint" : args[0]
return new VariableUiElement( return new VariableUiElement(
@ -1636,11 +1653,11 @@ export default class SpecialVisualizations {
}) })
.mapD((value) => { .mapD((value) => {
const dir = GeoOperations.bearingToHuman( const dir = GeoOperations.bearingToHuman(
GeoOperations.parseBearing(value), GeoOperations.parseBearing(value)
) )
console.log("Human dir", dir) console.log("Human dir", dir)
return Translations.t.general.visualFeedback.directionsAbsolute[dir] return Translations.t.general.visualFeedback.directionsAbsolute[dir]
}), })
) )
}, },
}, },
@ -1666,11 +1683,17 @@ export default class SpecialVisualizations {
{ {
name: "readonly", name: "readonly",
required: false, 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", 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 { constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
const url = args[0] const url = args[0]
const postprocessVelopark = args[2] === "velopark" const postprocessVelopark = args[2] === "velopark"
const readonly = args[3] === "yes" const readonly = args[3] === "yes"
@ -1681,7 +1704,7 @@ export default class SpecialVisualizations {
tags: tagSource, tags: tagSource,
layer, layer,
feature, feature,
readonly readonly,
}) })
}, },
}, },
@ -1696,7 +1719,7 @@ export default class SpecialVisualizations {
throw ( throw (
"Invalid special visualisation found: funcName is undefined for " + "Invalid special visualisation found: funcName is undefined for " +
invalid.map((sp) => sp.i).join(", ") + 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'
) )
} }

View file

@ -0,0 +1,4 @@
<script>
export let color = "#000000"
</script>
<svg {...$$restProps} on:click on:mouseover on:mouseenter on:mouseleave on:keydown width="500.94501" height="500.94501" viewBox="0 0 500.94501 500.94501" version="1.1" id="svg5" sodipodi:docname="square_rounded.svg" inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g id="surface1" transform="translate(0.4725,0.4725)" /> <path class="selectable" id="rect1" style="fill:{color}" d="m 75.4725,0.4725 h 350 c 41.55,0 75,33.45 75,75 v 350 c 0,41.55 -33.45,75 -75,75 h -350 c -41.55,0 -75,-33.45 -75,-75 v -350 c 0,-41.55 33.45,-75 75,-75 z" /> </svg>