{
+
+ const properties: {
+ "operator:email"?: string,
+ "operator:phone"?: string,
+ fee?: string,
+ opening_hours?: string
+ access?: string
+ maxstay?: string
+ operator?: string
+ } = {}
+
+ properties.operator = veloparkData.operatedBy?.companyName
+
+ if (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")
+ }
+
+ veloparkData.photos.forEach((p, i) => {
+ if (i === 0) {
+ properties["image"] = p.image
+ } else {
+ properties["image:" + i] = p.image
+
+ }
+ })
+
+ let coordinates: [number, number] = undefined
+ for (const g of veloparkData["@graph"]) {
+ 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)
+ properties.maxstay = duration + " days"
+ }
+ properties.access = g.publicAccess ? "yes" : "no"
+ const prefix = "http://schema.org/"
+ 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"
+ }
+ }
+
+
+ return { type: "Feature", properties, geometry: { type: "Point", coordinates } }
+ }
+
+}
+
+interface VeloparkData {
+ "@context": any,
+ "@id": string // "https://data.velopark.be/data/NMBS_541",
+ "@type": "BicycleParkingStation",
+ "dateModified": string,
+ "identifier": number,
+ "name": [
+ {
+ "@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
+ },
+ /**
+ * Contains various extra pieces of data, e.g. services or opening hours
+ */
+ "@graph": [
+ {
+ "@type": "https://data.velopark.be/openvelopark/terms#PublicBicycleParking",
+ "openingHoursSpecification": {
+ "@type": "OpeningHoursSpecification",
+ /**
+ * Ends with 'Monday', 'Tuesday', ...
+ */
+ "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",
+ /**
+ * opens: 00:00 and closes 23:59 for the entire day
+ */
+ "opens": string,
+ "closes": string
+ }[],
+ /**
+ * P30D = 30 days
+ */
+ "maximumParkingDuration": "P30D",
+ "publicAccess": true,
+ "totalCapacity": 110,
+ "allows": [
+ {
+ "@type": "AllowedBicycle",
+ /* TODO is cargo bikes etc also available?*/
+ "bicycleType": "https://data.velopark.be/openvelopark/terms#RegularBicycle",
+ "bicyclesAmount": number
+ }
+ ],
+ "geo": [
+ {
+ "@type": "GeoCoordinates",
+ "latitude": number,
+ "longitude": number
+ }
+ ],
+ "priceSpecification": [
+ {
+ "@type": "PriceSpecification",
+ "freeOfCharge": boolean
+ }
+ ]
+ }
+ ]
+
+}
diff --git a/src/UI/Comparison/ComparisonAction.svelte b/src/UI/Comparison/ComparisonAction.svelte
new file mode 100644
index 000000000..b16f216d4
--- /dev/null
+++ b/src/UI/Comparison/ComparisonAction.svelte
@@ -0,0 +1,65 @@
+
+
+
+ {key} |
+
+
+ {#if externalProperties[key].startsWith("http")}
+
+ {externalProperties[key]}
+
+ {:else}
+ {externalProperties[key]}
+ {/if}
+ |
+
+ {#if currentStep === "init"}
+
+ {:else if currentStep === "applying"}
+
+ {:else if currentStep === "done"}
+ Done
+ {:else }
+ Error
+ {/if}
+ |
+
diff --git a/src/UI/Comparison/ComparisonTable.svelte b/src/UI/Comparison/ComparisonTable.svelte
new file mode 100644
index 000000000..f6a8758f6
--- /dev/null
+++ b/src/UI/Comparison/ComparisonTable.svelte
@@ -0,0 +1,120 @@
+
+
+{#if different.length > 0}
+ Conflicting items
+ {JSON.stringify(different)}
+{/if}
+
+{#if missing.length > 0}
+ Missing items
+
+ {#if currentStep === "init"}
+
+
+ Key |
+ External |
+
+
+ {#each missing as key}
+
+ {/each}
+
+
+
+ {:else if currentStep === "applying_all"}
+ Applying all missing values
+ {:else if currentStep === "all_applied"}
+
+ All values are applied
+
+ {/if}
+{/if}
+
+
+{#if unknownImages.length === 0 && missing.length === 0 && different.length === 0}
+
+
+ All data from Velopark is also included into OpenStreetMap
+
+{/if}
+
+{#if unknownImages.length > 0}
+ Missing pictures
+ {#each unknownImages as image}
+
+ {/each}
+
+
+{/if}
+
diff --git a/src/UI/Comparison/ComparisonTool.svelte b/src/UI/Comparison/ComparisonTool.svelte
new file mode 100644
index 000000000..92652aec8
--- /dev/null
+++ b/src/UI/Comparison/ComparisonTool.svelte
@@ -0,0 +1,61 @@
+
+
+
+{#if error !== undefined}
+
+ Something went wrong: {error}
+
+{:else if data === undefined}
+
+ Loading {$tags[url]}
+
+{:else}
+
+{/if}
diff --git a/src/UI/Image/LinkableImage.svelte b/src/UI/Image/LinkableImage.svelte
index 572cc6776..8675cc8a4 100644
--- a/src/UI/Image/LinkableImage.svelte
+++ b/src/UI/Image/LinkableImage.svelte
@@ -15,8 +15,6 @@
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
export let tags: UIEventSource
- export let lon: number
- export let lat: number
export let state: SpecialVisualizationState
export let image: P4CPicture
export let feature: Feature
@@ -26,7 +24,6 @@
let isLinked = Object.values(tags.data).some((v) => image.pictureUrl === v)
const t = Translations.t.image.nearby
- const c = [lon, lat]
const providedImage: ProvidedImage = {
url: image.thumbUrl ?? image.pictureUrl,
url_hd: image.pictureUrl,
diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts
index 6bf55e99f..207362620 100644
--- a/src/UI/SpecialVisualizations.ts
+++ b/src/UI/SpecialVisualizations.ts
@@ -88,6 +88,7 @@ import MaprouletteSetStatus from "./MapRoulette/MaprouletteSetStatus.svelte"
import DirectionIndicator from "./Base/DirectionIndicator.svelte"
import Img from "./Base/Img"
import Qr from "../Utils/Qr"
+import ComparisonTool from "./Comparison/ComparisonTool.svelte"
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@@ -1638,6 +1639,28 @@ export default class SpecialVisualizations {
)
},
},
+ {
+ funcName: "compare_data",
+ needsUrls: (args) => args[0],
+ args:[
+ {
+ name: "url",
+ required: true,
+ doc: "The attribute containing the url where to fetch more data"
+ },
+ {
+ name: "postprocessing",
+ required: false,
+ doc: "Apply some postprocessing. Currently, only 'velopark' is allowed as value"
+ }
+ ],
+ 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 {
+ const url = args[0]
+ const postprocessVelopark = args[1] === "velopark"
+ return new SvelteUIElement(ComparisonTool, {url, postprocessVelopark, state, tags: tagSource, layer, feature})
+ }
+ }
]
specialVisualizations.push(new AutoApplyButton(specialVisualizations))