forked from MapComplete/MapComplete
		
	Merge branch 'master' into develop
This commit is contained in:
		
						commit
						d149a0d31d
					
				
					 316 changed files with 5387 additions and 2106 deletions
				
			
		| 
						 | 
				
			
			@ -113,7 +113,7 @@ export default class GeoLocationHandler {
 | 
			
		|||
     * - The GPS-location iss NULL-island
 | 
			
		||||
     * @constructor
 | 
			
		||||
     */
 | 
			
		||||
    public MoveMapToCurrentLocation(zoomToAtLeast: number = 14 ) {
 | 
			
		||||
    public MoveMapToCurrentLocation(zoomToAtLeast: number = 14) {
 | 
			
		||||
        const newLocation = this.geolocationState.currentGPSLocation.data
 | 
			
		||||
        const mapLocation = this.mapProperties.location
 | 
			
		||||
        // We got a new location.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,11 @@ import { Feature } from "geojson"
 | 
			
		|||
import { ImageUploadManager } from "../ImageProviders/ImageUploadManager"
 | 
			
		||||
 | 
			
		||||
export default class PendingChangesUploader {
 | 
			
		||||
    constructor(changes: Changes, selectedFeature: UIEventSource<Feature>, uploader : ImageUploadManager) {
 | 
			
		||||
    constructor(
 | 
			
		||||
        changes: Changes,
 | 
			
		||||
        selectedFeature: UIEventSource<Feature>,
 | 
			
		||||
        uploader: ImageUploadManager
 | 
			
		||||
    ) {
 | 
			
		||||
        changes.pendingChanges
 | 
			
		||||
            .stabilized(Constants.updateTimeoutSec * 1000)
 | 
			
		||||
            .addCallback(() => changes.flushChanges("Flushing changes due to timeout"))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -96,7 +96,7 @@ export default class FeaturePropertiesStore {
 | 
			
		|||
        if (newId === undefined) {
 | 
			
		||||
            // We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
 | 
			
		||||
            const element = this._elements.get(oldId)
 | 
			
		||||
            if(!element || element.data === undefined){
 | 
			
		||||
            if (!element || element.data === undefined) {
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
            element.data._deleted = "yes"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -96,7 +96,7 @@ export default class GeoJsonSource implements FeatureSource {
 | 
			
		|||
        const url = this.url
 | 
			
		||||
        try {
 | 
			
		||||
            const cacheAge = (options?.maxCacheAgeSec ?? 300) * 1000
 | 
			
		||||
            let json = <{features: Feature[]}> await Utils.downloadJsonCached(url, cacheAge)
 | 
			
		||||
            let json = <{ features: Feature[] }>await Utils.downloadJsonCached(url, cacheAge)
 | 
			
		||||
 | 
			
		||||
            if (json.features === undefined || json.features === null) {
 | 
			
		||||
                json.features = []
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,8 +32,10 @@ export interface SnappingOptions {
 | 
			
		|||
    reusePointWithin?: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class SnappingFeatureSource implements FeatureSource<Feature<Point,  { "snapped-to": string; dist: number }>> {
 | 
			
		||||
    public readonly features: Store<[Feature<Point,  { "snapped-to": string; dist: number }>]>
 | 
			
		||||
export default class SnappingFeatureSource
 | 
			
		||||
    implements FeatureSource<Feature<Point, { "snapped-to": string; dist: number }>>
 | 
			
		||||
{
 | 
			
		||||
    public readonly features: Store<[Feature<Point, { "snapped-to": string; dist: number }>]>
 | 
			
		||||
    /*Contains the id of the way it snapped to*/
 | 
			
		||||
    public readonly snappedTo: Store<string>
 | 
			
		||||
    private readonly _snappedTo: UIEventSource<string>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -817,7 +817,7 @@ export class GeoOperations {
 | 
			
		|||
                }
 | 
			
		||||
                return undefined
 | 
			
		||||
            default:
 | 
			
		||||
                throw "Unkown location type: " + location+" for feature "+feature.properties.id
 | 
			
		||||
                throw "Unkown location type: " + location + " for feature " + feature.properties.id
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,9 +42,12 @@ export class ImageUploadManager {
 | 
			
		|||
        const failed = this.getCounterFor(this._uploadFailed, "*")
 | 
			
		||||
        const done = this.getCounterFor(this._uploadFinished, "*")
 | 
			
		||||
 | 
			
		||||
        this.isUploading = this.getCounterFor(this._uploadStarted, "*").map(startedCount => {
 | 
			
		||||
            return startedCount > failed.data  + done.data
 | 
			
		||||
        }, [failed, done])
 | 
			
		||||
        this.isUploading = this.getCounterFor(this._uploadStarted, "*").map(
 | 
			
		||||
            (startedCount) => {
 | 
			
		||||
                return startedCount > failed.data + done.data
 | 
			
		||||
            },
 | 
			
		||||
            [failed, done]
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -92,9 +92,9 @@ export class Imgur extends ImageProvider implements ImageUploader {
 | 
			
		|||
     *
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    public async DownloadAttribution(providedImage: {url: string}): Promise<LicenseInfo> {
 | 
			
		||||
    public async DownloadAttribution(providedImage: { url: string }): Promise<LicenseInfo> {
 | 
			
		||||
        const url = providedImage.url
 | 
			
		||||
        const hash =  url.substr("https://i.imgur.com/".length).split(/\.jpe?g/i)[0]
 | 
			
		||||
        const hash = url.substr("https://i.imgur.com/".length).split(/\.jpe?g/i)[0]
 | 
			
		||||
 | 
			
		||||
        const apiUrl = "https://api.imgur.com/3/image/" + hash
 | 
			
		||||
        const response = await Utils.downloadJsonCached(apiUrl, 365 * 24 * 60 * 60, {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -167,7 +167,7 @@ export class Mapillary extends ImageProvider {
 | 
			
		|||
        const url_hd = <string>response["thumb_original_url"]
 | 
			
		||||
        const date = new Date()
 | 
			
		||||
        date.setTime(response["captured_at"])
 | 
			
		||||
        return <ProvidedImage> {
 | 
			
		||||
        return <ProvidedImage>{
 | 
			
		||||
            id: "" + mapillaryId,
 | 
			
		||||
            url,
 | 
			
		||||
            url_hd,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@ export class WikimediaImageProvider extends ImageProvider {
 | 
			
		|||
    public static readonly singleton = new WikimediaImageProvider()
 | 
			
		||||
    public static readonly apiUrls = [
 | 
			
		||||
        "https://commons.wikimedia.org/wiki/",
 | 
			
		||||
        "https://upload.wikimedia.org"
 | 
			
		||||
        "https://upload.wikimedia.org",
 | 
			
		||||
    ]
 | 
			
		||||
    public static readonly commonsPrefixes = [...WikimediaImageProvider.apiUrls, "File:"]
 | 
			
		||||
    private readonly commons_key = "wikimedia_commons"
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +42,6 @@ export class WikimediaImageProvider extends ImageProvider {
 | 
			
		|||
            return baseUrl
 | 
			
		||||
        }
 | 
			
		||||
        return baseUrl + `?width=500&height=400`
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static startsWithCommonsPrefix(value: string): boolean {
 | 
			
		||||
| 
						 | 
				
			
			@ -174,7 +173,7 @@ export class WikimediaImageProvider extends ImageProvider {
 | 
			
		|||
            url_hd: WikimediaImageProvider.PrepareUrl(image, true),
 | 
			
		||||
            key: undefined,
 | 
			
		||||
            provider: this,
 | 
			
		||||
            id: image
 | 
			
		||||
            id: image,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,7 +60,7 @@ export class OsmConnection {
 | 
			
		|||
        oauth_token?: UIEventSource<string>
 | 
			
		||||
        // Used to keep multiple changesets open and to write to the correct changeset
 | 
			
		||||
        singlePage?: boolean
 | 
			
		||||
        attemptLogin?: true | boolean,
 | 
			
		||||
        attemptLogin?: true | boolean
 | 
			
		||||
        /**
 | 
			
		||||
         * If true: automatically check if we're still online every 5 minutes + fetch messages
 | 
			
		||||
         */
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +119,7 @@ export class OsmConnection {
 | 
			
		|||
        this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false)
 | 
			
		||||
 | 
			
		||||
        this.updateAuthObject()
 | 
			
		||||
        if(!this.fakeUser){
 | 
			
		||||
        if (!this.fakeUser) {
 | 
			
		||||
            self.CheckForMessagesContinuously()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -202,9 +202,9 @@ export class OsmConnection {
 | 
			
		|||
        this.auth.xhr(
 | 
			
		||||
            {
 | 
			
		||||
                method: "GET",
 | 
			
		||||
                path: "/api/0.6/user/details"
 | 
			
		||||
                path: "/api/0.6/user/details",
 | 
			
		||||
            },
 | 
			
		||||
            function(err, details: XMLDocument) {
 | 
			
		||||
            function (err, details: XMLDocument) {
 | 
			
		||||
                if (err != null) {
 | 
			
		||||
                    console.log("Could not login due to:", err)
 | 
			
		||||
                    self.loadingStatus.setData("error")
 | 
			
		||||
| 
						 | 
				
			
			@ -313,12 +313,12 @@ export class OsmConnection {
 | 
			
		|||
                <any>{
 | 
			
		||||
                    method,
 | 
			
		||||
                    options: {
 | 
			
		||||
                        header
 | 
			
		||||
                        header,
 | 
			
		||||
                    },
 | 
			
		||||
                    content,
 | 
			
		||||
                    path: `/api/0.6/${path}`
 | 
			
		||||
                    path: `/api/0.6/${path}`,
 | 
			
		||||
                },
 | 
			
		||||
                function(err, response) {
 | 
			
		||||
                function (err, response) {
 | 
			
		||||
                    if (err !== null) {
 | 
			
		||||
                        error(err)
 | 
			
		||||
                    } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -398,7 +398,7 @@ export class OsmConnection {
 | 
			
		|||
            "notes.json",
 | 
			
		||||
            content,
 | 
			
		||||
            {
 | 
			
		||||
                "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
 | 
			
		||||
                "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
 | 
			
		||||
            },
 | 
			
		||||
            true
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -439,7 +439,7 @@ export class OsmConnection {
 | 
			
		|||
            file: gpx,
 | 
			
		||||
            description: options.description,
 | 
			
		||||
            tags: options.labels?.join(",") ?? "",
 | 
			
		||||
            visibility: options.visibility
 | 
			
		||||
            visibility: options.visibility,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!contents.description) {
 | 
			
		||||
| 
						 | 
				
			
			@ -447,9 +447,9 @@ export class OsmConnection {
 | 
			
		|||
        }
 | 
			
		||||
        const extras = {
 | 
			
		||||
            file:
 | 
			
		||||
                "; filename=\"" +
 | 
			
		||||
                '; filename="' +
 | 
			
		||||
                (options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
 | 
			
		||||
                "\"\r\nContent-Type: application/gpx+xml"
 | 
			
		||||
                '"\r\nContent-Type: application/gpx+xml',
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const boundary = "987654"
 | 
			
		||||
| 
						 | 
				
			
			@ -457,7 +457,7 @@ export class OsmConnection {
 | 
			
		|||
        let body = ""
 | 
			
		||||
        for (const key in contents) {
 | 
			
		||||
            body += "--" + boundary + "\r\n"
 | 
			
		||||
            body += "Content-Disposition: form-data; name=\"" + key + "\""
 | 
			
		||||
            body += 'Content-Disposition: form-data; name="' + key + '"'
 | 
			
		||||
            if (extras[key] !== undefined) {
 | 
			
		||||
                body += extras[key]
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -468,7 +468,7 @@ export class OsmConnection {
 | 
			
		|||
 | 
			
		||||
        const response = await this.post("gpx/create", body, {
 | 
			
		||||
            "Content-Type": "multipart/form-data; boundary=" + boundary,
 | 
			
		||||
            "Content-Length": body.length
 | 
			
		||||
            "Content-Length": body.length,
 | 
			
		||||
        })
 | 
			
		||||
        const parsed = JSON.parse(response)
 | 
			
		||||
        console.log("Uploaded GPX track", parsed)
 | 
			
		||||
| 
						 | 
				
			
			@ -491,9 +491,9 @@ export class OsmConnection {
 | 
			
		|||
                {
 | 
			
		||||
                    method: "POST",
 | 
			
		||||
 | 
			
		||||
                    path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`
 | 
			
		||||
                    path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`,
 | 
			
		||||
                },
 | 
			
		||||
                function(err, _) {
 | 
			
		||||
                function (err, _) {
 | 
			
		||||
                    if (err !== null) {
 | 
			
		||||
                        error(err)
 | 
			
		||||
                    } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -508,7 +508,7 @@ export class OsmConnection {
 | 
			
		|||
     * To be called by land.html
 | 
			
		||||
     */
 | 
			
		||||
    public finishLogin(callback: (previousURL: string) => void) {
 | 
			
		||||
        this.auth.authenticate(function() {
 | 
			
		||||
        this.auth.authenticate(function () {
 | 
			
		||||
            // Fully authed at this point
 | 
			
		||||
            console.log("Authentication successful!")
 | 
			
		||||
            const previousLocation = LocalStorageSource.Get("location_before_login")
 | 
			
		||||
| 
						 | 
				
			
			@ -547,7 +547,7 @@ export class OsmConnection {
 | 
			
		|||
                ? "https://mapcomplete.org/land.html"
 | 
			
		||||
                : window.location.protocol + "//" + window.location.host + "/land.html",
 | 
			
		||||
            singlepage: true,
 | 
			
		||||
            auto: true
 | 
			
		||||
            auto: true,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,42 @@
 | 
			
		|||
import { Utils } from "../../Utils"
 | 
			
		||||
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
 | 
			
		||||
export class ThemeMetaTagging {
 | 
			
		||||
   public static readonly themeName = "usersettings"
 | 
			
		||||
    public static readonly themeName = "usersettings"
 | 
			
		||||
 | 
			
		||||
   public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
 | 
			
		||||
      Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) ) 
 | 
			
		||||
      Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' ) 
 | 
			
		||||
      Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href   }) (feat)  ) 
 | 
			
		||||
      Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat)  ) 
 | 
			
		||||
      Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a ) 
 | 
			
		||||
      feat.properties['__current_backgroun'] = 'initial_value'
 | 
			
		||||
   }
 | 
			
		||||
}
 | 
			
		||||
    public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
 | 
			
		||||
        Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
 | 
			
		||||
            feat.properties._description
 | 
			
		||||
                .match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
 | 
			
		||||
                ?.at(1)
 | 
			
		||||
        )
 | 
			
		||||
        Utils.AddLazyProperty(
 | 
			
		||||
            feat.properties,
 | 
			
		||||
            "_d",
 | 
			
		||||
            () => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? ""
 | 
			
		||||
        )
 | 
			
		||||
        Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
 | 
			
		||||
            ((feat) => {
 | 
			
		||||
                const e = document.createElement("div")
 | 
			
		||||
                e.innerHTML = feat.properties._d
 | 
			
		||||
                return Array.from(e.getElementsByTagName("a")).filter(
 | 
			
		||||
                    (a) => a.href.match(/mastodon|en.osm.town/) !== null
 | 
			
		||||
                )[0]?.href
 | 
			
		||||
            })(feat)
 | 
			
		||||
        )
 | 
			
		||||
        Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
 | 
			
		||||
            ((feat) => {
 | 
			
		||||
                const e = document.createElement("div")
 | 
			
		||||
                e.innerHTML = feat.properties._d
 | 
			
		||||
                return Array.from(e.getElementsByTagName("a")).filter(
 | 
			
		||||
                    (a) => a.getAttribute("rel")?.indexOf("me") >= 0
 | 
			
		||||
                )[0]?.href
 | 
			
		||||
            })(feat)
 | 
			
		||||
        )
 | 
			
		||||
        Utils.AddLazyProperty(
 | 
			
		||||
            feat.properties,
 | 
			
		||||
            "_mastodon_candidate",
 | 
			
		||||
            () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
 | 
			
		||||
        )
 | 
			
		||||
        feat.properties["__current_backgroun"] = "initial_value"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -243,10 +243,7 @@ export class TagUtils {
 | 
			
		|||
     *
 | 
			
		||||
     * TagUtils.SplitKeysRegex([new Tag("isced:level", "bachelor; master")], true) // => {"isced:level": ["bachelor","master"]}
 | 
			
		||||
     */
 | 
			
		||||
    static SplitKeysRegex(
 | 
			
		||||
        tagsFilters: UploadableTag[],
 | 
			
		||||
        allowRegex: false
 | 
			
		||||
    ): Record<string, string[]>
 | 
			
		||||
    static SplitKeysRegex(tagsFilters: UploadableTag[], allowRegex: false): Record<string, string[]>
 | 
			
		||||
    static SplitKeysRegex(
 | 
			
		||||
        tagsFilters: UploadableTag[],
 | 
			
		||||
        allowRegex: boolean
 | 
			
		||||
| 
						 | 
				
			
			@ -514,7 +511,7 @@ export class TagUtils {
 | 
			
		|||
            )}`
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        return <UploadableTag> t
 | 
			
		||||
        return <UploadableTag>t
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -723,7 +720,9 @@ export class TagUtils {
 | 
			
		|||
        }
 | 
			
		||||
        if (typeof json != "string") {
 | 
			
		||||
            if (json["and"] !== undefined && json["or"] !== undefined) {
 | 
			
		||||
                throw `${context}: Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined. Did you override a value? Perhaps use \`"=parent": { ... }\` instead of \"parent": {...}\` to trigger a replacement and not a fuse of values. The value is ${JSON.stringify(json)}`
 | 
			
		||||
                throw `${context}: Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined. Did you override a value? Perhaps use \`"=parent": { ... }\` instead of \"parent": {...}\` to trigger a replacement and not a fuse of values. The value is ${JSON.stringify(
 | 
			
		||||
                    json
 | 
			
		||||
                )}`
 | 
			
		||||
            }
 | 
			
		||||
            if (json["and"] !== undefined) {
 | 
			
		||||
                return new And(json["and"].map((t) => TagUtils.Tag(t, context)))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -237,10 +237,10 @@ export abstract class Store<T> implements Readable<T> {
 | 
			
		|||
 | 
			
		||||
    public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>): Store<X> {
 | 
			
		||||
        return this.bind((t) => {
 | 
			
		||||
            if(t=== null){
 | 
			
		||||
            if (t === null) {
 | 
			
		||||
                return null
 | 
			
		||||
            }
 | 
			
		||||
            if (t === undefined ) {
 | 
			
		||||
            if (t === undefined) {
 | 
			
		||||
                return undefined
 | 
			
		||||
            }
 | 
			
		||||
            return f(<Exclude<T, undefined | null>>t)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,9 @@ interface JsonLdLoaderOptions {
 | 
			
		|||
    country?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PropertiesSpec<T extends string> = Partial<Record<T, string | string[] | Partial<Record<T, string>>>>
 | 
			
		||||
type PropertiesSpec<T extends string> = Partial<
 | 
			
		||||
    Record<T, string | string[] | Partial<Record<T, string>>>
 | 
			
		||||
>
 | 
			
		||||
 | 
			
		||||
export default class LinkedDataLoader {
 | 
			
		||||
    private static readonly COMPACTING_CONTEXT = {
 | 
			
		||||
| 
						 | 
				
			
			@ -25,23 +27,23 @@ export default class LinkedDataLoader {
 | 
			
		|||
        opening_hours: { "@id": "http://schema.org/openingHoursSpecification" },
 | 
			
		||||
        openingHours: { "@id": "http://schema.org/openingHours", "@container": "@set" },
 | 
			
		||||
        geo: { "@id": "http://schema.org/geo" },
 | 
			
		||||
        "alt_name": { "@id": "http://schema.org/alternateName" }
 | 
			
		||||
        alt_name: { "@id": "http://schema.org/alternateName" },
 | 
			
		||||
    }
 | 
			
		||||
    private static COMPACTING_CONTEXT_OH = {
 | 
			
		||||
        dayOfWeek: { "@id": "http://schema.org/dayOfWeek", "@container": "@set" },
 | 
			
		||||
        closes: {
 | 
			
		||||
            "@id": "http://schema.org/closes",
 | 
			
		||||
            "@type": "http://www.w3.org/2001/XMLSchema#time"
 | 
			
		||||
            "@type": "http://www.w3.org/2001/XMLSchema#time",
 | 
			
		||||
        },
 | 
			
		||||
        opens: {
 | 
			
		||||
            "@id": "http://schema.org/opens",
 | 
			
		||||
            "@type": "http://www.w3.org/2001/XMLSchema#time"
 | 
			
		||||
        }
 | 
			
		||||
            "@type": "http://www.w3.org/2001/XMLSchema#time",
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    private static formatters: Record<"phone" | "email" | "website", Validator> = {
 | 
			
		||||
        phone: new PhoneValidator(),
 | 
			
		||||
        email: new EmailValidator(),
 | 
			
		||||
        website: new UrlValidator(undefined, undefined, true)
 | 
			
		||||
        website: new UrlValidator(undefined, undefined, true),
 | 
			
		||||
    }
 | 
			
		||||
    private static ignoreKeys = [
 | 
			
		||||
        "http://schema.org/logo",
 | 
			
		||||
| 
						 | 
				
			
			@ -54,53 +56,61 @@ export default class LinkedDataLoader {
 | 
			
		|||
        "http://schema.org/description",
 | 
			
		||||
        "http://schema.org/hasMap",
 | 
			
		||||
        "http://schema.org/priceRange",
 | 
			
		||||
        "http://schema.org/contactPoint"
 | 
			
		||||
        "http://schema.org/contactPoint",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    private static shapeToPolygon(str: string): Polygon {
 | 
			
		||||
        const polygon = str.substring("POLYGON ((".length, str.length - 2)
 | 
			
		||||
        return <Polygon>{
 | 
			
		||||
            type: "Polygon",
 | 
			
		||||
            coordinates: [polygon.split(",").map(coors => coors.trim().split(" ").map(n => Number(n)))]
 | 
			
		||||
            coordinates: [
 | 
			
		||||
                polygon.split(",").map((coors) =>
 | 
			
		||||
                    coors
 | 
			
		||||
                        .trim()
 | 
			
		||||
                        .split(" ")
 | 
			
		||||
                        .map((n) => Number(n))
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static async geoToGeometry(geo): Promise<Geometry> {
 | 
			
		||||
        if (Array.isArray(geo)) {
 | 
			
		||||
            const features = await Promise.all(geo.map(g => LinkedDataLoader.geoToGeometry(g)))
 | 
			
		||||
            const polygon = features.find(f => f.type === "Polygon")
 | 
			
		||||
            const features = await Promise.all(geo.map((g) => LinkedDataLoader.geoToGeometry(g)))
 | 
			
		||||
            const polygon = features.find((f) => f.type === "Polygon")
 | 
			
		||||
            if (polygon) {
 | 
			
		||||
                return polygon
 | 
			
		||||
            }
 | 
			
		||||
            const ls = features.find(f => f.type === "LineString")
 | 
			
		||||
            const ls = features.find((f) => f.type === "LineString")
 | 
			
		||||
            if (ls) {
 | 
			
		||||
                return ls
 | 
			
		||||
            }
 | 
			
		||||
            return features[0]
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (geo["@type"] === "http://schema.org/GeoCoordinates") {
 | 
			
		||||
 | 
			
		||||
            const context = {
 | 
			
		||||
                lat: {
 | 
			
		||||
                    "@id": "http://schema.org/latitude",
 | 
			
		||||
                    "@type": "http://www.w3.org/2001/XMLSchema#double"
 | 
			
		||||
                    "@type": "http://www.w3.org/2001/XMLSchema#double",
 | 
			
		||||
                },
 | 
			
		||||
                lon: {
 | 
			
		||||
                    "@id": "http://schema.org/longitude",
 | 
			
		||||
                    "@type": "http://www.w3.org/2001/XMLSchema#double"
 | 
			
		||||
                }
 | 
			
		||||
                    "@type": "http://www.w3.org/2001/XMLSchema#double",
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
            const flattened = await jsonld.compact(geo, context)
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                type: "Point",
 | 
			
		||||
                coordinates: [Number(flattened.lon), Number(flattened.lat)]
 | 
			
		||||
                coordinates: [Number(flattened.lon), Number(flattened.lat)],
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (geo["@type"] === "http://schema.org/GeoShape" && geo["http://schema.org/polygon"] !== undefined) {
 | 
			
		||||
        if (
 | 
			
		||||
            geo["@type"] === "http://schema.org/GeoShape" &&
 | 
			
		||||
            geo["http://schema.org/polygon"] !== undefined
 | 
			
		||||
        ) {
 | 
			
		||||
            const str = geo["http://schema.org/polygon"]["@value"]
 | 
			
		||||
            LinkedDataLoader.shapeToPolygon(str)
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -167,9 +177,8 @@ export default class LinkedDataLoader {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    static async compact(data: object, options?: JsonLdLoaderOptions): Promise<object> {
 | 
			
		||||
 | 
			
		||||
        if (Array.isArray(data)) {
 | 
			
		||||
            return await Promise.all(data.map(point => LinkedDataLoader.compact(point, options)))
 | 
			
		||||
            return await Promise.all(data.map((point) => LinkedDataLoader.compact(point, options)))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const country = options?.country
 | 
			
		||||
| 
						 | 
				
			
			@ -214,10 +223,13 @@ export default class LinkedDataLoader {
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return compacted
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static async fetchJsonLd(url: string, options?: JsonLdLoaderOptions, useProxy: boolean = false): Promise<object> {
 | 
			
		||||
    static async fetchJsonLd(
 | 
			
		||||
        url: string,
 | 
			
		||||
        options?: JsonLdLoaderOptions,
 | 
			
		||||
        useProxy: boolean = false
 | 
			
		||||
    ): Promise<object> {
 | 
			
		||||
        if (useProxy) {
 | 
			
		||||
            url = Constants.linkedDataProxy.replace("{url}", encodeURIComponent(url))
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -230,7 +242,10 @@ export default class LinkedDataLoader {
 | 
			
		|||
     * @param externalData
 | 
			
		||||
     * @param currentData
 | 
			
		||||
     */
 | 
			
		||||
    static removeDuplicateData(externalData: Record<string, string>, currentData: Record<string, string>): Record<string, string> {
 | 
			
		||||
    static removeDuplicateData(
 | 
			
		||||
        externalData: Record<string, string>,
 | 
			
		||||
        currentData: Record<string, string>
 | 
			
		||||
    ): Record<string, string> {
 | 
			
		||||
        const d = { ...externalData }
 | 
			
		||||
        delete d["@context"]
 | 
			
		||||
        for (const k in d) {
 | 
			
		||||
| 
						 | 
				
			
			@ -239,7 +254,7 @@ export default class LinkedDataLoader {
 | 
			
		|||
                continue
 | 
			
		||||
            }
 | 
			
		||||
            if (k === "opening_hours") {
 | 
			
		||||
                const oh = [].concat(...v.split(";").map(r => OH.ParseRule(r) ?? []))
 | 
			
		||||
                const oh = [].concat(...v.split(";").map((r) => OH.ParseRule(r) ?? []))
 | 
			
		||||
                const merged = OH.ToString(OH.MergeTimes(oh ?? []))
 | 
			
		||||
                if (merged === d[k]) {
 | 
			
		||||
                    delete d[k]
 | 
			
		||||
| 
						 | 
				
			
			@ -259,7 +274,9 @@ export default class LinkedDataLoader {
 | 
			
		|||
        const properties: Record<string, string> = {}
 | 
			
		||||
        for (const k in linkedData) {
 | 
			
		||||
            if (linkedData[k].length > 1) {
 | 
			
		||||
                throw "Found multiple values in properties for " + k + ": " + linkedData[k].join("; ")
 | 
			
		||||
                throw (
 | 
			
		||||
                    "Found multiple values in properties for " + k + ": " + linkedData[k].join("; ")
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            properties[k] = linkedData[k].join("; ")
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -268,7 +285,7 @@ export default class LinkedDataLoader {
 | 
			
		|||
        if (properties["latitude"] && properties["longitude"]) {
 | 
			
		||||
            geometry = {
 | 
			
		||||
                type: "Point",
 | 
			
		||||
                coordinates: [Number(properties["longitude"]), Number(properties["latitude"])]
 | 
			
		||||
                coordinates: [Number(properties["longitude"]), Number(properties["latitude"])],
 | 
			
		||||
            }
 | 
			
		||||
            delete properties["latitude"]
 | 
			
		||||
            delete properties["longitude"]
 | 
			
		||||
| 
						 | 
				
			
			@ -277,11 +294,10 @@ export default class LinkedDataLoader {
 | 
			
		|||
            geometry = LinkedDataLoader.shapeToPolygon(properties["shape"])
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        const geo: GeoJSON = {
 | 
			
		||||
            type: "Feature",
 | 
			
		||||
            properties,
 | 
			
		||||
            geometry
 | 
			
		||||
            geometry,
 | 
			
		||||
        }
 | 
			
		||||
        delete linkedData.geo
 | 
			
		||||
        delete properties.shape
 | 
			
		||||
| 
						 | 
				
			
			@ -293,7 +309,9 @@ export default class LinkedDataLoader {
 | 
			
		|||
        return geo
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static patchVeloparkProperties(input: Record<string, Set<string>>): Record<string, string[]> {
 | 
			
		||||
    private static patchVeloparkProperties(
 | 
			
		||||
        input: Record<string, Set<string>>
 | 
			
		||||
    ): Record<string, string[]> {
 | 
			
		||||
        const output: Record<string, string[]> = {}
 | 
			
		||||
        console.log("Input for patchVelopark:", input)
 | 
			
		||||
        for (const k in input) {
 | 
			
		||||
| 
						 | 
				
			
			@ -309,12 +327,12 @@ export default class LinkedDataLoader {
 | 
			
		|||
            if (!output[key]) {
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
            output[key] = output[key].map(v => applyF(v))
 | 
			
		||||
            output[key] = output[key].map((v) => applyF(v))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function asBoolean(key: string, invert: boolean = false) {
 | 
			
		||||
            on(key, str => {
 | 
			
		||||
                const isTrue = ("" + str) === "true" || str === "True" || str === "yes"
 | 
			
		||||
            on(key, (str) => {
 | 
			
		||||
                const isTrue = "" + str === "true" || str === "True" || str === "yes"
 | 
			
		||||
                if (isTrue != invert) {
 | 
			
		||||
                    return "yes"
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -322,7 +340,7 @@ export default class LinkedDataLoader {
 | 
			
		|||
            })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        on("maxstay", (maxstay => {
 | 
			
		||||
        on("maxstay", (maxstay) => {
 | 
			
		||||
            const match = maxstay.match(/P([0-9]+)D/)
 | 
			
		||||
            if (match) {
 | 
			
		||||
                const days = Number(match[1])
 | 
			
		||||
| 
						 | 
				
			
			@ -332,7 +350,7 @@ export default class LinkedDataLoader {
 | 
			
		|||
                return days + " days"
 | 
			
		||||
            }
 | 
			
		||||
            return maxstay
 | 
			
		||||
        }))
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        function rename(source: string, target: string) {
 | 
			
		||||
            if (output[source] === undefined || output[source] === null) {
 | 
			
		||||
| 
						 | 
				
			
			@ -342,41 +360,40 @@ export default class LinkedDataLoader {
 | 
			
		|||
            delete output[source]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        on("phone", (p => this.formatters["phone"].reformat(p, () => "be")))
 | 
			
		||||
        on("phone", (p) => this.formatters["phone"].reformat(p, () => "be"))
 | 
			
		||||
 | 
			
		||||
        for (const attribute in LinkedDataLoader.formatters) {
 | 
			
		||||
            on(attribute, p => LinkedDataLoader.formatters[attribute].reformat(p))
 | 
			
		||||
            on(attribute, (p) => LinkedDataLoader.formatters[attribute].reformat(p))
 | 
			
		||||
        }
 | 
			
		||||
        rename("phone", "operator:phone")
 | 
			
		||||
        rename("email", "operator:email")
 | 
			
		||||
        rename("website", "operator:website")
 | 
			
		||||
 | 
			
		||||
        on("charge", (p => {
 | 
			
		||||
        on("charge", (p) => {
 | 
			
		||||
            if (Number(p) === 0) {
 | 
			
		||||
                output["fee"] = ["no"]
 | 
			
		||||
                return undefined
 | 
			
		||||
            }
 | 
			
		||||
            return "€" + Number(p)
 | 
			
		||||
        }))
 | 
			
		||||
        })
 | 
			
		||||
        if (output["charge"] && output["timeUnit"]) {
 | 
			
		||||
            const duration = Number(output["chargeEnd"] ?? "1") - Number(output["chargeStart"] ?? "0")
 | 
			
		||||
            const duration =
 | 
			
		||||
                Number(output["chargeEnd"] ?? "1") - Number(output["chargeStart"] ?? "0")
 | 
			
		||||
            const unit = output["timeUnit"][0]
 | 
			
		||||
            let durationStr = ""
 | 
			
		||||
            if (duration !== 1) {
 | 
			
		||||
                durationStr = duration + ""
 | 
			
		||||
            }
 | 
			
		||||
            output["charge"] = output["charge"].map(c => c + "/" + (durationStr + unit))
 | 
			
		||||
            output["charge"] = output["charge"].map((c) => c + "/" + (durationStr + unit))
 | 
			
		||||
        }
 | 
			
		||||
        delete output["chargeEnd"]
 | 
			
		||||
        delete output["chargeStart"]
 | 
			
		||||
        delete output["timeUnit"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        asBoolean("covered")
 | 
			
		||||
        asBoolean("fee", true)
 | 
			
		||||
        asBoolean("publicAccess")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        output["images"]?.forEach((p, i) => {
 | 
			
		||||
            if (i === 0) {
 | 
			
		||||
                output["image"] = [p]
 | 
			
		||||
| 
						 | 
				
			
			@ -386,9 +403,15 @@ export default class LinkedDataLoader {
 | 
			
		|||
        })
 | 
			
		||||
        delete output["images"]
 | 
			
		||||
 | 
			
		||||
        on("access", audience => {
 | 
			
		||||
 | 
			
		||||
            if (["brede publiek", "iedereen", "bezoekers", "iedereen - vooral bezoekers gemeentehuis of bibliotheek."].indexOf(audience.toLowerCase()) >= 0) {
 | 
			
		||||
        on("access", (audience) => {
 | 
			
		||||
            if (
 | 
			
		||||
                [
 | 
			
		||||
                    "brede publiek",
 | 
			
		||||
                    "iedereen",
 | 
			
		||||
                    "bezoekers",
 | 
			
		||||
                    "iedereen - vooral bezoekers gemeentehuis of bibliotheek.",
 | 
			
		||||
                ].indexOf(audience.toLowerCase()) >= 0
 | 
			
		||||
            ) {
 | 
			
		||||
                return "yes"
 | 
			
		||||
            }
 | 
			
		||||
            if (audience.toLowerCase().startsWith("bezoekers")) {
 | 
			
		||||
| 
						 | 
				
			
			@ -404,15 +427,22 @@ export default class LinkedDataLoader {
 | 
			
		|||
                return "permissive"
 | 
			
		||||
                //   return "members"
 | 
			
		||||
            }
 | 
			
		||||
            if (audience.toLowerCase().startsWith("klanten") ||
 | 
			
		||||
            if (
 | 
			
		||||
                audience.toLowerCase().startsWith("klanten") ||
 | 
			
		||||
                audience.toLowerCase().startsWith("werknemers") ||
 | 
			
		||||
                audience.toLowerCase().startsWith("personeel")) {
 | 
			
		||||
                audience.toLowerCase().startsWith("personeel")
 | 
			
		||||
            ) {
 | 
			
		||||
                return "customers"
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            console.warn("Suspicious 'access'-tag:", audience, "for", input["ref:velopark"], " assuming yes")
 | 
			
		||||
            console.warn(
 | 
			
		||||
                "Suspicious 'access'-tag:",
 | 
			
		||||
                audience,
 | 
			
		||||
                "for",
 | 
			
		||||
                input["ref:velopark"],
 | 
			
		||||
                " assuming yes"
 | 
			
		||||
            )
 | 
			
		||||
            return "yes"
 | 
			
		||||
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        if (output["publicAccess"]?.[0] == "no") {
 | 
			
		||||
| 
						 | 
				
			
			@ -420,7 +450,10 @@ export default class LinkedDataLoader {
 | 
			
		|||
        }
 | 
			
		||||
        delete output["publicAccess"]
 | 
			
		||||
 | 
			
		||||
        if (output["restrictions"]?.[0] === "Geen bromfietsen, noch andere gemotoriseerde voertuigen") {
 | 
			
		||||
        if (
 | 
			
		||||
            output["restrictions"]?.[0] ===
 | 
			
		||||
            "Geen bromfietsen, noch andere gemotoriseerde voertuigen"
 | 
			
		||||
        ) {
 | 
			
		||||
            output["motor_vehicle"] = ["no"]
 | 
			
		||||
            delete output["restrictions"]
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -437,7 +470,6 @@ export default class LinkedDataLoader {
 | 
			
		|||
        }
 | 
			
		||||
        rename("capacityTandem", "capacity:tandem")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if (output["electricBikeType"]) {
 | 
			
		||||
            output["electric_bicycle"] = ["yes"]
 | 
			
		||||
            delete output["electricBikeType"]
 | 
			
		||||
| 
						 | 
				
			
			@ -450,14 +482,18 @@ export default class LinkedDataLoader {
 | 
			
		|||
        return output
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static async fetchVeloparkProperty<T extends string, G extends T>(url: string, property: string, variable?: string): Promise<SparqlResult<T, G>> {
 | 
			
		||||
    private static async fetchVeloparkProperty<T extends string, G extends T>(
 | 
			
		||||
        url: string,
 | 
			
		||||
        property: string,
 | 
			
		||||
        variable?: string
 | 
			
		||||
    ): Promise<SparqlResult<T, G>> {
 | 
			
		||||
        const results = await new TypedSparql().typedSparql<T, G>(
 | 
			
		||||
            {
 | 
			
		||||
                schema: "http://schema.org/",
 | 
			
		||||
                mv: "http://schema.mobivoc.org/",
 | 
			
		||||
                gr: "http://purl.org/goodrelations/v1#",
 | 
			
		||||
                vp: "https://data.velopark.be/openvelopark/vocabulary#",
 | 
			
		||||
                vpt: "https://data.velopark.be/openvelopark/terms#"
 | 
			
		||||
                vpt: "https://data.velopark.be/openvelopark/terms#",
 | 
			
		||||
            },
 | 
			
		||||
            [url],
 | 
			
		||||
            undefined,
 | 
			
		||||
| 
						 | 
				
			
			@ -467,24 +503,24 @@ export default class LinkedDataLoader {
 | 
			
		|||
        return results
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static async fetchVeloparkGraphProperty<T extends string>(url: string, property: string, subExpr?: string):
 | 
			
		||||
        Promise<SparqlResult<T, "g">> {
 | 
			
		||||
    private static async fetchVeloparkGraphProperty<T extends string>(
 | 
			
		||||
        url: string,
 | 
			
		||||
        property: string,
 | 
			
		||||
        subExpr?: string
 | 
			
		||||
    ): Promise<SparqlResult<T, "g">> {
 | 
			
		||||
        return await new TypedSparql().typedSparql<T, "g">(
 | 
			
		||||
            {
 | 
			
		||||
                schema: "http://schema.org/",
 | 
			
		||||
                mv: "http://schema.mobivoc.org/",
 | 
			
		||||
                gr: "http://purl.org/goodrelations/v1#",
 | 
			
		||||
                vp: "https://data.velopark.be/openvelopark/vocabulary#",
 | 
			
		||||
                vpt: "https://data.velopark.be/openvelopark/terms#"
 | 
			
		||||
                vpt: "https://data.velopark.be/openvelopark/terms#",
 | 
			
		||||
            },
 | 
			
		||||
            [url],
 | 
			
		||||
            "g",
 | 
			
		||||
            "  ?parking a <http://schema.mobivoc.org/BicycleParkingStation>",
 | 
			
		||||
 | 
			
		||||
            S.graph("g",
 | 
			
		||||
                "?section " + property + " " + (subExpr ?? ""),
 | 
			
		||||
                "?section a ?type"
 | 
			
		||||
            )
 | 
			
		||||
            S.graph("g", "?section " + property + " " + (subExpr ?? ""), "?section a ?type")
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -493,8 +529,10 @@ export default class LinkedDataLoader {
 | 
			
		|||
     * THis is a workaround for 'optional' not working decently
 | 
			
		||||
     * @param r0
 | 
			
		||||
     */
 | 
			
		||||
    public static mergeResults(...r0: SparqlResult<string, string>[]): SparqlResult<string, string> {
 | 
			
		||||
        const r: SparqlResult<string> = { "default": {} }
 | 
			
		||||
    public static mergeResults(
 | 
			
		||||
        ...r0: SparqlResult<string, string>[]
 | 
			
		||||
    ): SparqlResult<string, string> {
 | 
			
		||||
        const r: SparqlResult<string> = { default: {} }
 | 
			
		||||
        for (const subResult of r0) {
 | 
			
		||||
            if (Object.keys(subResult).length === 0) {
 | 
			
		||||
                continue
 | 
			
		||||
| 
						 | 
				
			
			@ -524,22 +562,31 @@ export default class LinkedDataLoader {
 | 
			
		|||
        return r
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async fetchEntry<T extends string>(directUrl: string,
 | 
			
		||||
                                                     propertiesWithoutGraph: PropertiesSpec<T>,
 | 
			
		||||
                                                     propertiesInGraph: PropertiesSpec<T>,
 | 
			
		||||
                                                     extra?: string[]): Promise<SparqlResult<T, string>> {
 | 
			
		||||
    public static async fetchEntry<T extends string>(
 | 
			
		||||
        directUrl: string,
 | 
			
		||||
        propertiesWithoutGraph: PropertiesSpec<T>,
 | 
			
		||||
        propertiesInGraph: PropertiesSpec<T>,
 | 
			
		||||
        extra?: string[]
 | 
			
		||||
    ): Promise<SparqlResult<T, string>> {
 | 
			
		||||
        const allPartialResults: SparqlResult<T, string>[] = []
 | 
			
		||||
        for (const propertyName in propertiesWithoutGraph) {
 | 
			
		||||
            const e = propertiesWithoutGraph[propertyName]
 | 
			
		||||
            if (typeof e === "string") {
 | 
			
		||||
                const variableName = e
 | 
			
		||||
                const result = await this.fetchVeloparkProperty(directUrl, propertyName, "?" + variableName)
 | 
			
		||||
                const result = await this.fetchVeloparkProperty(
 | 
			
		||||
                    directUrl,
 | 
			
		||||
                    propertyName,
 | 
			
		||||
                    "?" + variableName
 | 
			
		||||
                )
 | 
			
		||||
                allPartialResults.push(result)
 | 
			
		||||
            } else {
 | 
			
		||||
                for (const subProperty in e) {
 | 
			
		||||
                    const variableName = e[subProperty]
 | 
			
		||||
                    const result = await this.fetchVeloparkProperty(directUrl,
 | 
			
		||||
                        propertyName, `[${subProperty} ?${variableName}]    `)
 | 
			
		||||
                    const result = await this.fetchVeloparkProperty(
 | 
			
		||||
                        directUrl,
 | 
			
		||||
                        propertyName,
 | 
			
		||||
                        `[${subProperty} ?${variableName}]    `
 | 
			
		||||
                    )
 | 
			
		||||
                    allPartialResults.push(result)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -553,7 +600,11 @@ export default class LinkedDataLoader {
 | 
			
		|||
                    if (variableName.match(/[a-zA-Z_]+/)) {
 | 
			
		||||
                        variableName = "?" + subquery
 | 
			
		||||
                    }
 | 
			
		||||
                    const result = await this.fetchVeloparkGraphProperty(directUrl, propertyName, variableName)
 | 
			
		||||
                    const result = await this.fetchVeloparkGraphProperty(
 | 
			
		||||
                        directUrl,
 | 
			
		||||
                        propertyName,
 | 
			
		||||
                        variableName
 | 
			
		||||
                    )
 | 
			
		||||
                    allPartialResults.push(result)
 | 
			
		||||
                }
 | 
			
		||||
            } else if (typeof e === "string") {
 | 
			
		||||
| 
						 | 
				
			
			@ -561,13 +612,20 @@ export default class LinkedDataLoader {
 | 
			
		|||
                if (variableName.match(/[a-zA-Z_]+/)) {
 | 
			
		||||
                    variableName = "?" + e
 | 
			
		||||
                }
 | 
			
		||||
                const result = await this.fetchVeloparkGraphProperty(directUrl, propertyName, variableName)
 | 
			
		||||
                const result = await this.fetchVeloparkGraphProperty(
 | 
			
		||||
                    directUrl,
 | 
			
		||||
                    propertyName,
 | 
			
		||||
                    variableName
 | 
			
		||||
                )
 | 
			
		||||
                allPartialResults.push(result)
 | 
			
		||||
            } else {
 | 
			
		||||
                for (const subProperty in e) {
 | 
			
		||||
                    const variableName = e[subProperty]
 | 
			
		||||
                    const result = await this.fetchVeloparkGraphProperty(directUrl,
 | 
			
		||||
                        propertyName, `[${subProperty} ?${variableName}]    `)
 | 
			
		||||
                    const result = await this.fetchVeloparkGraphProperty(
 | 
			
		||||
                        directUrl,
 | 
			
		||||
                        propertyName,
 | 
			
		||||
                        `[${subProperty} ?${variableName}]    `
 | 
			
		||||
                    )
 | 
			
		||||
                    allPartialResults.push(result)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -581,7 +639,6 @@ export default class LinkedDataLoader {
 | 
			
		|||
        const results = this.mergeResults(...allPartialResults)
 | 
			
		||||
 | 
			
		||||
        return results
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -593,22 +650,21 @@ export default class LinkedDataLoader {
 | 
			
		|||
        const withProxyUrl = Constants.linkedDataProxy.replace("{url}", encodeURIComponent(url))
 | 
			
		||||
        const optionalPaths: Record<string, string | Record<string, string>> = {
 | 
			
		||||
            "schema:interactionService": {
 | 
			
		||||
                "schema:url": "website"
 | 
			
		||||
                "schema:url": "website",
 | 
			
		||||
            },
 | 
			
		||||
            "schema:name": "name",
 | 
			
		||||
            "mv:operatedBy": {
 | 
			
		||||
                "gr:legalName": "operator"
 | 
			
		||||
 | 
			
		||||
                "gr:legalName": "operator",
 | 
			
		||||
            },
 | 
			
		||||
            "schema:contactPoint": {
 | 
			
		||||
                "schema:email": "email",
 | 
			
		||||
                "schema:telephone": "phone"
 | 
			
		||||
                "schema:telephone": "phone",
 | 
			
		||||
            },
 | 
			
		||||
            "schema:dateModified":"_last_edit_timestamp"
 | 
			
		||||
            "schema:dateModified": "_last_edit_timestamp",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const graphOptionalPaths = {
 | 
			
		||||
            "a": "type",
 | 
			
		||||
            a: "type",
 | 
			
		||||
            "vp:covered": "covered",
 | 
			
		||||
            "vp:maximumParkingDuration": "maxstay",
 | 
			
		||||
            "mv:totalCapacity": "capacity",
 | 
			
		||||
| 
						 | 
				
			
			@ -619,22 +675,27 @@ export default class LinkedDataLoader {
 | 
			
		|||
            "schema:geo": {
 | 
			
		||||
                "schema:latitude": "latitude",
 | 
			
		||||
                "schema:longitude": "longitude",
 | 
			
		||||
                "schema:polygon": "shape"
 | 
			
		||||
                "schema:polygon": "shape",
 | 
			
		||||
            },
 | 
			
		||||
            "schema:priceSpecification": {
 | 
			
		||||
                "mv:freeOfCharge": "fee",
 | 
			
		||||
                "schema:price": "charge"
 | 
			
		||||
            }
 | 
			
		||||
                "schema:price": "charge",
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const extra = [
 | 
			
		||||
            "schema:priceSpecification [ mv:dueForTime [ mv:timeStartValue ?chargeStart; mv:timeEndValue ?chargeEnd; mv:timeUnit ?timeUnit ]  ]",
 | 
			
		||||
            "vp:allows [vp:bicycleType <https://data.velopark.be/openvelopark/terms#CargoBicycle>; vp:bicyclesAmount ?capacityCargobike; vp:bicycleType ?cargoBikeType]",
 | 
			
		||||
            "vp:allows [vp:bicycleType <https://data.velopark.be/openvelopark/terms#ElectricBicycle>; vp:bicyclesAmount ?capacityElectric; vp:bicycleType ?electricBikeType]",
 | 
			
		||||
            "vp:allows [vp:bicycleType <https://data.velopark.be/openvelopark/terms#TandemBicycle>; vp:bicyclesAmount ?capacityTandem; vp:bicycleType ?tandemBikeType]"
 | 
			
		||||
            "vp:allows [vp:bicycleType <https://data.velopark.be/openvelopark/terms#TandemBicycle>; vp:bicyclesAmount ?capacityTandem; vp:bicycleType ?tandemBikeType]",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        const unpatched = await this.fetchEntry(withProxyUrl, optionalPaths, graphOptionalPaths, extra)
 | 
			
		||||
        const unpatched = await this.fetchEntry(
 | 
			
		||||
            withProxyUrl,
 | 
			
		||||
            optionalPaths,
 | 
			
		||||
            graphOptionalPaths,
 | 
			
		||||
            extra
 | 
			
		||||
        )
 | 
			
		||||
        const patched: Feature[] = []
 | 
			
		||||
        for (const section in unpatched) {
 | 
			
		||||
            const p = LinkedDataLoader.patchVeloparkProperties(unpatched[section])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,9 @@ import { GeoOperations } from "../GeoOperations"
 | 
			
		|||
import ScriptUtils from "../../../scripts/ScriptUtils"
 | 
			
		||||
 | 
			
		||||
export class MangroveIdentity {
 | 
			
		||||
    private readonly keypair: UIEventSource<CryptoKeyPair> = new UIEventSource<CryptoKeyPair>(undefined)
 | 
			
		||||
    private readonly keypair: UIEventSource<CryptoKeyPair> = new UIEventSource<CryptoKeyPair>(
 | 
			
		||||
        undefined
 | 
			
		||||
    )
 | 
			
		||||
    /**
 | 
			
		||||
     * Same as the one in the user settings
 | 
			
		||||
     */
 | 
			
		||||
| 
						 | 
				
			
			@ -14,16 +16,19 @@ export class MangroveIdentity {
 | 
			
		|||
    private readonly key_id: UIEventSource<string> = new UIEventSource<string>(undefined)
 | 
			
		||||
    private readonly _mangroveIdentityCreationDate: UIEventSource<string>
 | 
			
		||||
 | 
			
		||||
    constructor(mangroveIdentity: UIEventSource<string>, mangroveIdentityCreationDate: UIEventSource<string>) {
 | 
			
		||||
    constructor(
 | 
			
		||||
        mangroveIdentity: UIEventSource<string>,
 | 
			
		||||
        mangroveIdentityCreationDate: UIEventSource<string>
 | 
			
		||||
    ) {
 | 
			
		||||
        this.mangroveIdentity = mangroveIdentity
 | 
			
		||||
        this._mangroveIdentityCreationDate = mangroveIdentityCreationDate
 | 
			
		||||
        mangroveIdentity.addCallbackAndRunD(async (data) => {
 | 
			
		||||
               await this.setKeypair(data)
 | 
			
		||||
            await this.setKeypair(data)
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async setKeypair(data: string){
 | 
			
		||||
        console.log("Setting keypair from",data)
 | 
			
		||||
    private async setKeypair(data: string) {
 | 
			
		||||
        console.log("Setting keypair from", data)
 | 
			
		||||
        const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data))
 | 
			
		||||
        this.keypair.setData(keypair)
 | 
			
		||||
        const pem = await MangroveReviews.publicToPem(keypair.publicKey)
 | 
			
		||||
| 
						 | 
				
			
			@ -71,22 +76,21 @@ export class MangroveIdentity {
 | 
			
		|||
        return this.key_id
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private geoReviewsById: Store<(Review & { kid: string; signature: string })[]> =
 | 
			
		||||
        undefined
 | 
			
		||||
    private geoReviewsById: Store<(Review & { kid: string; signature: string })[]> = undefined
 | 
			
		||||
 | 
			
		||||
    public getGeoReviews(): Store<(Review & { kid: string, signature: string })[] | undefined> {
 | 
			
		||||
    public getGeoReviews(): Store<(Review & { kid: string; signature: string })[] | undefined> {
 | 
			
		||||
        if (!this.geoReviewsById) {
 | 
			
		||||
            const all = this.getAllReviews()
 | 
			
		||||
            this.geoReviewsById = this.getAllReviews().mapD(reviews => reviews.filter(
 | 
			
		||||
                review => {
 | 
			
		||||
            this.geoReviewsById = this.getAllReviews().mapD((reviews) =>
 | 
			
		||||
                reviews.filter((review) => {
 | 
			
		||||
                    try {
 | 
			
		||||
                        const subjectUrl = new URL(review.sub)
 | 
			
		||||
                        return subjectUrl.protocol === "geo:"
 | 
			
		||||
                    } catch (e) {
 | 
			
		||||
                        return false
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            ))
 | 
			
		||||
                })
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        return this.geoReviewsById
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -108,12 +112,12 @@ export class MangroveIdentity {
 | 
			
		|||
                return []
 | 
			
		||||
            }
 | 
			
		||||
            const allReviews = await MangroveReviews.getReviews({
 | 
			
		||||
                kid: pem
 | 
			
		||||
                kid: pem,
 | 
			
		||||
            })
 | 
			
		||||
            this.allReviewsById.setData(
 | 
			
		||||
                allReviews.reviews.map((r) => ({
 | 
			
		||||
                    ...r,
 | 
			
		||||
                    ...r.payload
 | 
			
		||||
                    ...r.payload,
 | 
			
		||||
                }))
 | 
			
		||||
            )
 | 
			
		||||
        })
 | 
			
		||||
| 
						 | 
				
			
			@ -247,7 +251,13 @@ export default class FeatureReviews {
 | 
			
		|||
        if (cached !== undefined) {
 | 
			
		||||
            return cached
 | 
			
		||||
        }
 | 
			
		||||
        const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options,testmode )
 | 
			
		||||
        const featureReviews = new FeatureReviews(
 | 
			
		||||
            feature,
 | 
			
		||||
            tagsSource,
 | 
			
		||||
            mangroveIdentity,
 | 
			
		||||
            options,
 | 
			
		||||
            testmode
 | 
			
		||||
        )
 | 
			
		||||
        FeatureReviews._featureReviewsCache[key] = featureReviews
 | 
			
		||||
        return featureReviews
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -268,7 +278,7 @@ export default class FeatureReviews {
 | 
			
		|||
        }
 | 
			
		||||
        const r: Review = {
 | 
			
		||||
            sub: this.subjectUri.data,
 | 
			
		||||
            ...review
 | 
			
		||||
            ...review,
 | 
			
		||||
        }
 | 
			
		||||
        const keypair: CryptoKeyPair = await this._identity.getKeypair()
 | 
			
		||||
        const jwt = await MangroveReviews.signReview(keypair, r)
 | 
			
		||||
| 
						 | 
				
			
			@ -283,7 +293,7 @@ export default class FeatureReviews {
 | 
			
		|||
            ...r,
 | 
			
		||||
            kid,
 | 
			
		||||
            signature: jwt,
 | 
			
		||||
            madeByLoggedInUser: new ImmutableStore(true)
 | 
			
		||||
            madeByLoggedInUser: new ImmutableStore(true),
 | 
			
		||||
        }
 | 
			
		||||
        this._reviews.data.push(reviewWithKid)
 | 
			
		||||
        this._reviews.ping()
 | 
			
		||||
| 
						 | 
				
			
			@ -331,7 +341,7 @@ export default class FeatureReviews {
 | 
			
		|||
                signature: reviewData.signature,
 | 
			
		||||
                madeByLoggedInUser: this._identity.getKeyId().map((user_key_id) => {
 | 
			
		||||
                    return reviewData.kid === user_key_id
 | 
			
		||||
                })
 | 
			
		||||
                }),
 | 
			
		||||
            })
 | 
			
		||||
            hasNew = true
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -352,7 +362,7 @@ export default class FeatureReviews {
 | 
			
		|||
        // https://www.rfc-editor.org/rfc/rfc5870#section-3.4.2
 | 
			
		||||
        // `u` stands for `uncertainty`, https://www.rfc-editor.org/rfc/rfc5870#section-3.4.3
 | 
			
		||||
        const self = this
 | 
			
		||||
        return this._name.map(function(name) {
 | 
			
		||||
        return this._name.map(function (name) {
 | 
			
		||||
            let uri = `geo:${self._lat},${self._lon}?u=${Math.round(self._uncertainty)}`
 | 
			
		||||
            if (name) {
 | 
			
		||||
                uri += "&q=" + (dontEncodeName ? name : encodeURIComponent(name))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,11 +3,16 @@ import { QueryEngine } from "@comunica/query-sparql"
 | 
			
		|||
 | 
			
		||||
export type SparqlVar<T extends string> = `?${T}`
 | 
			
		||||
export type SparqlExpr = string
 | 
			
		||||
export type SparqlStmt<T extends string> = `${SparqlVar<T> | SparqlExpr} ${SparqlVar<T> | SparqlExpr} ${SparqlVar<T> | SparqlExpr}`
 | 
			
		||||
export type SparqlStmt<T extends string> = `${SparqlVar<T> | SparqlExpr} ${
 | 
			
		||||
    | SparqlVar<T>
 | 
			
		||||
    | SparqlExpr} ${SparqlVar<T> | SparqlExpr}`
 | 
			
		||||
 | 
			
		||||
export type TypedExpression<T extends string> = SparqlStmt<T> | string
 | 
			
		||||
 | 
			
		||||
export type SparqlResult<T extends string, G extends string = "default"> = Record<G, Record<T, Set<string>>>
 | 
			
		||||
export type SparqlResult<T extends string, G extends string = "default"> = Record<
 | 
			
		||||
    G,
 | 
			
		||||
    Record<T, Set<string>>
 | 
			
		||||
>
 | 
			
		||||
 | 
			
		||||
export default class TypedSparql {
 | 
			
		||||
    private readonly comunica: QueryEngine
 | 
			
		||||
| 
						 | 
				
			
			@ -16,15 +21,23 @@ export default class TypedSparql {
 | 
			
		|||
        this.comunica = new QueryEngine()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static optional<Vars extends string>(...statements: (TypedExpression<Vars> | string)[]): TypedExpression<Vars> {
 | 
			
		||||
    public static optional<Vars extends string>(
 | 
			
		||||
        ...statements: (TypedExpression<Vars> | string)[]
 | 
			
		||||
    ): TypedExpression<Vars> {
 | 
			
		||||
        return ` OPTIONAL { ${statements.join(". \n\t")} }`
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static graph<Vars extends string>(varname: Vars, ...statements: (string | TypedExpression<Vars>)[]): TypedExpression<Vars> {
 | 
			
		||||
    public static graph<Vars extends string>(
 | 
			
		||||
        varname: Vars,
 | 
			
		||||
        ...statements: (string | TypedExpression<Vars>)[]
 | 
			
		||||
    ): TypedExpression<Vars> {
 | 
			
		||||
        return `GRAPH ?${varname} { ${statements.join(".\n")} }`
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static about<Vars extends string>(varname: Vars, ...statements: `${SparqlVar<Vars> | SparqlExpr} ${SparqlVar<Vars> | SparqlExpr}`[]): TypedExpression<Vars> {
 | 
			
		||||
    public static about<Vars extends string>(
 | 
			
		||||
        varname: Vars,
 | 
			
		||||
        ...statements: `${SparqlVar<Vars> | SparqlExpr} ${SparqlVar<Vars> | SparqlExpr}`[]
 | 
			
		||||
    ): TypedExpression<Vars> {
 | 
			
		||||
        return `?${varname} ${statements.join(";")}`
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -43,23 +56,22 @@ export default class TypedSparql {
 | 
			
		|||
    ): Promise<SparqlResult<VARS, G>> {
 | 
			
		||||
        const q: string = this.buildQuery(query, prefixes)
 | 
			
		||||
        try {
 | 
			
		||||
            const bindingsStream = await this.comunica.queryBindings(
 | 
			
		||||
                q, { sources: [...sources], lenient: true }
 | 
			
		||||
            )
 | 
			
		||||
            const bindingsStream = await this.comunica.queryBindings(q, {
 | 
			
		||||
                sources: [...sources],
 | 
			
		||||
                lenient: true,
 | 
			
		||||
            })
 | 
			
		||||
            const bindings = await bindingsStream.toArray()
 | 
			
		||||
 | 
			
		||||
            const resultAllGraphs: SparqlResult<VARS, G> = <SparqlResult<VARS, G>>{}
 | 
			
		||||
 | 
			
		||||
            bindings.forEach(item => {
 | 
			
		||||
            bindings.forEach((item) => {
 | 
			
		||||
                const result = <Record<VARS | G, Set<string>>>{}
 | 
			
		||||
                item.forEach(
 | 
			
		||||
                    (value, key) => {
 | 
			
		||||
                        if (!result[key.value]) {
 | 
			
		||||
                            result[key.value] = new Set()
 | 
			
		||||
                        }
 | 
			
		||||
                        result[key.value].add(value.value)
 | 
			
		||||
                item.forEach((value, key) => {
 | 
			
		||||
                    if (!result[key.value]) {
 | 
			
		||||
                        result[key.value] = new Set()
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
                    result[key.value].add(value.value)
 | 
			
		||||
                })
 | 
			
		||||
                if (graphVariable && result[graphVariable]?.size > 0) {
 | 
			
		||||
                    const id = Array.from(result[graphVariable])?.[0] ?? "default"
 | 
			
		||||
                    resultAllGraphs[id] = result
 | 
			
		||||
| 
						 | 
				
			
			@ -73,14 +85,13 @@ export default class TypedSparql {
 | 
			
		|||
            console.log("Running query failed. The query is", q)
 | 
			
		||||
            throw e
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private buildQuery(
 | 
			
		||||
        query: readonly string[],
 | 
			
		||||
        prefixes: Record<string, string>): string {
 | 
			
		||||
    private buildQuery(query: readonly string[], prefixes: Record<string, string>): string {
 | 
			
		||||
        return `
 | 
			
		||||
            ${Object.keys(prefixes).map(prefix => `PREFIX ${prefix}: <${prefixes[prefix]}>`).join("\n")}
 | 
			
		||||
            ${Object.keys(prefixes)
 | 
			
		||||
                .map((prefix) => `PREFIX ${prefix}: <${prefixes[prefix]}>`)
 | 
			
		||||
                .join("\n")}
 | 
			
		||||
            SELECT *
 | 
			
		||||
            WHERE {
 | 
			
		||||
            ${query.join(". \n")} .
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,7 +24,7 @@ export interface RasterLayerProperties {
 | 
			
		|||
    readonly url: string
 | 
			
		||||
    readonly category?: string | EliCategory
 | 
			
		||||
    readonly type?: "vector" | "raster" | string
 | 
			
		||||
    readonly style?: string,
 | 
			
		||||
    readonly style?: string
 | 
			
		||||
 | 
			
		||||
    readonly attribution?: {
 | 
			
		||||
        readonly url?: string
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,9 +51,10 @@ export class AvailableRasterLayers {
 | 
			
		|||
    /**
 | 
			
		||||
     * The default background layer that any theme uses which does not explicitly define a background
 | 
			
		||||
     */
 | 
			
		||||
    public static readonly defaultBackgroundLayer: RasterLayerPolygon = AvailableRasterLayers.globalLayers.find(l => {
 | 
			
		||||
        return l.properties.id === "protomaps.sunny"
 | 
			
		||||
    })
 | 
			
		||||
    public static readonly defaultBackgroundLayer: RasterLayerPolygon =
 | 
			
		||||
        AvailableRasterLayers.globalLayers.find((l) => {
 | 
			
		||||
            return l.properties.id === "protomaps.sunny"
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    public static layersAvailableAt(
 | 
			
		||||
        location: Store<{ lon: number; lat: number }>,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,31 +65,37 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
 | 
			
		|||
 | 
			
		||||
        const newFilters: FilterConfigJson[] = []
 | 
			
		||||
        const filters = <(FilterConfigJson | string)[]>json.filter
 | 
			
		||||
        for (let i = 0; i < filters.length; i++){
 | 
			
		||||
        for (let i = 0; i < filters.length; i++) {
 | 
			
		||||
            const filter = filters[i]
 | 
			
		||||
            if (typeof filter !== "string") {
 | 
			
		||||
                newFilters.push(filter)
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const matchingTr =<TagRenderingConfigJson> json.tagRenderings.find(tr => !!tr && tr["id"] === filter)
 | 
			
		||||
            if(matchingTr){
 | 
			
		||||
                if(!(matchingTr.mappings?.length >= 1)){
 | 
			
		||||
                    context.enters("filter",i ).err("Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings")
 | 
			
		||||
            const matchingTr = <TagRenderingConfigJson>(
 | 
			
		||||
                json.tagRenderings.find((tr) => !!tr && tr["id"] === filter)
 | 
			
		||||
            )
 | 
			
		||||
            if (matchingTr) {
 | 
			
		||||
                if (!(matchingTr.mappings?.length >= 1)) {
 | 
			
		||||
                    context
 | 
			
		||||
                        .enters("filter", i)
 | 
			
		||||
                        .err(
 | 
			
		||||
                            "Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings"
 | 
			
		||||
                        )
 | 
			
		||||
                }
 | 
			
		||||
                const options =  matchingTr.mappings.map(mapping => ({
 | 
			
		||||
                const options = matchingTr.mappings.map((mapping) => ({
 | 
			
		||||
                    question: mapping.then,
 | 
			
		||||
                    osmTags: mapping.if
 | 
			
		||||
                    osmTags: mapping.if,
 | 
			
		||||
                }))
 | 
			
		||||
                options.unshift({
 | 
			
		||||
                    question: {
 | 
			
		||||
                        en:"All types"
 | 
			
		||||
                        en: "All types",
 | 
			
		||||
                    },
 | 
			
		||||
                    osmTags: undefined
 | 
			
		||||
                    osmTags: undefined,
 | 
			
		||||
                })
 | 
			
		||||
                newFilters.push({
 | 
			
		||||
                    id: filter,
 | 
			
		||||
                    options
 | 
			
		||||
                    options,
 | 
			
		||||
                })
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -521,9 +527,9 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
 | 
			
		|||
        json = { ...json }
 | 
			
		||||
        json.tagRenderings = [...json.tagRenderings]
 | 
			
		||||
        const allSpecials: Exclude<RenderingSpecification, string>[] = <any>(
 | 
			
		||||
            ValidationUtils.getAllSpecialVisualisations(<QuestionableTagRenderingConfigJson[]> json.tagRenderings).filter(
 | 
			
		||||
                (spec) => typeof spec !== "string"
 | 
			
		||||
            )
 | 
			
		||||
            ValidationUtils.getAllSpecialVisualisations(
 | 
			
		||||
                <QuestionableTagRenderingConfigJson[]>json.tagRenderings
 | 
			
		||||
            ).filter((spec) => typeof spec !== "string")
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        const questionSpecials = allSpecials.filter((sp) => sp.func.funcName === "questions")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -289,7 +289,10 @@ class AddContextToTranslationsInLayout extends DesugaringStep<LayoutConfigJson>
 | 
			
		|||
    convert(json: LayoutConfigJson): LayoutConfigJson {
 | 
			
		||||
        const conversion = new AddContextToTranslations<LayoutConfigJson>("themes:")
 | 
			
		||||
        // The context is used to generate the 'context' in the translation .It _must_ be `json.id` to correctly link into weblate
 | 
			
		||||
        return conversion.convert(json, ConversionContext.construct([json.id],["AddContextToTranslation"]))
 | 
			
		||||
        return conversion.convert(
 | 
			
		||||
            json,
 | 
			
		||||
            ConversionContext.construct([json.id], ["AddContextToTranslation"])
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -602,19 +605,32 @@ class PostvalidateTheme extends DesugaringStep<LayoutConfigJson> {
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        for (const layer of json.layers) {
 | 
			
		||||
            if(typeof layer === "string"){
 | 
			
		||||
            if (typeof layer === "string") {
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
            const config = <LayerConfigJson> layer;
 | 
			
		||||
            const config = <LayerConfigJson>layer
 | 
			
		||||
            const sameAs = config.filter?.["sameAs"]
 | 
			
		||||
            if(!sameAs){
 | 
			
		||||
            if (!sameAs) {
 | 
			
		||||
                continue
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const matchingLayer = json.layers.find(l => l["id"] === sameAs)
 | 
			
		||||
            if(!matchingLayer){
 | 
			
		||||
                const closeLayers = Utils.sortedByLevenshteinDistance(sameAs, json.layers, l => l["id"]).map(l => l["id"])
 | 
			
		||||
                context.enters("layers", config.id, "filter","sameAs").err("The layer "+config.id+" follows the filter state of layer "+sameAs+", but no layer with this name was found.\n\tDid you perhaps mean one of: "+closeLayers.slice(0, 3).join(", "))
 | 
			
		||||
            const matchingLayer = json.layers.find((l) => l["id"] === sameAs)
 | 
			
		||||
            if (!matchingLayer) {
 | 
			
		||||
                const closeLayers = Utils.sortedByLevenshteinDistance(
 | 
			
		||||
                    sameAs,
 | 
			
		||||
                    json.layers,
 | 
			
		||||
                    (l) => l["id"]
 | 
			
		||||
                ).map((l) => l["id"])
 | 
			
		||||
                context
 | 
			
		||||
                    .enters("layers", config.id, "filter", "sameAs")
 | 
			
		||||
                    .err(
 | 
			
		||||
                        "The layer " +
 | 
			
		||||
                            config.id +
 | 
			
		||||
                            " follows the filter state of layer " +
 | 
			
		||||
                            sameAs +
 | 
			
		||||
                            ", but no layer with this name was found.\n\tDid you perhaps mean one of: " +
 | 
			
		||||
                            closeLayers.slice(0, 3).join(", ")
 | 
			
		||||
                    )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -278,10 +278,14 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
 | 
			
		|||
 | 
			
		||||
            if (!isCategory && !ValidateTheme._availableLayers.has(backgroundId)) {
 | 
			
		||||
                const options = Array.from(ValidateTheme._availableLayers)
 | 
			
		||||
                const nearby = Utils.sortedByLevenshteinDistance(backgroundId, options, t => t)
 | 
			
		||||
                const nearby = Utils.sortedByLevenshteinDistance(backgroundId, options, (t) => t)
 | 
			
		||||
                context
 | 
			
		||||
                    .enter("defaultBackgroundId")
 | 
			
		||||
                    .err(`This layer ID is not known: ${backgroundId}. Perhaps you meant one of ${nearby.slice(0,5).join(", ")}`)
 | 
			
		||||
                    .err(
 | 
			
		||||
                        `This layer ID is not known: ${backgroundId}. Perhaps you meant one of ${nearby
 | 
			
		||||
                            .slice(0, 5)
 | 
			
		||||
                            .join(", ")}`
 | 
			
		||||
                    )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1014,7 +1018,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
 | 
			
		|||
                    ) {
 | 
			
		||||
                        continue
 | 
			
		||||
                    }
 | 
			
		||||
                    if(json.freeform.key.indexOf("wikidata")>=0){
 | 
			
		||||
                    if (json.freeform.key.indexOf("wikidata") >= 0) {
 | 
			
		||||
                        context
 | 
			
		||||
                            .enter("render")
 | 
			
		||||
                            .err(
 | 
			
		||||
| 
						 | 
				
			
			@ -1273,7 +1277,14 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
 | 
			
		|||
                // It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list
 | 
			
		||||
                context
 | 
			
		||||
                    .enter("tagRenderings")
 | 
			
		||||
                    .err("Some tagrenderings have a duplicate id: " + duplicates.join(", ")+"\n"+JSON.stringify(json.tagRenderings.filter(tr=> duplicates.indexOf(tr["id"])>=0)))
 | 
			
		||||
                    .err(
 | 
			
		||||
                        "Some tagrenderings have a duplicate id: " +
 | 
			
		||||
                            duplicates.join(", ") +
 | 
			
		||||
                            "\n" +
 | 
			
		||||
                            JSON.stringify(
 | 
			
		||||
                                json.tagRenderings.filter((tr) => duplicates.indexOf(tr["id"]) >= 0)
 | 
			
		||||
                            )
 | 
			
		||||
                    )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1865,11 +1876,14 @@ export class ValidateThemeEnsemble extends Conversion<
 | 
			
		|||
        string,
 | 
			
		||||
        {
 | 
			
		||||
            tags: TagsFilter
 | 
			
		||||
            foundInTheme: string[],
 | 
			
		||||
            foundInTheme: string[]
 | 
			
		||||
            isCounted: boolean
 | 
			
		||||
        }
 | 
			
		||||
    > {
 | 
			
		||||
        const idToSource = new Map<string, { tags: TagsFilter; foundInTheme: string[], isCounted: boolean }>()
 | 
			
		||||
        const idToSource = new Map<
 | 
			
		||||
            string,
 | 
			
		||||
            { tags: TagsFilter; foundInTheme: string[]; isCounted: boolean }
 | 
			
		||||
        >()
 | 
			
		||||
 | 
			
		||||
        for (const theme of json) {
 | 
			
		||||
            for (const layer of theme.layers) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,8 +62,6 @@ export default class ValidationUtils {
 | 
			
		|||
            }
 | 
			
		||||
 | 
			
		||||
            for (const key in translation) {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                const template = translation[key]
 | 
			
		||||
                const parts = SpecialVisualizations.constructSpecification(template)
 | 
			
		||||
                const specials = parts.filter((p) => typeof p !== "string")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -91,7 +91,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                    mercatorCrs: json.source["mercatorCrs"],
 | 
			
		||||
                    idKey: json.source["idKey"],
 | 
			
		||||
                },
 | 
			
		||||
                json.id,
 | 
			
		||||
                json.id
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -106,8 +106,8 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
        }
 | 
			
		||||
        this.units = [].concat(
 | 
			
		||||
            ...(json.units ?? []).map((unitJson, i) =>
 | 
			
		||||
                Unit.fromJson(unitJson, `${context}.unit[${i}]`),
 | 
			
		||||
            ),
 | 
			
		||||
                Unit.fromJson(unitJson, `${context}.unit[${i}]`)
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if (json.description !== undefined) {
 | 
			
		||||
| 
						 | 
				
			
			@ -122,7 +122,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
        if (json.calculatedTags !== undefined) {
 | 
			
		||||
            if (!official) {
 | 
			
		||||
                console.warn(
 | 
			
		||||
                    `Unofficial theme ${this.id} with custom javascript! This is a security risk`,
 | 
			
		||||
                    `Unofficial theme ${this.id} with custom javascript! This is a security risk`
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            this.calculatedTags = []
 | 
			
		||||
| 
						 | 
				
			
			@ -191,7 +191,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                tags: pr.tags.map((t) => TagUtils.SimpleTag(t)),
 | 
			
		||||
                description: Translations.T(
 | 
			
		||||
                    pr.description,
 | 
			
		||||
                    `${translationContext}.presets.${i}.description`,
 | 
			
		||||
                    `${translationContext}.presets.${i}.description`
 | 
			
		||||
                ),
 | 
			
		||||
                preciseInput: preciseInput,
 | 
			
		||||
                exampleImages: pr.exampleImages,
 | 
			
		||||
| 
						 | 
				
			
			@ -205,7 +205,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
 | 
			
		||||
        if (json.lineRendering) {
 | 
			
		||||
            this.lineRendering = Utils.NoNull(json.lineRendering).map(
 | 
			
		||||
                (r, i) => new LineRenderingConfig(r, `${context}[${i}]`),
 | 
			
		||||
                (r, i) => new LineRenderingConfig(r, `${context}[${i}]`)
 | 
			
		||||
            )
 | 
			
		||||
        } else {
 | 
			
		||||
            this.lineRendering = []
 | 
			
		||||
| 
						 | 
				
			
			@ -213,7 +213,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
 | 
			
		||||
        if (json.pointRendering) {
 | 
			
		||||
            this.mapRendering = Utils.NoNull(json.pointRendering).map(
 | 
			
		||||
                (r, i) => new PointRenderingConfig(r, `${context}[${i}](${this.id})`),
 | 
			
		||||
                (r, i) => new PointRenderingConfig(r, `${context}[${i}](${this.id})`)
 | 
			
		||||
            )
 | 
			
		||||
        } else {
 | 
			
		||||
            this.mapRendering = []
 | 
			
		||||
| 
						 | 
				
			
			@ -225,7 +225,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                    r.location.has("centroid") ||
 | 
			
		||||
                    r.location.has("projected_centerpoint") ||
 | 
			
		||||
                    r.location.has("start") ||
 | 
			
		||||
                    r.location.has("end"),
 | 
			
		||||
                    r.location.has("end")
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
| 
						 | 
				
			
			@ -247,7 +247,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                Constants.priviliged_layers.indexOf(<any>this.id) < 0 &&
 | 
			
		||||
                this.source !== null /*library layer*/ &&
 | 
			
		||||
                !this.source?.geojsonSource?.startsWith(
 | 
			
		||||
                    "https://api.openstreetmap.org/api/0.6/notes.json",
 | 
			
		||||
                    "https://api.openstreetmap.org/api/0.6/notes.json"
 | 
			
		||||
                )
 | 
			
		||||
            ) {
 | 
			
		||||
                throw (
 | 
			
		||||
| 
						 | 
				
			
			@ -266,7 +266,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                    typeof tr !== "string" &&
 | 
			
		||||
                    tr["builtin"] === undefined &&
 | 
			
		||||
                    tr["id"] === undefined &&
 | 
			
		||||
                    tr["rewrite"] === undefined,
 | 
			
		||||
                    tr["rewrite"] === undefined
 | 
			
		||||
            ) ?? []
 | 
			
		||||
        if (missingIds?.length > 0 && official) {
 | 
			
		||||
            console.error("Some tagRenderings of", this.id, "are missing an id:", missingIds)
 | 
			
		||||
| 
						 | 
				
			
			@ -277,8 +277,8 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
            (tr, i) =>
 | 
			
		||||
                new TagRenderingConfig(
 | 
			
		||||
                    <QuestionableTagRenderingConfigJson>tr,
 | 
			
		||||
                    this.id + ".tagRenderings[" + i + "]",
 | 
			
		||||
                ),
 | 
			
		||||
                    this.id + ".tagRenderings[" + i + "]"
 | 
			
		||||
                )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
| 
						 | 
				
			
			@ -352,7 +352,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
 | 
			
		||||
    public GetBaseTags(): Record<string, string> {
 | 
			
		||||
        return TagUtils.changeAsProperties(
 | 
			
		||||
            this.source?.osmTags?.asChange({ id: "node/-1" }) ?? [{ k: "id", v: "node/-1" }],
 | 
			
		||||
            this.source?.osmTags?.asChange({ id: "node/-1" }) ?? [{ k: "id", v: "node/-1" }]
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -365,7 +365,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
            neededLayer: string
 | 
			
		||||
        }[] = [],
 | 
			
		||||
        addedByDefault = false,
 | 
			
		||||
        canBeIncluded = true,
 | 
			
		||||
        canBeIncluded = true
 | 
			
		||||
    ): BaseUIElement {
 | 
			
		||||
        const extraProps: (string | BaseUIElement)[] = []
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -374,32 +374,32 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
        if (canBeIncluded) {
 | 
			
		||||
            if (addedByDefault) {
 | 
			
		||||
                extraProps.push(
 | 
			
		||||
                    "**This layer is included automatically in every theme. This layer might contain no points**",
 | 
			
		||||
                    "**This layer is included automatically in every theme. This layer might contain no points**"
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            if (this.shownByDefault === false) {
 | 
			
		||||
                extraProps.push(
 | 
			
		||||
                    "This layer is not visible by default and must be enabled in the filter by the user. ",
 | 
			
		||||
                    "This layer is not visible by default and must be enabled in the filter by the user. "
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            if (this.title === undefined) {
 | 
			
		||||
                extraProps.push(
 | 
			
		||||
                    "Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable.",
 | 
			
		||||
                    "Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable."
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            if (this.name === undefined && this.shownByDefault === false) {
 | 
			
		||||
                extraProps.push(
 | 
			
		||||
                    "This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-<id>=true",
 | 
			
		||||
                    "This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-<id>=true"
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            if (this.name === undefined) {
 | 
			
		||||
                extraProps.push(
 | 
			
		||||
                    "Not visible in the layer selection by default. If you want to make this layer toggable, override `name`",
 | 
			
		||||
                    "Not visible in the layer selection by default. If you want to make this layer toggable, override `name`"
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
            if (this.mapRendering.length === 0) {
 | 
			
		||||
                extraProps.push(
 | 
			
		||||
                    "Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`",
 | 
			
		||||
                    "Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`"
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -411,26 +411,27 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                            : undefined,
 | 
			
		||||
                        "This layer is loaded from an external source, namely ",
 | 
			
		||||
                        new FixedUiElement(this.source.geojsonSource).SetClass("code"),
 | 
			
		||||
                    ]),
 | 
			
		||||
                    ])
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            extraProps.push(
 | 
			
		||||
                "This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data.",
 | 
			
		||||
                "This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data."
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let usingLayer: BaseUIElement[] = []
 | 
			
		||||
        if (!addedByDefault) {
 | 
			
		||||
 | 
			
		||||
            if (usedInThemes?.length > 0) {
 | 
			
		||||
                usingLayer = [
 | 
			
		||||
                    new Title("Themes using this layer", 4),
 | 
			
		||||
                    new List(
 | 
			
		||||
                        (usedInThemes ?? []).map((id) => new Link(id, "https://mapcomplete.org/" + id)),
 | 
			
		||||
                        (usedInThemes ?? []).map(
 | 
			
		||||
                            (id) => new Link(id, "https://mapcomplete.org/" + id)
 | 
			
		||||
                        )
 | 
			
		||||
                    ),
 | 
			
		||||
                ]
 | 
			
		||||
            } else if(this.source !== null) {
 | 
			
		||||
            } else if (this.source !== null) {
 | 
			
		||||
                usingLayer = [new FixedUiElement("No themes use this layer")]
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -443,7 +444,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                    " into the layout as it depends on it: ",
 | 
			
		||||
                    dep.reason,
 | 
			
		||||
                    "(" + dep.context + ")",
 | 
			
		||||
                ]),
 | 
			
		||||
                ])
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -452,7 +453,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                new Combine([
 | 
			
		||||
                    "This layer is needed as dependency for layer",
 | 
			
		||||
                    new Link(revDep, "#" + revDep),
 | 
			
		||||
                ]),
 | 
			
		||||
                ])
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -464,14 +465,14 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                        return undefined
 | 
			
		||||
                    }
 | 
			
		||||
                    const embedded: (Link | string)[] = values.values?.map((v) =>
 | 
			
		||||
                        Link.OsmWiki(values.key, v, true).SetClass("mr-2"),
 | 
			
		||||
                        Link.OsmWiki(values.key, v, true).SetClass("mr-2")
 | 
			
		||||
                    ) ?? ["_no preset options defined, or no values in them_"]
 | 
			
		||||
                    return [
 | 
			
		||||
                        new Combine([
 | 
			
		||||
                            new Link(
 | 
			
		||||
                                "<img src='https://mapcomplete.org/assets/svg/statistics.svg' height='18px'>",
 | 
			
		||||
                                "https://taginfo.openstreetmap.org/keys/" + values.key + "#values",
 | 
			
		||||
                                true,
 | 
			
		||||
                                true
 | 
			
		||||
                            ),
 | 
			
		||||
                            Link.OsmWiki(values.key),
 | 
			
		||||
                        ]).SetClass("flex"),
 | 
			
		||||
| 
						 | 
				
			
			@ -480,7 +481,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                            : new Link(values.type, "../SpecialInputElements.md#" + values.type),
 | 
			
		||||
                        new Combine(embedded).SetClass("flex"),
 | 
			
		||||
                    ]
 | 
			
		||||
                }),
 | 
			
		||||
                })
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        let quickOverview: BaseUIElement = undefined
 | 
			
		||||
| 
						 | 
				
			
			@ -490,7 +491,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                "this quick overview is incomplete",
 | 
			
		||||
                new Table(
 | 
			
		||||
                    ["attribute", "type", "values which are supported by this layer"],
 | 
			
		||||
                    tableRows,
 | 
			
		||||
                    tableRows
 | 
			
		||||
                ).SetClass("zebra-table"),
 | 
			
		||||
            ]).SetClass("flex-col flex")
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -504,7 +505,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                    (mr) =>
 | 
			
		||||
                        mr.RenderIcon(new ImmutableStore<OsmTags>({ id: "node/-1" }), {
 | 
			
		||||
                            includeBadges: false,
 | 
			
		||||
                        }).html,
 | 
			
		||||
                        }).html
 | 
			
		||||
                )
 | 
			
		||||
                .find((i) => i !== undefined)
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -516,7 +517,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                    "Execute on overpass",
 | 
			
		||||
                    Overpass.AsOverpassTurboLink(<TagsFilter>this.source.osmTags.optimize())
 | 
			
		||||
                        .replaceAll("(", "%28")
 | 
			
		||||
                        .replaceAll(")", "%29"),
 | 
			
		||||
                        .replaceAll(")", "%29")
 | 
			
		||||
                )
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                console.error("Could not generate overpasslink for " + this.id)
 | 
			
		||||
| 
						 | 
				
			
			@ -538,19 +539,19 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
                const parts = neededTags["and"]
 | 
			
		||||
                tagsDescription.push(
 | 
			
		||||
                    "Elements must match **all** of the following expressions:",
 | 
			
		||||
                    parts.map((p, i) => i + ". " + p.asHumanString(true, false, {})).join("\n"),
 | 
			
		||||
                    parts.map((p, i) => i + ". " + p.asHumanString(true, false, {})).join("\n")
 | 
			
		||||
                )
 | 
			
		||||
            } else if (neededTags["or"]) {
 | 
			
		||||
                const parts = neededTags["or"]
 | 
			
		||||
                tagsDescription.push(
 | 
			
		||||
                    "Elements must match **any** of the following expressions:",
 | 
			
		||||
                    parts.map((p) => " - " + p.asHumanString(true, false, {})).join("\n"),
 | 
			
		||||
                    parts.map((p) => " - " + p.asHumanString(true, false, {})).join("\n")
 | 
			
		||||
                )
 | 
			
		||||
            } else {
 | 
			
		||||
                tagsDescription.push(
 | 
			
		||||
                    "Elements must match the expression **" +
 | 
			
		||||
                    neededTags.asHumanString(true, false, {}) +
 | 
			
		||||
                    "**",
 | 
			
		||||
                        neededTags.asHumanString(true, false, {}) +
 | 
			
		||||
                        "**"
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -561,7 +562,7 @@ export default class LayerConfig extends WithContextLoader {
 | 
			
		|||
 | 
			
		||||
        return new Combine([
 | 
			
		||||
            new Combine([new Title(this.id, 1), iconImg, this.description, "\n"]).SetClass(
 | 
			
		||||
                "flex flex-col",
 | 
			
		||||
                "flex flex-col"
 | 
			
		||||
            ),
 | 
			
		||||
            new List(extraProps),
 | 
			
		||||
            ...usingLayer,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -313,7 +313,7 @@ export default class LayoutConfig implements LayoutInformation {
 | 
			
		|||
        if (tags === undefined) {
 | 
			
		||||
            return undefined
 | 
			
		||||
        }
 | 
			
		||||
        if(tags.id.startsWith("current_view")){
 | 
			
		||||
        if (tags.id.startsWith("current_view")) {
 | 
			
		||||
            return this.getLayer("current_view")
 | 
			
		||||
        }
 | 
			
		||||
        for (const layer of this.layers) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -232,11 +232,10 @@ export default class TagRenderingConfig {
 | 
			
		|||
                throw "Tagrendering has a 'mappings'-object, but expected a list (" + context + ")"
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const firstMappingSize: string =   json.mappings.map((m) => (m.icon?.["class"])).find(c => !!c)
 | 
			
		||||
            const commonIconSize =
 | 
			
		||||
              firstMappingSize ??
 | 
			
		||||
                json["#iconsize"] ??
 | 
			
		||||
                "small"
 | 
			
		||||
            const firstMappingSize: string = json.mappings
 | 
			
		||||
                .map((m) => m.icon?.["class"])
 | 
			
		||||
                .find((c) => !!c)
 | 
			
		||||
            const commonIconSize = firstMappingSize ?? json["#iconsize"] ?? "small"
 | 
			
		||||
            this.mappings = json.mappings.map((m, i) =>
 | 
			
		||||
                TagRenderingConfig.ExtractMapping(
 | 
			
		||||
                    m,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,10 +6,10 @@
 | 
			
		|||
   */
 | 
			
		||||
  export let selected: UIEventSource<boolean>
 | 
			
		||||
  let _c: boolean = selected.data ?? true
 | 
			
		||||
  let id = `checkbox-input-${Math.round(Math.random()*100000000)}`
 | 
			
		||||
  let id = `checkbox-input-${Math.round(Math.random() * 100000000)}`
 | 
			
		||||
  $: selected.set(_c)
 | 
			
		||||
  selected.addCallbackD(s => {
 | 
			
		||||
      _c = s
 | 
			
		||||
  selected.addCallbackD((s) => {
 | 
			
		||||
    _c = s
 | 
			
		||||
  })
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@
 | 
			
		|||
   */
 | 
			
		||||
  const dispatch = createEventDispatcher()
 | 
			
		||||
  export let cls = "m-0.5 p-0.5 sm:p-1 md:m-1"
 | 
			
		||||
  export let enabled : Store<boolean> = new ImmutableStore(true)
 | 
			
		||||
  export let enabled: Store<boolean> = new ImmutableStore(true)
 | 
			
		||||
  export let arialabel: Translation = undefined
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -18,7 +18,11 @@
 | 
			
		|||
  on:click={(e) => dispatch("click", e)}
 | 
			
		||||
  on:keydown
 | 
			
		||||
  use:ariaLabel={arialabel}
 | 
			
		||||
  class={twJoin("pointer-events-auto relative h-fit w-fit rounded-full", cls, $enabled ? "" : "disabled")}
 | 
			
		||||
  class={twJoin(
 | 
			
		||||
    "pointer-events-auto relative h-fit w-fit rounded-full",
 | 
			
		||||
    cls,
 | 
			
		||||
    $enabled ? "" : "disabled"
 | 
			
		||||
  )}
 | 
			
		||||
>
 | 
			
		||||
  <slot />
 | 
			
		||||
</button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,13 +5,9 @@
 | 
			
		|||
    imgSize?: string
 | 
			
		||||
    extraClasses?: string
 | 
			
		||||
  } = {}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<button
 | 
			
		||||
  class={twMerge(options.extraClasses, "secondary no-image-background")}
 | 
			
		||||
  on:click
 | 
			
		||||
>
 | 
			
		||||
<button class={twMerge(options.extraClasses, "secondary no-image-background")} on:click>
 | 
			
		||||
  <slot name="image" />
 | 
			
		||||
  <slot name="message" />
 | 
			
		||||
</button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,7 @@ export default class SvelteUIElement<
 | 
			
		|||
    private readonly _props: Props
 | 
			
		||||
    private readonly _events: Events
 | 
			
		||||
    private readonly _slots: Slots
 | 
			
		||||
    private tag : "div" | "span" = "div"
 | 
			
		||||
    private tag: "div" | "span" = "div"
 | 
			
		||||
 | 
			
		||||
    constructor(svelteElement, props?: Props, events?: Events, slots?: Slots) {
 | 
			
		||||
        super()
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +32,7 @@ export default class SvelteUIElement<
 | 
			
		|||
        this._slots = slots
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public setSpan(){
 | 
			
		||||
    public setSpan() {
 | 
			
		||||
        this.tag = "span"
 | 
			
		||||
        return this
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,49 +1,48 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
    /**
 | 
			
		||||
     * THe panel containing all filter- and layerselection options
 | 
			
		||||
     */
 | 
			
		||||
  /**
 | 
			
		||||
   * THe panel containing all filter- and layerselection options
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
    import OverlayToggle from "./OverlayToggle.svelte"
 | 
			
		||||
    import Filterview from "./Filterview.svelte"
 | 
			
		||||
    import ThemeViewState from "../../Models/ThemeViewState"
 | 
			
		||||
    import Translations from "../i18n/Translations"
 | 
			
		||||
    import Tr from "../Base/Tr.svelte"
 | 
			
		||||
    import Filter from "../../assets/svg/Filter.svelte"
 | 
			
		||||
  import OverlayToggle from "./OverlayToggle.svelte"
 | 
			
		||||
  import Filterview from "./Filterview.svelte"
 | 
			
		||||
  import ThemeViewState from "../../Models/ThemeViewState"
 | 
			
		||||
  import Translations from "../i18n/Translations"
 | 
			
		||||
  import Tr from "../Base/Tr.svelte"
 | 
			
		||||
  import Filter from "../../assets/svg/Filter.svelte"
 | 
			
		||||
 | 
			
		||||
    export let state: ThemeViewState
 | 
			
		||||
    let layout = state.layout
 | 
			
		||||
    
 | 
			
		||||
    let allEnabled : boolean
 | 
			
		||||
    let allDisabled: boolean
 | 
			
		||||
    
 | 
			
		||||
    function updateEnableState(){
 | 
			
		||||
        allEnabled = true
 | 
			
		||||
        allDisabled = true
 | 
			
		||||
        state.layerState.filteredLayers.forEach((v) => {
 | 
			
		||||
            if(!v.layerDef.name){
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
            allEnabled &&= v.isDisplayed.data
 | 
			
		||||
            allDisabled &&= !v.isDisplayed.data
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    updateEnableState()
 | 
			
		||||
  export let state: ThemeViewState
 | 
			
		||||
  let layout = state.layout
 | 
			
		||||
 | 
			
		||||
  let allEnabled: boolean
 | 
			
		||||
  let allDisabled: boolean
 | 
			
		||||
 | 
			
		||||
  function updateEnableState() {
 | 
			
		||||
    allEnabled = true
 | 
			
		||||
    allDisabled = true
 | 
			
		||||
    state.layerState.filteredLayers.forEach((v) => {
 | 
			
		||||
        if(!v.layerDef.name){
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
       v.isDisplayed.addCallbackD(_ => updateEnableState())
 | 
			
		||||
      if (!v.layerDef.name) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      allEnabled &&= v.isDisplayed.data
 | 
			
		||||
      allDisabled &&= !v.isDisplayed.data
 | 
			
		||||
    })
 | 
			
		||||
    function enableAll(doEnable: boolean){
 | 
			
		||||
        state.layerState.filteredLayers.forEach((v) => {
 | 
			
		||||
            if(!v.layerDef.name){
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
            v.isDisplayed.setData(doEnable)
 | 
			
		||||
        })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateEnableState()
 | 
			
		||||
  state.layerState.filteredLayers.forEach((v) => {
 | 
			
		||||
    if (!v.layerDef.name) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    v.isDisplayed.addCallbackD((_) => updateEnableState())
 | 
			
		||||
  })
 | 
			
		||||
  function enableAll(doEnable: boolean) {
 | 
			
		||||
    state.layerState.filteredLayers.forEach((v) => {
 | 
			
		||||
      if (!v.layerDef.name) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      v.isDisplayed.setData(doEnable)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="m-2 flex flex-col">
 | 
			
		||||
| 
						 | 
				
			
			@ -59,12 +58,12 @@
 | 
			
		|||
      highlightedLayer={state.guistate.highlightedLayerInFilters}
 | 
			
		||||
    />
 | 
			
		||||
  {/each}
 | 
			
		||||
  <div class="flex self-end mt-1">
 | 
			
		||||
  <div class="mt-1 flex self-end">
 | 
			
		||||
    <button class="small" class:disabled={allEnabled} on:click={() => enableAll(true)}>
 | 
			
		||||
      <Tr t={Translations.t.general.filterPanel.enableAll}/>
 | 
			
		||||
      <Tr t={Translations.t.general.filterPanel.enableAll} />
 | 
			
		||||
    </button>
 | 
			
		||||
    <button class="small"  class:disabled={allDisabled} on:click={() => enableAll(false)}>
 | 
			
		||||
      <Tr t={Translations.t.general.filterPanel.disableAll}/>
 | 
			
		||||
    <button class="small" class:disabled={allDisabled} on:click={() => enableAll(false)}>
 | 
			
		||||
      <Tr t={Translations.t.general.filterPanel.disableAll} />
 | 
			
		||||
    </button>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,7 +66,7 @@
 | 
			
		|||
        />
 | 
			
		||||
      </If>
 | 
			
		||||
 | 
			
		||||
      <Tr t={filteredLayer.layerDef.name}/>
 | 
			
		||||
      <Tr t={filteredLayer.layerDef.name} />
 | 
			
		||||
 | 
			
		||||
      {#if $zoomlevel < layer.minzoom}
 | 
			
		||||
        <span class="alert">
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +82,7 @@
 | 
			
		|||
            <!-- There are three (and a half) modes of filters: a single checkbox, a radio button/dropdown or with searchable fields -->
 | 
			
		||||
            {#if filter.options.length === 1 && filter.options[0].fields.length === 0}
 | 
			
		||||
              <Checkbox selected={getBooleanStateFor(filter)}>
 | 
			
		||||
                <Tr t={filter.options[0].question}/>
 | 
			
		||||
                <Tr t={filter.options[0].question} />
 | 
			
		||||
              </Checkbox>
 | 
			
		||||
            {/if}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -94,7 +94,7 @@
 | 
			
		|||
              <Dropdown value={getStateFor(filter)}>
 | 
			
		||||
                {#each filter.options as option, i}
 | 
			
		||||
                  <option value={i}>
 | 
			
		||||
                   <Tr  t={option.question}/>
 | 
			
		||||
                    <Tr t={option.question} />
 | 
			
		||||
                  </option>
 | 
			
		||||
                {/each}
 | 
			
		||||
              </Dropdown>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,7 +41,7 @@
 | 
			
		|||
  let compass = Orientation.singleton.alpha
 | 
			
		||||
  let relativeBearing: Store<{ distance: string; bearing: Translation }> = compass.mapD(
 | 
			
		||||
    (compass) => {
 | 
			
		||||
      if(!distanceToCurrentLocation.data){
 | 
			
		||||
      if (!distanceToCurrentLocation.data) {
 | 
			
		||||
        return undefined
 | 
			
		||||
      }
 | 
			
		||||
      const bearing: Translation =
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@
 | 
			
		|||
  import ShowDataLayer from "../Map/ShowDataLayer"
 | 
			
		||||
  import type {
 | 
			
		||||
    FeatureSource,
 | 
			
		||||
    FeatureSourceForLayer
 | 
			
		||||
    FeatureSourceForLayer,
 | 
			
		||||
  } from "../../Logic/FeatureSource/FeatureSource"
 | 
			
		||||
  import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource"
 | 
			
		||||
  import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +72,7 @@
 | 
			
		|||
    allowMoving: new UIEventSource<boolean>(true),
 | 
			
		||||
    allowZooming: new UIEventSource<boolean>(true),
 | 
			
		||||
    minzoom: new UIEventSource<number>(18),
 | 
			
		||||
    rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer)
 | 
			
		||||
    rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
 | 
			
		||||
  }
 | 
			
		||||
  state?.showCurrentLocationOn(map)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -82,7 +82,7 @@
 | 
			
		|||
    if (featuresForLayer) {
 | 
			
		||||
      new ShowDataLayer(map, {
 | 
			
		||||
        layer: targetLayer,
 | 
			
		||||
        features: featuresForLayer
 | 
			
		||||
        features: featuresForLayer,
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -99,7 +99,7 @@
 | 
			
		|||
      new ShowDataLayer(map, {
 | 
			
		||||
        layer: layer.layer.layerDef,
 | 
			
		||||
        zoomToFeatures: false,
 | 
			
		||||
        features: layer
 | 
			
		||||
        features: layer,
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    const snappedLocation = new SnappingFeatureSource(
 | 
			
		||||
| 
						 | 
				
			
			@ -110,28 +110,30 @@
 | 
			
		|||
        maxDistance: maxSnapDistance ?? 15,
 | 
			
		||||
        allowUnsnapped: true,
 | 
			
		||||
        snappedTo,
 | 
			
		||||
        snapLocation: value
 | 
			
		||||
        snapLocation: value,
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
    const withCorrectedAttributes = new StaticFeatureSource(
 | 
			
		||||
      snappedLocation.features.mapD(feats => feats.map(f => {
 | 
			
		||||
        const properties =  {
 | 
			
		||||
        ...f.properties,
 | 
			
		||||
        ...presetPropertiesUnpacked
 | 
			
		||||
        }
 | 
			
		||||
        properties["_referencing_ways"] = f.properties["snapped-to"]
 | 
			
		||||
        return ({
 | 
			
		||||
          ...f,
 | 
			
		||||
         properties
 | 
			
		||||
      snappedLocation.features.mapD((feats) =>
 | 
			
		||||
        feats.map((f) => {
 | 
			
		||||
          const properties = {
 | 
			
		||||
            ...f.properties,
 | 
			
		||||
            ...presetPropertiesUnpacked,
 | 
			
		||||
          }
 | 
			
		||||
          properties["_referencing_ways"] = f.properties["snapped-to"]
 | 
			
		||||
          return {
 | 
			
		||||
            ...f,
 | 
			
		||||
            properties,
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }))
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
    // The actual point to be created, snapped at the new location
 | 
			
		||||
    new ShowDataLayer(map, {
 | 
			
		||||
      layer: targetLayer,
 | 
			
		||||
      features: withCorrectedAttributes
 | 
			
		||||
      features: withCorrectedAttributes,
 | 
			
		||||
    })
 | 
			
		||||
    withCorrectedAttributes.features.addCallbackAndRunD(f => console.log("Snapped point is", f))
 | 
			
		||||
    withCorrectedAttributes.features.addCallbackAndRunD((f) => console.log("Snapped point is", f))
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,8 +22,7 @@
 | 
			
		|||
  arialabel={Translations.t.general.labels.background}
 | 
			
		||||
  on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}
 | 
			
		||||
>
 | 
			
		||||
  
 | 
			
		||||
  <StyleLoadingIndicator map={map ?? state.map} rasterLayer={state.mapProperties.rasterLayer} >
 | 
			
		||||
  <StyleLoadingIndicator map={map ?? state.map} rasterLayer={state.mapProperties.rasterLayer}>
 | 
			
		||||
    <Square3Stack3dIcon class="h-6 w-6" />
 | 
			
		||||
  </StyleLoadingIndicator>
 | 
			
		||||
  {#if !hideTooltip}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,7 @@
 | 
			
		|||
  export let layer: LayerConfig
 | 
			
		||||
  export let selectedElement: Feature
 | 
			
		||||
  let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(
 | 
			
		||||
    selectedElement.properties.id,
 | 
			
		||||
    selectedElement.properties.id
 | 
			
		||||
  )
 | 
			
		||||
  $: {
 | 
			
		||||
    tags = state.featureProperties.getStore(selectedElement.properties.id)
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +43,7 @@
 | 
			
		|||
          class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 pt-0.5 sm:pt-1"
 | 
			
		||||
        >
 | 
			
		||||
          {#each layer.titleIcons as titleIconConfig}
 | 
			
		||||
            {#if (titleIconConfig.condition?.matchesProperties($tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...$metatags, ...$tags }) ?? true) && titleIconConfig.IsKnown($tags)}
 | 
			
		||||
            {#if (titleIconConfig.condition?.matchesProperties($tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ...$metatags, ...$tags } ) ?? true) && titleIconConfig.IsKnown($tags)}
 | 
			
		||||
              <div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
 | 
			
		||||
                <TagRenderingAnswer
 | 
			
		||||
                  config={titleIconConfig}
 | 
			
		||||
| 
						 | 
				
			
			@ -58,28 +58,33 @@
 | 
			
		|||
          {/each}
 | 
			
		||||
 | 
			
		||||
          {#if $isTesting || $isDebugging}
 | 
			
		||||
            <a class="subtle" href="https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Layers/{layer.id}.md"
 | 
			
		||||
               target="_blank" rel="noreferrer noopener ">{layer.id}</a>
 | 
			
		||||
            <a
 | 
			
		||||
              class="subtle"
 | 
			
		||||
              href="https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Layers/{layer.id}.md"
 | 
			
		||||
              target="_blank"
 | 
			
		||||
              rel="noreferrer noopener "
 | 
			
		||||
            >
 | 
			
		||||
              {layer.id}
 | 
			
		||||
            </a>
 | 
			
		||||
          {/if}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    {/if}
 | 
			
		||||
  </div>
 | 
			
		||||
  <slot name="close-button">
 | 
			
		||||
 | 
			
		||||
  <button
 | 
			
		||||
    class="mt-2 h-fit shrink-0 rounded-full border-none p-0"
 | 
			
		||||
    on:click={() => state.selectedElement.setData(undefined)}
 | 
			
		||||
    style="border: 0 !important; padding: 0 !important;"
 | 
			
		||||
    use:ariaLabel={Translations.t.general.backToMap}
 | 
			
		||||
  >
 | 
			
		||||
    <XCircleIcon aria-hidden={true} class="h-8 w-8" />
 | 
			
		||||
  </button>
 | 
			
		||||
    <button
 | 
			
		||||
      class="mt-2 h-fit shrink-0 rounded-full border-none p-0"
 | 
			
		||||
      on:click={() => state.selectedElement.setData(undefined)}
 | 
			
		||||
      style="border: 0 !important; padding: 0 !important;"
 | 
			
		||||
      use:ariaLabel={Translations.t.general.backToMap}
 | 
			
		||||
    >
 | 
			
		||||
      <XCircleIcon aria-hidden={true} class="h-8 w-8" />
 | 
			
		||||
    </button>
 | 
			
		||||
  </slot>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
    :global(.title-icons a) {
 | 
			
		||||
        display: block !important;
 | 
			
		||||
    }
 | 
			
		||||
  :global(.title-icons a) {
 | 
			
		||||
    display: block !important;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,19 +20,22 @@
 | 
			
		|||
    selectedElement.properties.id
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  let layer: LayerConfig = selectedElement.properties.id === "settings" ? UserRelatedState.usersettingsConfig : state.layout.getMatchingLayer(tags.data)
 | 
			
		||||
  let layer: LayerConfig =
 | 
			
		||||
    selectedElement.properties.id === "settings"
 | 
			
		||||
      ? UserRelatedState.usersettingsConfig
 | 
			
		||||
      : state.layout.getMatchingLayer(tags.data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  let stillMatches = tags.map(tags => !layer?.source?.osmTags || layer.source.osmTags?.matchesProperties(tags))
 | 
			
		||||
  let stillMatches = tags.map(
 | 
			
		||||
    (tags) => !layer?.source?.osmTags || layer.source.osmTags?.matchesProperties(tags)
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  let _metatags: Record<string, string>
 | 
			
		||||
  if(state?.userRelatedState?.preferencesAsTags){
 | 
			
		||||
    
 | 
			
		||||
  onDestroy(
 | 
			
		||||
    state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => {
 | 
			
		||||
      _metatags = tags
 | 
			
		||||
    }),
 | 
			
		||||
  )
 | 
			
		||||
  if (state?.userRelatedState?.preferencesAsTags) {
 | 
			
		||||
    onDestroy(
 | 
			
		||||
      state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => {
 | 
			
		||||
        _metatags = tags
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let knownTagRenderings: Store<TagRenderingConfig[]> = tags.mapD((tgs) =>
 | 
			
		||||
| 
						 | 
				
			
			@ -40,8 +43,8 @@
 | 
			
		|||
      (config) =>
 | 
			
		||||
        (config.condition?.matchesProperties(tgs) ?? true) &&
 | 
			
		||||
        (config.metacondition?.matchesProperties({ ...tgs, ..._metatags }) ?? true) &&
 | 
			
		||||
        config.IsKnown(tgs),
 | 
			
		||||
    ),
 | 
			
		||||
        config.IsKnown(tgs)
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -52,13 +55,12 @@
 | 
			
		|||
{:else if $tags._deleted === "yes"}
 | 
			
		||||
  <div class="flex w-full flex-col p-2">
 | 
			
		||||
    <div aria-live="assertive" class="alert flex items-center justify-center self-stretch">
 | 
			
		||||
      <Delete_icon class="w-8 h-8 m-2" />
 | 
			
		||||
      <Delete_icon class="m-2 h-8 w-8" />
 | 
			
		||||
      <Tr t={Translations.t.delete.isDeleted} />
 | 
			
		||||
    </div>
 | 
			
		||||
    <BackButton clss="self-stretch mt-4" on:click={() => state.selectedElement.setData(undefined)}>
 | 
			
		||||
      <Tr t={Translations.t.general.returnToTheMap} />
 | 
			
		||||
    </BackButton>
 | 
			
		||||
    
 | 
			
		||||
  </div>
 | 
			
		||||
{:else}
 | 
			
		||||
  <div
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,10 +71,8 @@
 | 
			
		|||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col">
 | 
			
		||||
  <div class="flex justify-between items-start">
 | 
			
		||||
 | 
			
		||||
  <div class="flex items-start justify-between">
 | 
			
		||||
    <div class="flex flex-col">
 | 
			
		||||
 | 
			
		||||
      <Tr t={tr.intro} />
 | 
			
		||||
      <div class="flex">
 | 
			
		||||
        {#if typeof navigator?.share === "function"}
 | 
			
		||||
| 
						 | 
				
			
			@ -92,10 +90,10 @@
 | 
			
		|||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
      
 | 
			
		||||
    <ToSvelte construct={() => new Img(new Qr(linkToShare).toImageElement(125)).SetStyle(
 | 
			
		||||
                                    "width: 125px"
 | 
			
		||||
                                )} />
 | 
			
		||||
 | 
			
		||||
    <ToSvelte
 | 
			
		||||
      construct={() => new Img(new Qr(linkToShare).toImageElement(125)).SetStyle("width: 125px")}
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="flex justify-center">
 | 
			
		||||
| 
						 | 
				
			
			@ -106,8 +104,7 @@
 | 
			
		|||
 | 
			
		||||
  <Tr t={tr.embedIntro} />
 | 
			
		||||
 | 
			
		||||
  <div class="flex flex-col interactive p-1">
 | 
			
		||||
 | 
			
		||||
  <div class="interactive flex flex-col p-1">
 | 
			
		||||
    <div class="literal-code m-1">
 | 
			
		||||
      <span class="literal-code iframe-code-block"> <br />
 | 
			
		||||
      <iframe src="{linkToShare}"
 | 
			
		||||
| 
						 | 
				
			
			@ -127,7 +124,7 @@
 | 
			
		|||
      </label>
 | 
			
		||||
 | 
			
		||||
      <label>
 | 
			
		||||
        <input bind:checked={enableLogin} type="checkbox" id="share_enable_login"/>
 | 
			
		||||
        <input bind:checked={enableLogin} type="checkbox" id="share_enable_login" />
 | 
			
		||||
        <Tr t={tr.fsUserbadge} />
 | 
			
		||||
      </label>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,10 +17,9 @@ export default class StatisticsForLayerPanel extends VariableUiElement {
 | 
			
		|||
                        return new Loading("Loading data")
 | 
			
		||||
                    }
 | 
			
		||||
                    if (features.length === 0) {
 | 
			
		||||
                        return new Combine([
 | 
			
		||||
                            "No elements in view for layer ",
 | 
			
		||||
                            layer.id
 | 
			
		||||
                        ]).SetClass("block")
 | 
			
		||||
                        return new Combine(["No elements in view for layer ", layer.id]).SetClass(
 | 
			
		||||
                            "block"
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    const els: BaseUIElement[] = []
 | 
			
		||||
                    const featuresForLayer = features
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,8 +8,8 @@ import { OsmFeature } from "../../Models/OsmFeature"
 | 
			
		|||
 | 
			
		||||
export interface TagRenderingChartOptions {
 | 
			
		||||
    groupToOtherCutoff?: 3 | number
 | 
			
		||||
    sort?: boolean,
 | 
			
		||||
    hideUnkown?: boolean,
 | 
			
		||||
    sort?: boolean
 | 
			
		||||
    hideUnkown?: boolean
 | 
			
		||||
    hideNotApplicable?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,8 +21,8 @@ export class StackedRenderingChart extends ChartJs {
 | 
			
		|||
            period: "day" | "month"
 | 
			
		||||
            groupToOtherCutoff?: 3 | number
 | 
			
		||||
            // If given, take the sum of these fields to get the feature weight
 | 
			
		||||
            sumFields?: string[],
 | 
			
		||||
            hideUnknown?: boolean,
 | 
			
		||||
            sumFields?: string[]
 | 
			
		||||
            hideUnknown?: boolean
 | 
			
		||||
            hideNotApplicable?: boolean
 | 
			
		||||
        }
 | 
			
		||||
    ) {
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +30,7 @@ export class StackedRenderingChart extends ChartJs {
 | 
			
		|||
            sort: true,
 | 
			
		||||
            groupToOtherCutoff: options?.groupToOtherCutoff,
 | 
			
		||||
            hideNotApplicable: options?.hideNotApplicable,
 | 
			
		||||
            hideUnkown: options?.hideUnknown
 | 
			
		||||
            hideUnkown: options?.hideUnknown,
 | 
			
		||||
        })
 | 
			
		||||
        if (labels === undefined || data === undefined) {
 | 
			
		||||
            console.error(
 | 
			
		||||
| 
						 | 
				
			
			@ -121,13 +121,13 @@ export class StackedRenderingChart extends ChartJs {
 | 
			
		|||
            datasets.push({
 | 
			
		||||
                data: countsPerDay,
 | 
			
		||||
                backgroundColor,
 | 
			
		||||
                label
 | 
			
		||||
                label,
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const perDayData = {
 | 
			
		||||
            labels: trimmedDays,
 | 
			
		||||
            datasets
 | 
			
		||||
            datasets,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const config = <ChartConfiguration>{
 | 
			
		||||
| 
						 | 
				
			
			@ -136,17 +136,17 @@ export class StackedRenderingChart extends ChartJs {
 | 
			
		|||
            options: {
 | 
			
		||||
                responsive: true,
 | 
			
		||||
                legend: {
 | 
			
		||||
                    display: false
 | 
			
		||||
                    display: false,
 | 
			
		||||
                },
 | 
			
		||||
                scales: {
 | 
			
		||||
                    x: {
 | 
			
		||||
                        stacked: true
 | 
			
		||||
                        stacked: true,
 | 
			
		||||
                    },
 | 
			
		||||
                    y: {
 | 
			
		||||
                        stacked: true
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
                        stacked: true,
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        super(config)
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -199,7 +199,7 @@ export default class TagRenderingChart extends Combine {
 | 
			
		|||
        "rgba(255, 206, 86, 0.2)",
 | 
			
		||||
        "rgba(75, 192, 192, 0.2)",
 | 
			
		||||
        "rgba(153, 102, 255, 0.2)",
 | 
			
		||||
        "rgba(255, 159, 64, 0.2)"
 | 
			
		||||
        "rgba(255, 159, 64, 0.2)",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    public static readonly borderColors = [
 | 
			
		||||
| 
						 | 
				
			
			@ -208,7 +208,7 @@ export default class TagRenderingChart extends Combine {
 | 
			
		|||
        "rgba(255, 206, 86, 1)",
 | 
			
		||||
        "rgba(75, 192, 192, 1)",
 | 
			
		||||
        "rgba(153, 102, 255, 1)",
 | 
			
		||||
        "rgba(255, 159, 64, 1)"
 | 
			
		||||
        "rgba(255, 159, 64, 1)",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -244,12 +244,12 @@ export default class TagRenderingChart extends Combine {
 | 
			
		|||
        const borderColor = [
 | 
			
		||||
            TagRenderingChart.unkownBorderColor,
 | 
			
		||||
            TagRenderingChart.otherBorderColor,
 | 
			
		||||
            TagRenderingChart.notApplicableBorderColor
 | 
			
		||||
            TagRenderingChart.notApplicableBorderColor,
 | 
			
		||||
        ]
 | 
			
		||||
        const backgroundColor = [
 | 
			
		||||
            TagRenderingChart.unkownColor,
 | 
			
		||||
            TagRenderingChart.otherColor,
 | 
			
		||||
            TagRenderingChart.notApplicableColor
 | 
			
		||||
            TagRenderingChart.notApplicableColor,
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        while (borderColor.length < data.length) {
 | 
			
		||||
| 
						 | 
				
			
			@ -281,17 +281,17 @@ export default class TagRenderingChart extends Combine {
 | 
			
		|||
                        backgroundColor,
 | 
			
		||||
                        borderColor,
 | 
			
		||||
                        borderWidth: 1,
 | 
			
		||||
                        label: undefined
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
                        label: undefined,
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
            options: {
 | 
			
		||||
                plugins: {
 | 
			
		||||
                    legend: {
 | 
			
		||||
                        display: !barchartMode
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
                        display: !barchartMode,
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const chart = new ChartJs(config).SetClass(options?.chartclasses ?? "w-32 h-32")
 | 
			
		||||
| 
						 | 
				
			
			@ -302,7 +302,7 @@ export default class TagRenderingChart extends Combine {
 | 
			
		|||
 | 
			
		||||
        super([
 | 
			
		||||
            options?.includeTitle ? tagRendering.question.Clone() ?? tagRendering.id : undefined,
 | 
			
		||||
            chart
 | 
			
		||||
            chart,
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
        this.SetClass("block")
 | 
			
		||||
| 
						 | 
				
			
			@ -402,15 +402,10 @@ export default class TagRenderingChart extends Combine {
 | 
			
		|||
        labels.push("Other")
 | 
			
		||||
        if (!options.hideNotApplicable) {
 | 
			
		||||
            data.push(notApplicable)
 | 
			
		||||
            labels.push(
 | 
			
		||||
                "Not applicable"
 | 
			
		||||
            )
 | 
			
		||||
            labels.push("Not applicable")
 | 
			
		||||
        }
 | 
			
		||||
        data.push(...categoryCounts,
 | 
			
		||||
            ...otherData)
 | 
			
		||||
        labels.push(
 | 
			
		||||
            ...(mappings?.map((m) => m.then.txt) ?? []),
 | 
			
		||||
            ...otherLabels)
 | 
			
		||||
        data.push(...categoryCounts, ...otherData)
 | 
			
		||||
        labels.push(...(mappings?.map((m) => m.then.txt) ?? []), ...otherLabels)
 | 
			
		||||
 | 
			
		||||
        return { labels, data }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -85,10 +85,8 @@
 | 
			
		|||
</script>
 | 
			
		||||
 | 
			
		||||
{#if theme.id !== personal.id || $unlockedPersonal}
 | 
			
		||||
  <a
 | 
			
		||||
  class={"w-full button text-ellipsis"}
 | 
			
		||||
  href={$href}
 | 
			
		||||
  > <img src={theme.icon} class="m-1 mr-2 block h-11 w-11 sm:m-2 sm:mr-4" alt="" />
 | 
			
		||||
  <a class={"button w-full text-ellipsis"} href={$href}>
 | 
			
		||||
    <img src={theme.icon} class="m-1 mr-2 block h-11 w-11 sm:m-2 sm:mr-4" alt="" />
 | 
			
		||||
    <span class="flex flex-col overflow-hidden text-ellipsis">
 | 
			
		||||
      <Tr t={title} />
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -97,5 +95,6 @@
 | 
			
		|||
          <Tr t={Translations.t.general.morescreen.enterToOpen} />
 | 
			
		||||
        </span>
 | 
			
		||||
      {/if}
 | 
			
		||||
    </span></a>
 | 
			
		||||
    </span>
 | 
			
		||||
  </a>
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,13 +29,13 @@
 | 
			
		|||
  const splitpoint_style = new LayerConfig(
 | 
			
		||||
    <LayerConfigJson>split_point,
 | 
			
		||||
    "(BUILTIN) SplitRoadWizard.ts",
 | 
			
		||||
    true,
 | 
			
		||||
    true
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const splitroad_style = new LayerConfig(
 | 
			
		||||
    <LayerConfigJson>split_road,
 | 
			
		||||
    "(BUILTIN) SplitRoadWizard.ts",
 | 
			
		||||
    true,
 | 
			
		||||
    true
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
| 
						 | 
				
			
			@ -105,7 +105,7 @@
 | 
			
		|||
  })
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="h-full w-full relative">
 | 
			
		||||
<div class="relative h-full w-full">
 | 
			
		||||
  <MaplibreMap {map} mapProperties={adaptor} />
 | 
			
		||||
  <SmallZoomButtons {adaptor} />
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,7 +38,7 @@
 | 
			
		|||
      tags.data,
 | 
			
		||||
      {
 | 
			
		||||
        theme: state.layout.id,
 | 
			
		||||
        changeType: "import"
 | 
			
		||||
        changeType: "import",
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
    await state.changes.applyChanges(await change.CreateChangeDescriptions())
 | 
			
		||||
| 
						 | 
				
			
			@ -47,43 +47,51 @@
 | 
			
		|||
 | 
			
		||||
  let _country = $tags["_country"]
 | 
			
		||||
  let mockPropertiesOsm = { id: feature.properties.id, [key]: $tags[key], _country }
 | 
			
		||||
  let mockPropertiesExternal = { id: feature.properties.id, [key]: externalProperties[key], _country }
 | 
			
		||||
  let trsWithKeys = layer.tagRenderings.filter(tr => {
 | 
			
		||||
    const keys: string[] = [].concat(...tr.usedTags().map(t => t.usedKeys()))
 | 
			
		||||
  let mockPropertiesExternal = {
 | 
			
		||||
    id: feature.properties.id,
 | 
			
		||||
    [key]: externalProperties[key],
 | 
			
		||||
    _country,
 | 
			
		||||
  }
 | 
			
		||||
  let trsWithKeys = layer.tagRenderings.filter((tr) => {
 | 
			
		||||
    const keys: string[] = [].concat(...tr.usedTags().map((t) => t.usedKeys()))
 | 
			
		||||
    return keys.indexOf(key) >= 0
 | 
			
		||||
  })
 | 
			
		||||
  let renderingBoth = trsWithKeys.find(tr => tr.IsKnown(mockPropertiesOsm) && tr.IsKnown(mockPropertiesExternal))
 | 
			
		||||
  let renderingExternal = renderingBoth ?? trsWithKeys.find(tr => tr.IsKnown(mockPropertiesExternal))
 | 
			
		||||
  let renderingBoth = trsWithKeys.find(
 | 
			
		||||
    (tr) => tr.IsKnown(mockPropertiesOsm) && tr.IsKnown(mockPropertiesExternal)
 | 
			
		||||
  )
 | 
			
		||||
  let renderingExternal =
 | 
			
		||||
    renderingBoth ?? trsWithKeys.find((tr) => tr.IsKnown(mockPropertiesExternal))
 | 
			
		||||
  let onOverwrite = false
 | 
			
		||||
  const t = Translations.t.external
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div>
 | 
			
		||||
 | 
			
		||||
  <div class="py-1 px-2 interactive flex w-full justify-between">
 | 
			
		||||
  <div class="interactive flex w-full justify-between py-1 px-2">
 | 
			
		||||
    <div class="flex flex-col">
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
 | 
			
		||||
        {#if renderingExternal}
 | 
			
		||||
          <TagRenderingAnswer tags={new UIEventSource(mockPropertiesExternal)} selectedElement={feature}
 | 
			
		||||
                              config={renderingExternal}
 | 
			
		||||
                              {layer} {state} />
 | 
			
		||||
          <TagRenderingAnswer
 | 
			
		||||
            tags={new UIEventSource(mockPropertiesExternal)}
 | 
			
		||||
            selectedElement={feature}
 | 
			
		||||
            config={renderingExternal}
 | 
			
		||||
            {layer}
 | 
			
		||||
            {state}
 | 
			
		||||
          />
 | 
			
		||||
        {:else}
 | 
			
		||||
          <div class="flex gap-x-1 items-center">
 | 
			
		||||
            <b>{key}</b>{externalProperties[key]}
 | 
			
		||||
          <div class="flex items-center gap-x-1">
 | 
			
		||||
            <b>{key}</b>
 | 
			
		||||
            {externalProperties[key]}
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
        {/if}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {#if !readonly && ( $isTesting || $isDebug || $showTags === "yes" || $showTags === "always" || $showTags === "full")}
 | 
			
		||||
      {#if !readonly && ($isTesting || $isDebug || $showTags === "yes" || $showTags === "always" || $showTags === "full")}
 | 
			
		||||
        <div class="subtle text-sm">
 | 
			
		||||
          {#if $tags[key] !== undefined}
 | 
			
		||||
          <span>
 | 
			
		||||
        OSM:
 | 
			
		||||
            {key}={$tags[key]}
 | 
			
		||||
          </span>
 | 
			
		||||
            <span>
 | 
			
		||||
              OSM:
 | 
			
		||||
              {key}={$tags[key]}
 | 
			
		||||
            </span>
 | 
			
		||||
          {/if}
 | 
			
		||||
          <span>
 | 
			
		||||
            {key}= {externalProperties[key]}
 | 
			
		||||
| 
						 | 
				
			
			@ -92,21 +100,20 @@
 | 
			
		|||
      {/if}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    {#if !readonly}
 | 
			
		||||
      {#if currentStep === "init"}
 | 
			
		||||
        <button class="small" on:click={() => apply(key)}
 | 
			
		||||
 | 
			
		||||
                on:mouseover={() => onOverwrite = true}
 | 
			
		||||
                on:focus={() => onOverwrite = true}
 | 
			
		||||
                on:blur={() => onOverwrite = false}
 | 
			
		||||
                on:mouseout={() => onOverwrite = false  }
 | 
			
		||||
        <button
 | 
			
		||||
          class="small"
 | 
			
		||||
          on:click={() => apply(key)}
 | 
			
		||||
          on:mouseover={() => (onOverwrite = true)}
 | 
			
		||||
          on:focus={() => (onOverwrite = true)}
 | 
			
		||||
          on:blur={() => (onOverwrite = false)}
 | 
			
		||||
          on:mouseout={() => (onOverwrite = false)}
 | 
			
		||||
        >
 | 
			
		||||
          {#if $tags[key]}
 | 
			
		||||
            <Tr t={t.overwrite} />
 | 
			
		||||
          {:else}
 | 
			
		||||
            <Tr t={t.apply} />
 | 
			
		||||
 | 
			
		||||
          {/if}
 | 
			
		||||
        </button>
 | 
			
		||||
      {:else if currentStep === "applying"}
 | 
			
		||||
| 
						 | 
				
			
			@ -114,7 +121,6 @@
 | 
			
		|||
      {:else if currentStep === "done"}
 | 
			
		||||
        <div class="thanks">
 | 
			
		||||
          <Tr t={t.done} />
 | 
			
		||||
 | 
			
		||||
        </div>
 | 
			
		||||
      {:else}
 | 
			
		||||
        <div class="alert">
 | 
			
		||||
| 
						 | 
				
			
			@ -122,22 +128,26 @@
 | 
			
		|||
        </div>
 | 
			
		||||
      {/if}
 | 
			
		||||
    {/if}
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
  {#if $tags[key] && $tags[key] !== externalProperties[key]}
 | 
			
		||||
    <div class:glowing-shadow={onOverwrite}>
 | 
			
		||||
    <span class="subtle">
 | 
			
		||||
      <Tr t={t.currentInOsmIs} />
 | 
			
		||||
    </span>
 | 
			
		||||
      <span class="subtle">
 | 
			
		||||
        <Tr t={t.currentInOsmIs} />
 | 
			
		||||
      </span>
 | 
			
		||||
      {#if renderingBoth}
 | 
			
		||||
        <TagRenderingAnswer tags={new UIEventSource(mockPropertiesOsm)} selectedElement={feature} config={renderingBoth}
 | 
			
		||||
                            {layer} {state} />
 | 
			
		||||
        <TagRenderingAnswer
 | 
			
		||||
          tags={new UIEventSource(mockPropertiesOsm)}
 | 
			
		||||
          selectedElement={feature}
 | 
			
		||||
          config={renderingBoth}
 | 
			
		||||
          {layer}
 | 
			
		||||
          {state}
 | 
			
		||||
        />
 | 
			
		||||
      {:else}
 | 
			
		||||
        <div class="flex gap-x-2 items-center">
 | 
			
		||||
          <b>{key}</b> {$tags[key]}
 | 
			
		||||
        <div class="flex items-center gap-x-2">
 | 
			
		||||
          <b>{key}</b>
 | 
			
		||||
          {$tags[key]}
 | 
			
		||||
        </div>
 | 
			
		||||
      {/if}
 | 
			
		||||
    </div>
 | 
			
		||||
  {/if}
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,32 +49,30 @@
 | 
			
		|||
    return osmProperties[k] === undefined && typeof externalProperties[k] === "string"
 | 
			
		||||
  })
 | 
			
		||||
  // let same = propertyKeysExternal.filter((key) => osmProperties[key] === externalProperties[key])
 | 
			
		||||
  let different = propertyKeysExternal.filter(
 | 
			
		||||
    (key) => {
 | 
			
		||||
      if (key.startsWith("_")) {
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
      if (osmProperties[key] === undefined) {
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
      if (typeof externalProperties[key] !== "string") {
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
      if (osmProperties[key] === externalProperties[key]) {
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (key === "website") {
 | 
			
		||||
        const osmCanon = new URL(osmProperties[key]).toString()
 | 
			
		||||
        const externalCanon = new URL(externalProperties[key]).toString()
 | 
			
		||||
        if (osmCanon === externalCanon) {
 | 
			
		||||
          return false
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return true
 | 
			
		||||
  let different = propertyKeysExternal.filter((key) => {
 | 
			
		||||
    if (key.startsWith("_")) {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
    if (osmProperties[key] === undefined) {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
    if (typeof externalProperties[key] !== "string") {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
    if (osmProperties[key] === externalProperties[key]) {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (key === "website") {
 | 
			
		||||
      const osmCanon = new URL(osmProperties[key]).toString()
 | 
			
		||||
      const externalCanon = new URL(externalProperties[key]).toString()
 | 
			
		||||
      if (osmCanon === externalCanon) {
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return true
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  let currentStep: "init" | "applying_all" | "all_applied" = "init"
 | 
			
		||||
  let applyAllHovered = false
 | 
			
		||||
| 
						 | 
				
			
			@ -84,12 +82,13 @@
 | 
			
		|||
    const tagsToApply = missing.map((k) => new Tag(k, externalProperties[k]))
 | 
			
		||||
    const change = new ChangeTagAction(tags.data.id, new And(tagsToApply), tags.data, {
 | 
			
		||||
      theme: state.layout.id,
 | 
			
		||||
      changeType: "import"
 | 
			
		||||
      changeType: "import",
 | 
			
		||||
    })
 | 
			
		||||
    await state.changes.applyChanges(await change.CreateChangeDescriptions())
 | 
			
		||||
    currentStep = "all_applied"
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if propertyKeysExternal.length === 0 && knownImages.size + unknownImages.length === 0}
 | 
			
		||||
  <Tr cls="subtle" t={t.noDataLoaded} />
 | 
			
		||||
{:else if unknownImages.length === 0 && missing.length === 0 && different.length === 0}
 | 
			
		||||
| 
						 | 
				
			
			@ -98,9 +97,9 @@
 | 
			
		|||
    <Tr t={t.allIncluded} />
 | 
			
		||||
  </div>
 | 
			
		||||
{:else}
 | 
			
		||||
  <div class="low-interaction p-1 border-interactive">
 | 
			
		||||
  <div class="low-interaction border-interactive p-1">
 | 
			
		||||
    {#if !readonly}
 | 
			
		||||
      <Tr t={t.loadedFrom.Subs({url: sourceUrl, source: sourceUrl})} />
 | 
			
		||||
      <Tr t={t.loadedFrom.Subs({ url: sourceUrl, source: sourceUrl })} />
 | 
			
		||||
      <h3>
 | 
			
		||||
        <Tr t={t.conflicting.title} />
 | 
			
		||||
      </h3>
 | 
			
		||||
| 
						 | 
				
			
			@ -113,25 +112,41 @@
 | 
			
		|||
      {#if different.length > 0}
 | 
			
		||||
        {#each different as key}
 | 
			
		||||
          <div class="mx-2 rounded-2xl">
 | 
			
		||||
            <ComparisonAction {key} {state} {tags} {externalProperties} {layer} {feature} {readonly} />
 | 
			
		||||
            <ComparisonAction
 | 
			
		||||
              {key}
 | 
			
		||||
              {state}
 | 
			
		||||
              {tags}
 | 
			
		||||
              {externalProperties}
 | 
			
		||||
              {layer}
 | 
			
		||||
              {feature}
 | 
			
		||||
              {readonly}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        {/each}
 | 
			
		||||
      {/if}
 | 
			
		||||
 | 
			
		||||
      {#if missing.length > 0}
 | 
			
		||||
        {#if currentStep === "init"}
 | 
			
		||||
 | 
			
		||||
          {#each missing as key}
 | 
			
		||||
            <div class:glowing-shadow={applyAllHovered} class="mx-2 rounded-2xl">
 | 
			
		||||
              <ComparisonAction {key} {state} {tags} {externalProperties} {layer} {feature} {readonly} />
 | 
			
		||||
              <ComparisonAction
 | 
			
		||||
                {key}
 | 
			
		||||
                {state}
 | 
			
		||||
                {tags}
 | 
			
		||||
                {externalProperties}
 | 
			
		||||
                {layer}
 | 
			
		||||
                {feature}
 | 
			
		||||
                {readonly}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          {/each}
 | 
			
		||||
          {#if !readonly && missing.length > 1}
 | 
			
		||||
            <button on:click={() => applyAllMissing()}
 | 
			
		||||
                    on:mouseover={() => applyAllHovered = true}
 | 
			
		||||
                    on:focus={() => applyAllHovered = true}
 | 
			
		||||
                    on:blur={() => applyAllHovered = false}
 | 
			
		||||
                    on:mouseout={() => applyAllHovered = false  }
 | 
			
		||||
            <button
 | 
			
		||||
              on:click={() => applyAllMissing()}
 | 
			
		||||
              on:mouseover={() => (applyAllHovered = true)}
 | 
			
		||||
              on:focus={() => (applyAllHovered = true)}
 | 
			
		||||
              on:blur={() => (applyAllHovered = false)}
 | 
			
		||||
              on:mouseout={() => (applyAllHovered = false)}
 | 
			
		||||
            >
 | 
			
		||||
              <Tr t={t.applyAll} />
 | 
			
		||||
            </button>
 | 
			
		||||
| 
						 | 
				
			
			@ -144,7 +159,6 @@
 | 
			
		|||
          </div>
 | 
			
		||||
        {/if}
 | 
			
		||||
      {/if}
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    {#if unknownImages.length > 0}
 | 
			
		||||
| 
						 | 
				
			
			@ -166,13 +180,13 @@
 | 
			
		|||
            {tags}
 | 
			
		||||
            {state}
 | 
			
		||||
            image={{
 | 
			
		||||
          pictureUrl: image,
 | 
			
		||||
          provider: "Velopark",
 | 
			
		||||
          thumbUrl: image,
 | 
			
		||||
          details: undefined,
 | 
			
		||||
          coordinates: undefined,
 | 
			
		||||
          osmTags: { image },
 | 
			
		||||
        }}
 | 
			
		||||
              pictureUrl: image,
 | 
			
		||||
              provider: "Velopark",
 | 
			
		||||
              thumbUrl: image,
 | 
			
		||||
              details: undefined,
 | 
			
		||||
              coordinates: undefined,
 | 
			
		||||
              osmTags: { image },
 | 
			
		||||
            }}
 | 
			
		||||
            {feature}
 | 
			
		||||
            {layer}
 | 
			
		||||
          />
 | 
			
		||||
| 
						 | 
				
			
			@ -181,10 +195,8 @@
 | 
			
		|||
    {/if}
 | 
			
		||||
    {#if externalProperties["_last_edit_timestamp"] !== undefined}
 | 
			
		||||
      <span class="subtle text-sm">
 | 
			
		||||
 | 
			
		||||
      External data has been last modified on {externalProperties["_last_edit_timestamp"]}
 | 
			
		||||
        External data has been last modified on {externalProperties["_last_edit_timestamp"]}
 | 
			
		||||
      </span>
 | 
			
		||||
    {/if}
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,34 +1,39 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
    /**
 | 
			
		||||
     * The comparison tool loads json-data from a speficied URL, eventually post-processes it
 | 
			
		||||
     * and compares it with the current object
 | 
			
		||||
     */
 | 
			
		||||
    import Loading from "../Base/Loading.svelte"
 | 
			
		||||
    import type { SpecialVisualizationState } from "../SpecialVisualization"
 | 
			
		||||
    import { Store, UIEventSource } from "../../Logic/UIEventSource"
 | 
			
		||||
    import ComparisonTable from "./ComparisonTable.svelte"
 | 
			
		||||
    import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
 | 
			
		||||
    import type { Feature } from "geojson"
 | 
			
		||||
    import type { OsmTags } from "../../Models/OsmFeature"
 | 
			
		||||
    import Translations from "../i18n/Translations"
 | 
			
		||||
    import Tr from "../Base/Tr.svelte"
 | 
			
		||||
  /**
 | 
			
		||||
   * The comparison tool loads json-data from a speficied URL, eventually post-processes it
 | 
			
		||||
   * and compares it with the current object
 | 
			
		||||
   */
 | 
			
		||||
  import Loading from "../Base/Loading.svelte"
 | 
			
		||||
  import type { SpecialVisualizationState } from "../SpecialVisualization"
 | 
			
		||||
  import { Store, UIEventSource } from "../../Logic/UIEventSource"
 | 
			
		||||
  import ComparisonTable from "./ComparisonTable.svelte"
 | 
			
		||||
  import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
 | 
			
		||||
  import type { Feature } from "geojson"
 | 
			
		||||
  import type { OsmTags } from "../../Models/OsmFeature"
 | 
			
		||||
  import Translations from "../i18n/Translations"
 | 
			
		||||
  import Tr from "../Base/Tr.svelte"
 | 
			
		||||
 | 
			
		||||
    export let externalData: Store<{ success: {content: Record<string, string> } } | { error: string } | undefined | null /* null if no URL is found, undefined if loading*/>
 | 
			
		||||
    export let state: SpecialVisualizationState
 | 
			
		||||
    export let tags: UIEventSource<OsmTags>
 | 
			
		||||
    export let layer: LayerConfig
 | 
			
		||||
    export let feature: Feature
 | 
			
		||||
    export let readonly = false
 | 
			
		||||
    export let sourceUrl: Store<string>
 | 
			
		||||
    
 | 
			
		||||
  export let externalData: Store<
 | 
			
		||||
    | { success: { content: Record<string, string> } }
 | 
			
		||||
    | { error: string }
 | 
			
		||||
    | undefined
 | 
			
		||||
    | null /* null if no URL is found, undefined if loading*/
 | 
			
		||||
  >
 | 
			
		||||
  export let state: SpecialVisualizationState
 | 
			
		||||
  export let tags: UIEventSource<OsmTags>
 | 
			
		||||
  export let layer: LayerConfig
 | 
			
		||||
  export let feature: Feature
 | 
			
		||||
  export let readonly = false
 | 
			
		||||
  export let sourceUrl: Store<string>
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if !$sourceUrl}
 | 
			
		||||
  <!-- empty block -->
 | 
			
		||||
{:else if $externalData === undefined}
 | 
			
		||||
  <Loading/>
 | 
			
		||||
  <Loading />
 | 
			
		||||
{:else if $externalData["error"] !== undefined}
 | 
			
		||||
  <div class="alert flex">
 | 
			
		||||
    <Tr t={Translations.t.general.error}/>
 | 
			
		||||
    <Tr t={Translations.t.general.error} />
 | 
			
		||||
    {$externalData["error"]}
 | 
			
		||||
  </div>
 | 
			
		||||
{:else if $externalData["success"] !== undefined}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,10 +13,7 @@
 | 
			
		|||
 | 
			
		||||
  export let extension: string
 | 
			
		||||
  export let mimetype: string
 | 
			
		||||
  export let construct: (
 | 
			
		||||
    title: string,
 | 
			
		||||
    status?: UIEventSource<string>
 | 
			
		||||
  ) => Promise<Blob | string>
 | 
			
		||||
  export let construct: (title: string, status?: UIEventSource<string>) => Promise<Blob | string>
 | 
			
		||||
  export let mainText: Translation
 | 
			
		||||
  export let helperText: Translation
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +25,7 @@
 | 
			
		|||
  let status: UIEventSource<string> = new UIEventSource<string>(undefined)
 | 
			
		||||
  async function clicked() {
 | 
			
		||||
    isExporting = true
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    const gpsLayer = state.layerState.filteredLayers.get(<PriviligedLayerType>"gps_location")
 | 
			
		||||
    state.userRelatedState.preferencesAsTags.data["__showTimeSensitiveIcons"] = "no"
 | 
			
		||||
    state.userRelatedState.preferencesAsTags.ping()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,7 +38,7 @@
 | 
			
		|||
      mapExtent: state.mapProperties.bounds.data,
 | 
			
		||||
      width: maindiv.offsetWidth,
 | 
			
		||||
      height: maindiv.offsetHeight,
 | 
			
		||||
      noSelfIntersectingLines
 | 
			
		||||
      noSelfIntersectingLines,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -46,13 +46,17 @@
 | 
			
		|||
  let customHeight = LocalStorageSource.Get("custom-png-height", "20")
 | 
			
		||||
 | 
			
		||||
  async function offerCustomPng(): Promise<Blob> {
 | 
			
		||||
    console.log("Creating a custom size png with dimensions", customWidth.data + "mm *", customHeight.data + "mm")
 | 
			
		||||
    console.log(
 | 
			
		||||
      "Creating a custom size png with dimensions",
 | 
			
		||||
      customWidth.data + "mm *",
 | 
			
		||||
      customHeight.data + "mm"
 | 
			
		||||
    )
 | 
			
		||||
    const creator = new PngMapCreator(state, {
 | 
			
		||||
      height: Number(customHeight.data), width: Number(customWidth.data)
 | 
			
		||||
      height: Number(customHeight.data),
 | 
			
		||||
      width: Number(customWidth.data),
 | 
			
		||||
    })
 | 
			
		||||
    return await creator.CreatePng("belowmap")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if $isLoading}
 | 
			
		||||
| 
						 | 
				
			
			@ -123,25 +127,26 @@
 | 
			
		|||
    {/each}
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="low-interaction p-2 mt-4">
 | 
			
		||||
  <div class="low-interaction mt-4 p-2">
 | 
			
		||||
    <h3 class="m-0 mb-2">
 | 
			
		||||
      <Tr t={t.custom.title}/></h3>
 | 
			
		||||
        <div class="flex">
 | 
			
		||||
          <Tr t={t.custom.width} />
 | 
			
		||||
          <ValidatedInput {state} type="pnat" value={customWidth} />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="flex">
 | 
			
		||||
          <Tr t={t.custom.height} />
 | 
			
		||||
          <ValidatedInput {state} type="pnat" value={customHeight} />
 | 
			
		||||
        </div>
 | 
			
		||||
        <DownloadButton
 | 
			
		||||
          mainText={t.custom.download.Subs({width: $customWidth, height: $customHeight})}
 | 
			
		||||
          helperText={t.custom.downloadHelper}
 | 
			
		||||
          extension="png"
 | 
			
		||||
          construct={() => offerCustomPng()}
 | 
			
		||||
          {state}
 | 
			
		||||
          mimetype="image/png"
 | 
			
		||||
        />
 | 
			
		||||
      <Tr t={t.custom.title} />
 | 
			
		||||
    </h3>
 | 
			
		||||
    <div class="flex">
 | 
			
		||||
      <Tr t={t.custom.width} />
 | 
			
		||||
      <ValidatedInput {state} type="pnat" value={customWidth} />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="flex">
 | 
			
		||||
      <Tr t={t.custom.height} />
 | 
			
		||||
      <ValidatedInput {state} type="pnat" value={customHeight} />
 | 
			
		||||
    </div>
 | 
			
		||||
    <DownloadButton
 | 
			
		||||
      mainText={t.custom.download.Subs({ width: $customWidth, height: $customHeight })}
 | 
			
		||||
      helperText={t.custom.downloadHelper}
 | 
			
		||||
      extension="png"
 | 
			
		||||
      construct={() => offerCustomPng()}
 | 
			
		||||
      {state}
 | 
			
		||||
      mimetype="image/png"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <Tr cls="link-underline" t={t.licenseInfo} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,7 +38,7 @@
 | 
			
		|||
        </div>
 | 
			
		||||
      {/if}
 | 
			
		||||
 | 
			
		||||
      <div class="flex justify-between w-full gap-x-1">
 | 
			
		||||
      <div class="flex w-full justify-between gap-x-1">
 | 
			
		||||
        {#if $license.license !== undefined || $license.licenseShortName !== undefined}
 | 
			
		||||
          <div>
 | 
			
		||||
            {$license?.license ?? $license?.licenseShortName}
 | 
			
		||||
| 
						 | 
				
			
			@ -58,8 +58,6 @@
 | 
			
		|||
          {$license.date.toLocaleDateString()}
 | 
			
		||||
        </div>
 | 
			
		||||
      {/if}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,13 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
 | 
			
		||||
  import Tr from "../Base/Tr.svelte"
 | 
			
		||||
  import Translations from "../i18n/Translations"
 | 
			
		||||
  import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
 | 
			
		||||
 | 
			
		||||
  export let failed: number
 | 
			
		||||
  const t = Translations.t.image
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
<div class="alert flex">
 | 
			
		||||
 | 
			
		||||
<div class="alert flex">
 | 
			
		||||
  <div class="flex flex-col items-start">
 | 
			
		||||
    {#if failed === 1}
 | 
			
		||||
      <Tr t={t.upload.one.failed} />
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +18,7 @@
 | 
			
		|||
    <Tr cls="text-xs" t={t.upload.failReasonsAdvanced} />
 | 
			
		||||
  </div>
 | 
			
		||||
  <button
 | 
			
		||||
    class="mt-2 h-fit shrink-0 rounded-full border-none p-0 pointer-events-auto"
 | 
			
		||||
    class="pointer-events-auto mt-2 h-fit shrink-0 rounded-full border-none p-0"
 | 
			
		||||
    on:click
 | 
			
		||||
    style="border: 0 !important; padding: 0 !important;"
 | 
			
		||||
  >
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,9 @@
 | 
			
		|||
</script>
 | 
			
		||||
 | 
			
		||||
{#if $debugging}
 | 
			
		||||
  <div class="low-interaction">Started {$uploadStarted} Done {$uploadFinished} Retry {$retried} Err {$failed}</div>
 | 
			
		||||
  <div class="low-interaction">
 | 
			
		||||
    Started {$uploadStarted} Done {$uploadFinished} Retry {$retried} Err {$failed}
 | 
			
		||||
  </div>
 | 
			
		||||
{/if}
 | 
			
		||||
{#if dismissed === $uploadStarted}
 | 
			
		||||
  <!-- We don't show anything as we ignore this number of failed items-->
 | 
			
		||||
| 
						 | 
				
			
			@ -39,18 +41,18 @@
 | 
			
		|||
      <Tr cls="thanks" t={t.upload.one.done} />
 | 
			
		||||
    {/if}
 | 
			
		||||
  {:else if $failed === 1}
 | 
			
		||||
    <UploadFailedMessage failed={$failed} on:click={() => dismissed = $failed}/>
 | 
			
		||||
    <UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} />
 | 
			
		||||
  {:else if $retried === 1}
 | 
			
		||||
    <div class="alert">
 | 
			
		||||
    <Loading>
 | 
			
		||||
      <Tr t={t.upload.one.retrying} />
 | 
			
		||||
    </Loading>
 | 
			
		||||
      <Loading>
 | 
			
		||||
        <Tr t={t.upload.one.retrying} />
 | 
			
		||||
      </Loading>
 | 
			
		||||
    </div>
 | 
			
		||||
  {:else}
 | 
			
		||||
    <div class="alert">
 | 
			
		||||
    <Loading>
 | 
			
		||||
      <Tr t={t.upload.one.uploading} />
 | 
			
		||||
    </Loading>
 | 
			
		||||
      <Loading>
 | 
			
		||||
        <Tr t={t.upload.one.uploading} />
 | 
			
		||||
      </Loading>
 | 
			
		||||
    </div>
 | 
			
		||||
  {/if}
 | 
			
		||||
{:else if $uploadStarted > 1}
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +77,6 @@
 | 
			
		|||
    </Loading>
 | 
			
		||||
  {/if}
 | 
			
		||||
  {#if $failed > 0}
 | 
			
		||||
    
 | 
			
		||||
   <UploadFailedMessage failed={$failed} on:click={() => dismissed = $failed}/>
 | 
			
		||||
    <UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} />
 | 
			
		||||
  {/if}
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -67,7 +67,7 @@
 | 
			
		|||
 | 
			
		||||
        if (!rangeIsShown) {
 | 
			
		||||
          new ShowDataLayer(map, {
 | 
			
		||||
            layer: new LayerConfig(<any> boundsdisplay),
 | 
			
		||||
            layer: new LayerConfig(<any>boundsdisplay),
 | 
			
		||||
            features: new StaticFeatureSource([
 | 
			
		||||
              turf.circle(c, maxDistanceInMeters, {
 | 
			
		||||
                units: "meters",
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +84,11 @@
 | 
			
		|||
 | 
			
		||||
<div class="min-h-32 relative h-full cursor-pointer overflow-hidden">
 | 
			
		||||
  <div class="absolute top-0 left-0 h-full w-full cursor-pointer">
 | 
			
		||||
    <MaplibreMap center={{ lng: initialCoordinate.lon, lat: initialCoordinate.lat }} {map} mapProperties={mla}/>
 | 
			
		||||
    <MaplibreMap
 | 
			
		||||
      center={{ lng: initialCoordinate.lon, lat: initialCoordinate.lat }}
 | 
			
		||||
      {map}
 | 
			
		||||
      mapProperties={mla}
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,13 +64,13 @@
 | 
			
		|||
      update()
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col gap-y-1">
 | 
			
		||||
  <div class="interactive m-1 mt-2 flex space-x-1 font-bold">
 | 
			
		||||
  <span>
 | 
			
		||||
    {prefix}
 | 
			
		||||
  </span>
 | 
			
		||||
    <span>
 | 
			
		||||
      {prefix}
 | 
			
		||||
    </span>
 | 
			
		||||
    <select bind:value={$currentLang}>
 | 
			
		||||
      {#each allLanguages as language}
 | 
			
		||||
        <option value={language}>
 | 
			
		||||
| 
						 | 
				
			
			@ -89,14 +89,18 @@
 | 
			
		|||
      on:submit={() => dispatch("submit")}
 | 
			
		||||
    />
 | 
			
		||||
    <span>
 | 
			
		||||
    {postfix}
 | 
			
		||||
  </span>
 | 
			
		||||
 | 
			
		||||
      {postfix}
 | 
			
		||||
    </span>
 | 
			
		||||
  </div>
 | 
			
		||||
  You have currently set translations for
 | 
			
		||||
  <ul>
 | 
			
		||||
    {#each Object.keys($translations) as l}
 | 
			
		||||
      <li><button class="small" on:click={() => currentLang.setData(l)}><b>{l}:</b> {$translations[l]}</button></li>
 | 
			
		||||
      <li>
 | 
			
		||||
        <button class="small" on:click={() => currentLang.setData(l)}>
 | 
			
		||||
          <b>{l}:</b>
 | 
			
		||||
          {$translations[l]}
 | 
			
		||||
        </button>
 | 
			
		||||
      </li>
 | 
			
		||||
    {/each}
 | 
			
		||||
  </ul>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,7 +61,7 @@ export default class InputHelpers {
 | 
			
		|||
            mapProperties = { ...mapProperties, zoom: new UIEventSource<number>(zoom) }
 | 
			
		||||
        }
 | 
			
		||||
        if (!mapProperties.rasterLayer) {
 | 
			
		||||
       /*     mapProperties = {
 | 
			
		||||
            /*     mapProperties = {
 | 
			
		||||
                ...mapProperties, rasterLayer: properties?.mapProperties?.rasterLayer
 | 
			
		||||
            }*/
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -76,8 +76,11 @@ export default class InputHelpers {
 | 
			
		|||
        const args = inputHelperOptions.args ?? []
 | 
			
		||||
        const searchKey: string = <string>args[0] ?? "name"
 | 
			
		||||
 | 
			
		||||
        const searchFor: string = searchKey.split(";").map(k => inputHelperOptions.feature?.properties[k]?.toLowerCase())
 | 
			
		||||
            .find(foundValue => !!foundValue) ?? ""
 | 
			
		||||
        const searchFor: string =
 | 
			
		||||
            searchKey
 | 
			
		||||
                .split(";")
 | 
			
		||||
                .map((k) => inputHelperOptions.feature?.properties[k]?.toLowerCase())
 | 
			
		||||
                .find((foundValue) => !!foundValue) ?? ""
 | 
			
		||||
 | 
			
		||||
        let searchForValue: UIEventSource<string> = new UIEventSource(searchFor)
 | 
			
		||||
        const options: any = args[1]
 | 
			
		||||
| 
						 | 
				
			
			@ -125,7 +128,7 @@ export default class InputHelpers {
 | 
			
		|||
            value,
 | 
			
		||||
            searchText: searchForValue,
 | 
			
		||||
            instanceOf,
 | 
			
		||||
            notInstanceOf
 | 
			
		||||
            notInstanceOf,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,31 +47,31 @@
 | 
			
		|||
      <LanguageIcon class="mr-1 h-4 w-4 shrink-0" aria-hidden="true" />
 | 
			
		||||
    </label>
 | 
			
		||||
 | 
			
		||||
      <Dropdown cls="max-w-full" value={assignTo} id="pick-language">
 | 
			
		||||
        {#if preferredFiltered}
 | 
			
		||||
          {#each preferredFiltered as language}
 | 
			
		||||
            <option value={language} class="font-bold">
 | 
			
		||||
              {native[language] ?? ""}
 | 
			
		||||
              {#if language !== $current}
 | 
			
		||||
                ({language_translations[language]?.[$current] ?? language})
 | 
			
		||||
              {/if}
 | 
			
		||||
            </option>
 | 
			
		||||
          {/each}
 | 
			
		||||
          <option disabled />
 | 
			
		||||
        {/if}
 | 
			
		||||
 | 
			
		||||
        {#each availableLanguages.filter((l) => l !== "_context") as language}
 | 
			
		||||
    <Dropdown cls="max-w-full" value={assignTo} id="pick-language">
 | 
			
		||||
      {#if preferredFiltered}
 | 
			
		||||
        {#each preferredFiltered as language}
 | 
			
		||||
          <option value={language} class="font-bold">
 | 
			
		||||
            {native[language] ?? ""}
 | 
			
		||||
            {#if language !== $current}
 | 
			
		||||
              {#if language_translations[language]?.[$current] !== undefined}
 | 
			
		||||
                ({language_translations[language]?.[$current] + " - " + language ?? language})
 | 
			
		||||
              {:else}
 | 
			
		||||
                ({language})
 | 
			
		||||
              {/if}
 | 
			
		||||
              ({language_translations[language]?.[$current] ?? language})
 | 
			
		||||
            {/if}
 | 
			
		||||
          </option>
 | 
			
		||||
        {/each}
 | 
			
		||||
      </Dropdown>
 | 
			
		||||
        <option disabled />
 | 
			
		||||
      {/if}
 | 
			
		||||
 | 
			
		||||
      {#each availableLanguages.filter((l) => l !== "_context") as language}
 | 
			
		||||
        <option value={language} class="font-bold">
 | 
			
		||||
          {native[language] ?? ""}
 | 
			
		||||
          {#if language !== $current}
 | 
			
		||||
            {#if language_translations[language]?.[$current] !== undefined}
 | 
			
		||||
              ({language_translations[language]?.[$current] + " - " + language ?? language})
 | 
			
		||||
            {:else}
 | 
			
		||||
              ({language})
 | 
			
		||||
            {/if}
 | 
			
		||||
          {/if}
 | 
			
		||||
        </option>
 | 
			
		||||
      {/each}
 | 
			
		||||
    </Dropdown>
 | 
			
		||||
  </form>
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,5 +51,4 @@ export default class OpeningHoursValidator extends Validator {
 | 
			
		|||
            ])
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,7 +45,7 @@ export default class UrlValidator extends Validator {
 | 
			
		|||
                "pk_medium",
 | 
			
		||||
                "pk_campaign",
 | 
			
		||||
                "pk_content",
 | 
			
		||||
                "pk_kwd"
 | 
			
		||||
                "pk_kwd",
 | 
			
		||||
            ]
 | 
			
		||||
            for (const dontLike of blacklistedTrackingParams) {
 | 
			
		||||
                url.searchParams.delete(dontLike.toLowerCase())
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,13 +26,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
 | 
			
		|||
        "dragRotate",
 | 
			
		||||
        "dragPan",
 | 
			
		||||
        "keyboard",
 | 
			
		||||
        "touchZoomRotate"
 | 
			
		||||
        "touchZoomRotate",
 | 
			
		||||
    ]
 | 
			
		||||
    private static maplibre_zoom_handlers = [
 | 
			
		||||
        "scrollZoom",
 | 
			
		||||
        "boxZoom",
 | 
			
		||||
        "doubleClickZoom",
 | 
			
		||||
        "touchZoomRotate"
 | 
			
		||||
        "touchZoomRotate",
 | 
			
		||||
    ]
 | 
			
		||||
    readonly location: UIEventSource<{ lon: number; lat: number }>
 | 
			
		||||
    readonly zoom: UIEventSource<number>
 | 
			
		||||
| 
						 | 
				
			
			@ -62,8 +62,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
 | 
			
		|||
        if (!MapLibreAdaptor.pmtilesInited) {
 | 
			
		||||
            maplibregl.addProtocol("pmtiles", new Protocol().tile)
 | 
			
		||||
            MapLibreAdaptor.pmtilesInited = true
 | 
			
		||||
            console.log("PM-tiles protocol added" +
 | 
			
		||||
                "")
 | 
			
		||||
            console.log("PM-tiles protocol added" + "")
 | 
			
		||||
        }
 | 
			
		||||
        this._maplibreMap = maplibreMap
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -113,7 +112,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        maplibreMap.addCallbackAndRunD((map) => {
 | 
			
		||||
 | 
			
		||||
            map.on("load", () => {
 | 
			
		||||
                self.MoveMapToCurrentLoc(self.location.data)
 | 
			
		||||
                self.SetZoom(self.zoom.data)
 | 
			
		||||
| 
						 | 
				
			
			@ -216,9 +214,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
 | 
			
		|||
        return {
 | 
			
		||||
            map: mlmap,
 | 
			
		||||
            ui: new SvelteUIElement(MaplibreMap, {
 | 
			
		||||
                map: mlmap
 | 
			
		||||
                map: mlmap,
 | 
			
		||||
            }),
 | 
			
		||||
            mapproperties: new MapLibreAdaptor(mlmap)
 | 
			
		||||
            mapproperties: new MapLibreAdaptor(mlmap),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -286,7 +284,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
 | 
			
		|||
    ) {
 | 
			
		||||
        const event = {
 | 
			
		||||
            date: new Date(),
 | 
			
		||||
            key: key
 | 
			
		||||
            key: key,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < this._onKeyNavigation.length; i++) {
 | 
			
		||||
| 
						 | 
				
			
			@ -330,13 +328,19 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
 | 
			
		|||
        rescaleIcons: number,
 | 
			
		||||
        pixelRatio: number
 | 
			
		||||
    ) {
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
            const allimages = element.getElementsByTagName("img")
 | 
			
		||||
            for (const img of Array.from(allimages)) {
 | 
			
		||||
                let isLoaded: boolean = false
 | 
			
		||||
                while (!isLoaded) {
 | 
			
		||||
                    console.log("Waiting for image", img.src, "to load", img.complete, img.naturalWidth, img)
 | 
			
		||||
                    console.log(
 | 
			
		||||
                        "Waiting for image",
 | 
			
		||||
                        img.src,
 | 
			
		||||
                        "to load",
 | 
			
		||||
                        img.complete,
 | 
			
		||||
                        img.naturalWidth,
 | 
			
		||||
                        img
 | 
			
		||||
                    )
 | 
			
		||||
                    await Utils.waitFor(250)
 | 
			
		||||
                    isLoaded = img.complete && img.width > 0
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -349,22 +353,31 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
 | 
			
		|||
        element.style.transform = ""
 | 
			
		||||
        const offset = style.match(/translate\(([-0-9]+)%, ?([-0-9]+)%\)/)
 | 
			
		||||
 | 
			
		||||
        let labels =<HTMLElement[]> Array.from(element.getElementsByClassName("marker-label"))
 | 
			
		||||
        const origLabelTransforms = labels.map(l => l.style.transform)
 | 
			
		||||
        let labels = <HTMLElement[]>Array.from(element.getElementsByClassName("marker-label"))
 | 
			
		||||
        const origLabelTransforms = labels.map((l) => l.style.transform)
 | 
			
		||||
        // We save the original width (`w`) and height (`h`) in order to restore them later on
 | 
			
		||||
        const w = element.style.width
 | 
			
		||||
        const h = Number(element.style.height)
 | 
			
		||||
        const targetW = Math.max(element.getBoundingClientRect().width * 4,
 | 
			
		||||
            ...labels.map(l => l.getBoundingClientRect().width))
 | 
			
		||||
        const targetH = element.getBoundingClientRect().height +
 | 
			
		||||
            Math.max(...labels.map(l => l.getBoundingClientRect().height * 2 /* A bit of buffer to catch eventual 'margin-top'*/))
 | 
			
		||||
        const targetW = Math.max(
 | 
			
		||||
            element.getBoundingClientRect().width * 4,
 | 
			
		||||
            ...labels.map((l) => l.getBoundingClientRect().width)
 | 
			
		||||
        )
 | 
			
		||||
        const targetH =
 | 
			
		||||
            element.getBoundingClientRect().height +
 | 
			
		||||
            Math.max(
 | 
			
		||||
                ...labels.map(
 | 
			
		||||
                    (l) =>
 | 
			
		||||
                        l.getBoundingClientRect().height *
 | 
			
		||||
                        2 /* A bit of buffer to catch eventual 'margin-top'*/
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        // Force a wider view for icon badges
 | 
			
		||||
        element.style.width = targetW + "px"
 | 
			
		||||
        // Force more height to include labels
 | 
			
		||||
        element.style.height = targetH + "px"
 | 
			
		||||
        element.classList.add("w-full", "flex", "flex-col", "items-center")
 | 
			
		||||
        labels.forEach(l => {
 | 
			
		||||
        labels.forEach((l) => {
 | 
			
		||||
            l.style.transform = ""
 | 
			
		||||
        })
 | 
			
		||||
        await Utils.awaitAnimationFrame()
 | 
			
		||||
| 
						 | 
				
			
			@ -386,13 +399,12 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
 | 
			
		|||
        y *= pixelRatio
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const xdiff = img.width * rescaleIcons / 2
 | 
			
		||||
            const xdiff = (img.width * rescaleIcons) / 2
 | 
			
		||||
            drawOn.drawImage(img, x - xdiff, y, img.width * rescaleIcons, img.height * rescaleIcons)
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.log("Could not draw image because of", e)
 | 
			
		||||
        }
 | 
			
		||||
        element.classList.remove("w-full", "flex", "flex-col", "items-center")
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -461,7 +473,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
 | 
			
		|||
        const bounds = map.getBounds()
 | 
			
		||||
        const bbox = new BBox([
 | 
			
		||||
            [bounds.getEast(), bounds.getNorth()],
 | 
			
		||||
            [bounds.getWest(), bounds.getSouth()]
 | 
			
		||||
            [bounds.getWest(), bounds.getSouth()],
 | 
			
		||||
        ])
 | 
			
		||||
        if (this.bounds.data === undefined || !isSetup) {
 | 
			
		||||
            this.bounds.setData(bbox)
 | 
			
		||||
| 
						 | 
				
			
			@ -639,14 +651,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
 | 
			
		|||
                type: "raster-dem",
 | 
			
		||||
                url:
 | 
			
		||||
                    "https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" +
 | 
			
		||||
                    Constants.maptilerApiKey
 | 
			
		||||
                    Constants.maptilerApiKey,
 | 
			
		||||
            })
 | 
			
		||||
            try {
 | 
			
		||||
                while (!map?.isStyleLoaded()) {
 | 
			
		||||
                    await Utils.waitFor(250)
 | 
			
		||||
                }
 | 
			
		||||
                map.setTerrain({
 | 
			
		||||
                    source: id
 | 
			
		||||
                    source: id,
 | 
			
		||||
                })
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                console.error(e)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,7 +25,6 @@
 | 
			
		|||
 | 
			
		||||
  let container: HTMLElement
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  let _map: Map
 | 
			
		||||
  onMount(() => {
 | 
			
		||||
    const { lon, lat } = mapProperties?.location?.data ?? { lon: 0, lat: 0 }
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +32,10 @@
 | 
			
		|||
    const rasterLayer: RasterLayerProperties = mapProperties?.rasterLayer?.data?.properties
 | 
			
		||||
    let styleUrl: string
 | 
			
		||||
    if (rasterLayer?.type === "vector") {
 | 
			
		||||
      styleUrl = rasterLayer?.style ?? rasterLayer?.url ?? AvailableRasterLayers.defaultBackgroundLayer.properties.url
 | 
			
		||||
      styleUrl =
 | 
			
		||||
        rasterLayer?.style ??
 | 
			
		||||
        rasterLayer?.url ??
 | 
			
		||||
        AvailableRasterLayers.defaultBackgroundLayer.properties.url
 | 
			
		||||
    } else {
 | 
			
		||||
      const defaultLayer = AvailableRasterLayers.defaultBackgroundLayer.properties
 | 
			
		||||
      styleUrl = defaultLayer.style ?? defaultLayer.url
 | 
			
		||||
| 
						 | 
				
			
			@ -48,13 +50,13 @@
 | 
			
		|||
      center: { lng: lon, lat },
 | 
			
		||||
      maxZoom: 24,
 | 
			
		||||
      interactive: true,
 | 
			
		||||
      attributionControl: false
 | 
			
		||||
      attributionControl: false,
 | 
			
		||||
    }
 | 
			
		||||
    _map = new maplibre.Map(options)
 | 
			
		||||
    window.requestAnimationFrame(() => {
 | 
			
		||||
      _map.resize()
 | 
			
		||||
    })
 | 
			
		||||
    _map.on("load", function() {
 | 
			
		||||
    _map.on("load", function () {
 | 
			
		||||
      _map.resize()
 | 
			
		||||
      const canvas = _map.getCanvas()
 | 
			
		||||
      if (interactive) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,13 +65,12 @@
 | 
			
		|||
        updateLocation()
 | 
			
		||||
        window.setTimeout(updateLocation, 150)
 | 
			
		||||
        window.setTimeout(updateLocation, 500)
 | 
			
		||||
      }),
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="absolute w-full h-full flex items-center justify-center"
 | 
			
		||||
     style="z-index: 100">
 | 
			
		||||
<div class="absolute flex h-full w-full items-center justify-center" style="z-index: 100">
 | 
			
		||||
  <StyleLoadingIndicator map={altmap} />
 | 
			
		||||
</div>
 | 
			
		||||
<MaplibreMap {interactive} map={altmap} mapProperties={altproperties} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -109,8 +109,12 @@ class SingleBackgroundHandler {
 | 
			
		|||
        const background = this._targetLayer.properties
 | 
			
		||||
        console.debug("Enabling", background.id)
 | 
			
		||||
        let addLayerBeforeId = "transit_pier" // this is the first non-landuse item in the stylesheet, we add the raster layer before the roads but above the landuse
 | 
			
		||||
        if(!map.getLayer(addLayerBeforeId)){
 | 
			
		||||
            console.warn("Layer", addLayerBeforeId,"not foundhttp://127.0.0.1:1234/theme.html?layout=cyclofix&z=14.8&lat=51.05282501324558&lon=3.720591622281745&layer-range=true")
 | 
			
		||||
        if (!map.getLayer(addLayerBeforeId)) {
 | 
			
		||||
            console.warn(
 | 
			
		||||
                "Layer",
 | 
			
		||||
                addLayerBeforeId,
 | 
			
		||||
                "not foundhttp://127.0.0.1:1234/theme.html?layout=cyclofix&z=14.8&lat=51.05282501324558&lon=3.720591622281745&layer-range=true"
 | 
			
		||||
            )
 | 
			
		||||
            addLayerBeforeId = undefined
 | 
			
		||||
        }
 | 
			
		||||
        if (background.category === "osmbasedmap" || background.category === "map") {
 | 
			
		||||
| 
						 | 
				
			
			@ -140,15 +144,15 @@ class SingleBackgroundHandler {
 | 
			
		|||
                        type: "raster",
 | 
			
		||||
                        source: background.id,
 | 
			
		||||
                        paint: {
 | 
			
		||||
                            "raster-opacity": 0
 | 
			
		||||
                        }
 | 
			
		||||
                            "raster-opacity": 0,
 | 
			
		||||
                        },
 | 
			
		||||
                    },
 | 
			
		||||
                    addLayerBeforeId
 | 
			
		||||
                )
 | 
			
		||||
                this.opacity.addCallbackAndRun((o) => {
 | 
			
		||||
                    try{
 | 
			
		||||
                    try {
 | 
			
		||||
                        map.setPaintProperty(background.id, "raster-opacity", o)
 | 
			
		||||
                    }catch (e) {
 | 
			
		||||
                    } catch (e) {
 | 
			
		||||
                        console.debug("Could not set raster-opacity of", background.id)
 | 
			
		||||
                        return true // This layer probably doesn't exist anymore, so we unregister
 | 
			
		||||
                    }
 | 
			
		||||
| 
						 | 
				
			
			@ -185,11 +189,13 @@ export default class RasterLayerHandler {
 | 
			
		|||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static prepareSource(layer: RasterLayerProperties): RasterSourceSpecification | VectorSourceSpecification {
 | 
			
		||||
    public static prepareSource(
 | 
			
		||||
        layer: RasterLayerProperties
 | 
			
		||||
    ): RasterSourceSpecification | VectorSourceSpecification {
 | 
			
		||||
        if (layer.type === "vector") {
 | 
			
		||||
            const vs: VectorSourceSpecification = {
 | 
			
		||||
                type: "vector",
 | 
			
		||||
                url: layer.url
 | 
			
		||||
                url: layer.url,
 | 
			
		||||
            }
 | 
			
		||||
            return vs
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -202,7 +208,7 @@ export default class RasterLayerHandler {
 | 
			
		|||
            minzoom: layer["min_zoom"] ?? 1,
 | 
			
		||||
            maxzoom: layer["max_zoom"] ?? 25,
 | 
			
		||||
            // Bit of a hack, but seems to work
 | 
			
		||||
            scheme: layer.url.includes("{-y}") ? "tms" : "xyz"
 | 
			
		||||
            scheme: layer.url.includes("{-y}") ? "tms" : "xyz",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -216,7 +222,7 @@ export default class RasterLayerHandler {
 | 
			
		|||
            "{width}": "" + size,
 | 
			
		||||
            "{height}": "" + size,
 | 
			
		||||
            "{zoom}": "{z}",
 | 
			
		||||
            "{-y}": "{y}"
 | 
			
		||||
            "{-y}": "{y}",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const key in toReplace) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -78,7 +78,7 @@
 | 
			
		|||
{#if hasLayers}
 | 
			
		||||
  <form class="flex h-full w-full flex-col" on:submit|preventDefault={() => {}}>
 | 
			
		||||
    <button tabindex="-1" on:click={() => apply()} class="m-0 h-full w-full cursor-pointer p-1">
 | 
			
		||||
      <span class="pointer-events-none h-full w-full relative">
 | 
			
		||||
      <span class="pointer-events-none relative h-full w-full">
 | 
			
		||||
        <OverlayMap
 | 
			
		||||
          interactive={false}
 | 
			
		||||
          rasterLayer={rasterLayerOnMap}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -154,7 +154,7 @@ class PointRenderingLayer {
 | 
			
		|||
 | 
			
		||||
        if (this._onClick) {
 | 
			
		||||
            const self = this
 | 
			
		||||
            el.addEventListener("click", function(ev) {
 | 
			
		||||
            el.addEventListener("click", function (ev) {
 | 
			
		||||
                ev.preventDefault()
 | 
			
		||||
                self._onClick(feature)
 | 
			
		||||
                // Workaround to signal the MapLibreAdaptor to ignore this click
 | 
			
		||||
| 
						 | 
				
			
			@ -200,7 +200,7 @@ class LineRenderingLayer {
 | 
			
		|||
        "lineCap",
 | 
			
		||||
        "offset",
 | 
			
		||||
        "fill",
 | 
			
		||||
        "fillColor"
 | 
			
		||||
        "fillColor",
 | 
			
		||||
    ] as const
 | 
			
		||||
 | 
			
		||||
    private static readonly lineConfigKeysColor = ["color", "fillColor"] as const
 | 
			
		||||
| 
						 | 
				
			
			@ -264,8 +264,8 @@ class LineRenderingLayer {
 | 
			
		|||
                        "icon-rotation-alignment": "map",
 | 
			
		||||
                        "icon-pitch-alignment": "map",
 | 
			
		||||
                        "icon-image": imgId,
 | 
			
		||||
                        "icon-size": 0.055
 | 
			
		||||
                    }
 | 
			
		||||
                        "icon-size": 0.055,
 | 
			
		||||
                    },
 | 
			
		||||
                }
 | 
			
		||||
                const filter = img.if?.asMapboxExpression()
 | 
			
		||||
                if (filter) {
 | 
			
		||||
| 
						 | 
				
			
			@ -338,9 +338,9 @@ class LineRenderingLayer {
 | 
			
		|||
                    type: "geojson",
 | 
			
		||||
                    data: {
 | 
			
		||||
                        type: "FeatureCollection",
 | 
			
		||||
                        features
 | 
			
		||||
                        features,
 | 
			
		||||
                    },
 | 
			
		||||
                    promoteId: "id"
 | 
			
		||||
                    promoteId: "id",
 | 
			
		||||
                })
 | 
			
		||||
                const linelayer = this._layername + "_line"
 | 
			
		||||
                const layer: AddLayerObject = {
 | 
			
		||||
| 
						 | 
				
			
			@ -351,15 +351,15 @@ class LineRenderingLayer {
 | 
			
		|||
                        "line-color": ["feature-state", "color"],
 | 
			
		||||
                        "line-opacity": ["feature-state", "color-opacity"],
 | 
			
		||||
                        "line-width": ["feature-state", "width"],
 | 
			
		||||
                        "line-offset": ["feature-state", "offset"]
 | 
			
		||||
                        "line-offset": ["feature-state", "offset"],
 | 
			
		||||
                    },
 | 
			
		||||
                    layout: {
 | 
			
		||||
                        "line-cap": "round"
 | 
			
		||||
                    }
 | 
			
		||||
                        "line-cap": "round",
 | 
			
		||||
                    },
 | 
			
		||||
                }
 | 
			
		||||
                if (this._config.dashArray) {
 | 
			
		||||
 | 
			
		||||
                    layer.paint["line-dasharray"] = this._config.dashArray?.split(" ")?.map(s => Number(s)) ?? null
 | 
			
		||||
                    layer.paint["line-dasharray"] =
 | 
			
		||||
                        this._config.dashArray?.split(" ")?.map((s) => Number(s)) ?? null
 | 
			
		||||
                }
 | 
			
		||||
                map.addLayer(layer)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -393,8 +393,8 @@ class LineRenderingLayer {
 | 
			
		|||
                    layout: {},
 | 
			
		||||
                    paint: {
 | 
			
		||||
                        "fill-color": ["feature-state", "fillColor"],
 | 
			
		||||
                        "fill-opacity": ["feature-state", "fillColor-opacity"]
 | 
			
		||||
                    }
 | 
			
		||||
                        "fill-opacity": ["feature-state", "fillColor-opacity"],
 | 
			
		||||
                    },
 | 
			
		||||
                })
 | 
			
		||||
                if (this._onClick) {
 | 
			
		||||
                    map.on("click", polylayer, (e) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -425,7 +425,7 @@ class LineRenderingLayer {
 | 
			
		|||
                this.currentSourceData = features
 | 
			
		||||
                src.setData({
 | 
			
		||||
                    type: "FeatureCollection",
 | 
			
		||||
                    features: this.currentSourceData
 | 
			
		||||
                    features: this.currentSourceData,
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -509,14 +509,14 @@ export default class ShowDataLayer {
 | 
			
		|||
                layers.filter((l) => l.source !== null).map((l) => new FilteredLayer(l)),
 | 
			
		||||
                features,
 | 
			
		||||
                {
 | 
			
		||||
                    constructStore: (features, layer) => new SimpleFeatureSource(layer, features)
 | 
			
		||||
                    constructStore: (features, layer) => new SimpleFeatureSource(layer, features),
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        perLayer.forEach((fs) => {
 | 
			
		||||
            new ShowDataLayer(mlmap, {
 | 
			
		||||
                layer: fs.layer.layerDef,
 | 
			
		||||
                features: fs,
 | 
			
		||||
                ...(options ?? {})
 | 
			
		||||
                ...(options ?? {}),
 | 
			
		||||
            })
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -529,12 +529,11 @@ export default class ShowDataLayer {
 | 
			
		|||
        return new ShowDataLayer(map, {
 | 
			
		||||
            layer: ShowDataLayer.rangeLayer,
 | 
			
		||||
            features,
 | 
			
		||||
            doShowLayer
 | 
			
		||||
            doShowLayer,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public destruct() {
 | 
			
		||||
    }
 | 
			
		||||
    public destruct() {}
 | 
			
		||||
 | 
			
		||||
    private zoomToCurrentFeatures(map: MlMap) {
 | 
			
		||||
        if (this._options.zoomToFeatures) {
 | 
			
		||||
| 
						 | 
				
			
			@ -543,7 +542,7 @@ export default class ShowDataLayer {
 | 
			
		|||
            map.resize()
 | 
			
		||||
            map.fitBounds(bbox.toLngLat(), {
 | 
			
		||||
                padding: { top: 10, bottom: 10, left: 10, right: 10 },
 | 
			
		||||
                animate: false
 | 
			
		||||
                animate: false,
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -552,11 +551,12 @@ export default class ShowDataLayer {
 | 
			
		|||
        let { features, doShowLayer, fetchStore, selectedElement } = this._options
 | 
			
		||||
        let onClick = this._options.onClick
 | 
			
		||||
        if (!onClick && selectedElement) {
 | 
			
		||||
            onClick = (this._options.layer.title === undefined
 | 
			
		||||
                ? undefined
 | 
			
		||||
                : (feature: Feature) => {
 | 
			
		||||
                    selectedElement?.setData(feature)
 | 
			
		||||
                })
 | 
			
		||||
            onClick =
 | 
			
		||||
                this._options.layer.title === undefined
 | 
			
		||||
                    ? undefined
 | 
			
		||||
                    : (feature: Feature) => {
 | 
			
		||||
                          selectedElement?.setData(feature)
 | 
			
		||||
                      }
 | 
			
		||||
        }
 | 
			
		||||
        if (this._options.drawLines !== false) {
 | 
			
		||||
            for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,16 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
import Translations from "../i18n/Translations.js";
 | 
			
		||||
import Min from "../../assets/svg/Min.svelte";
 | 
			
		||||
import MapControlButton from "../Base/MapControlButton.svelte";
 | 
			
		||||
import Plus from "../../assets/svg/Plus.svelte";
 | 
			
		||||
import type { MapProperties } from "../../Models/MapProperties"
 | 
			
		||||
  import Translations from "../i18n/Translations.js"
 | 
			
		||||
  import Min from "../../assets/svg/Min.svelte"
 | 
			
		||||
  import MapControlButton from "../Base/MapControlButton.svelte"
 | 
			
		||||
  import Plus from "../../assets/svg/Plus.svelte"
 | 
			
		||||
  import type { MapProperties } from "../../Models/MapProperties"
 | 
			
		||||
 | 
			
		||||
export let adaptor: MapProperties
 | 
			
		||||
let canZoomIn = adaptor.maxzoom.map(mz => adaptor.zoom.data < mz, [adaptor.zoom] )
 | 
			
		||||
let canZoomOut = adaptor.minzoom.map(mz => adaptor.zoom.data > mz, [adaptor.zoom] )
 | 
			
		||||
  export let adaptor: MapProperties
 | 
			
		||||
  let canZoomIn = adaptor.maxzoom.map((mz) => adaptor.zoom.data < mz, [adaptor.zoom])
 | 
			
		||||
  let canZoomOut = adaptor.minzoom.map((mz) => adaptor.zoom.data > mz, [adaptor.zoom])
 | 
			
		||||
</script>
 | 
			
		||||
<div class="absolute bottom-0 right-0 pointer-events-none flex flex-col">
 | 
			
		||||
 | 
			
		||||
<div class="pointer-events-none absolute bottom-0 right-0 flex flex-col">
 | 
			
		||||
  <MapControlButton
 | 
			
		||||
    enabled={canZoomIn}
 | 
			
		||||
    cls="m-0.5 p-1"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,24 +10,25 @@
 | 
			
		|||
   * Optional. Only used for the 'global' change indicator so that it won't spin on pan/zoom but only when a change _actually_ occured
 | 
			
		||||
   */
 | 
			
		||||
  export let rasterLayer: UIEventSource<any> = undefined
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  let didChange = undefined
 | 
			
		||||
  onDestroy(rasterLayer?.addCallback(() => {
 | 
			
		||||
    didChange = true
 | 
			
		||||
  }) ??( () => {}))
 | 
			
		||||
  
 | 
			
		||||
  onDestroy(Stores.Chronic(250).addCallback(
 | 
			
		||||
    () => {
 | 
			
		||||
  onDestroy(
 | 
			
		||||
    rasterLayer?.addCallback(() => {
 | 
			
		||||
      didChange = true
 | 
			
		||||
    }) ?? (() => {})
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  onDestroy(
 | 
			
		||||
    Stores.Chronic(250).addCallback(() => {
 | 
			
		||||
      const mapIsLoading = !map.data?.isStyleLoaded()
 | 
			
		||||
      isLoading = mapIsLoading && (didChange || rasterLayer === undefined)
 | 
			
		||||
      if(didChange && !mapIsLoading){
 | 
			
		||||
      if (didChange && !mapIsLoading) {
 | 
			
		||||
        didChange = false
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  ))
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{#if isLoading}
 | 
			
		||||
  <Loading cls="h-6 w-6" />
 | 
			
		||||
{:else}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,7 +32,7 @@ export class OH {
 | 
			
		|||
        th: 3,
 | 
			
		||||
        fr: 4,
 | 
			
		||||
        sa: 5,
 | 
			
		||||
        su: 6
 | 
			
		||||
        su: 6,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static hhmm(h: number, m: number): string {
 | 
			
		||||
| 
						 | 
				
			
			@ -143,7 +143,7 @@ export class OH {
 | 
			
		|||
        const queue = ohs.map((oh) => {
 | 
			
		||||
            if (oh.endHour === 0 && oh.endMinutes === 0) {
 | 
			
		||||
                const newOh = {
 | 
			
		||||
                    ...oh
 | 
			
		||||
                    ...oh,
 | 
			
		||||
                }
 | 
			
		||||
                newOh.endHour = 24
 | 
			
		||||
                return newOh
 | 
			
		||||
| 
						 | 
				
			
			@ -211,7 +211,7 @@ export class OH {
 | 
			
		|||
                        startMinutes: startMinutes,
 | 
			
		||||
                        endHour: endHour,
 | 
			
		||||
                        endMinutes: endMinutes,
 | 
			
		||||
                        weekday: guard.weekday
 | 
			
		||||
                        weekday: guard.weekday,
 | 
			
		||||
                    })
 | 
			
		||||
 | 
			
		||||
                    doAddEntry = false
 | 
			
		||||
| 
						 | 
				
			
			@ -279,7 +279,7 @@ export class OH {
 | 
			
		|||
            startHour: start.hours,
 | 
			
		||||
            startMinutes: start.minutes,
 | 
			
		||||
            endHour: end.hours,
 | 
			
		||||
            endMinutes: end.minutes
 | 
			
		||||
            endMinutes: end.minutes,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -337,8 +337,8 @@ export class OH {
 | 
			
		|||
                            startHour: 0,
 | 
			
		||||
                            startMinutes: 0,
 | 
			
		||||
                            endHour: 24,
 | 
			
		||||
                            endMinutes: 0
 | 
			
		||||
                        }
 | 
			
		||||
                            endMinutes: 0,
 | 
			
		||||
                        },
 | 
			
		||||
                    ]
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -388,13 +388,13 @@ export class OH {
 | 
			
		|||
        str = str.trim()
 | 
			
		||||
        if (str.toLowerCase() === "ph off") {
 | 
			
		||||
            return {
 | 
			
		||||
                mode: "off"
 | 
			
		||||
                mode: "off",
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (str.toLowerCase() === "ph open") {
 | 
			
		||||
            return {
 | 
			
		||||
                mode: "open"
 | 
			
		||||
                mode: "open",
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -410,7 +410,7 @@ export class OH {
 | 
			
		|||
            return {
 | 
			
		||||
                mode: " ",
 | 
			
		||||
                start: OH.hhmm(timerange.startHour, timerange.startMinutes),
 | 
			
		||||
                end: OH.hhmm(timerange.endHour, timerange.endMinutes)
 | 
			
		||||
                end: OH.hhmm(timerange.endHour, timerange.endMinutes),
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            return null
 | 
			
		||||
| 
						 | 
				
			
			@ -576,8 +576,8 @@ This list will be sorted
 | 
			
		|||
                lon: tags._lon,
 | 
			
		||||
                address: {
 | 
			
		||||
                    country_code: country.toLowerCase(),
 | 
			
		||||
                    state: undefined
 | 
			
		||||
                }
 | 
			
		||||
                    state: undefined,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            <any>{ tag_key: "opening_hours" }
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -753,7 +753,7 @@ This list will be sorted
 | 
			
		|||
                isOpen: iterator.getState(),
 | 
			
		||||
                comment: iterator.getComment(),
 | 
			
		||||
                startDate: iterator.getDate() as Date,
 | 
			
		||||
                endDate: endDate // Should be overwritten by the next iteration
 | 
			
		||||
                endDate: endDate, // Should be overwritten by the next iteration
 | 
			
		||||
            }
 | 
			
		||||
            prevValue = value
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -891,7 +891,7 @@ This list will be sorted
 | 
			
		|||
                        startHour: timerange.startHour,
 | 
			
		||||
                        startMinutes: timerange.startMinutes,
 | 
			
		||||
                        endHour: timerange.endHour,
 | 
			
		||||
                        endMinutes: timerange.endMinutes
 | 
			
		||||
                        endMinutes: timerange.endMinutes,
 | 
			
		||||
                    })
 | 
			
		||||
                } else {
 | 
			
		||||
                    ohs.push({
 | 
			
		||||
| 
						 | 
				
			
			@ -899,14 +899,14 @@ This list will be sorted
 | 
			
		|||
                        startHour: timerange.startHour,
 | 
			
		||||
                        startMinutes: timerange.startMinutes,
 | 
			
		||||
                        endHour: 0,
 | 
			
		||||
                        endMinutes: 0
 | 
			
		||||
                        endMinutes: 0,
 | 
			
		||||
                    })
 | 
			
		||||
                    ohs.push({
 | 
			
		||||
                        weekday: (weekday + 1) % 7,
 | 
			
		||||
                        startHour: 0,
 | 
			
		||||
                        startMinutes: 0,
 | 
			
		||||
                        endHour: timerange.endHour,
 | 
			
		||||
                        endMinutes: timerange.endMinutes
 | 
			
		||||
                        endMinutes: timerange.endMinutes,
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -967,7 +967,7 @@ export class ToTextualDescription {
 | 
			
		|||
            "thursday",
 | 
			
		||||
            "friday",
 | 
			
		||||
            "saturday",
 | 
			
		||||
            "sunday"
 | 
			
		||||
            "sunday",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        function addRange(start: number, end: number) {
 | 
			
		||||
| 
						 | 
				
			
			@ -1025,7 +1025,7 @@ export class ToTextualDescription {
 | 
			
		|||
    private static createRangeFor(range: OpeningRange): Translation {
 | 
			
		||||
        return Translations.t.general.opening_hours.ranges.Subs({
 | 
			
		||||
            starttime: ToTextualDescription.timeString(range.startDate),
 | 
			
		||||
            endtime: ToTextualDescription.timeString(range.endDate)
 | 
			
		||||
            endtime: ToTextualDescription.timeString(range.endDate),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1037,7 +1037,7 @@ export class ToTextualDescription {
 | 
			
		|||
        for (let i = 1; i < ranges.length; i++) {
 | 
			
		||||
            tr = Translations.t.general.opening_hours.rangescombined.Subs({
 | 
			
		||||
                range0: tr,
 | 
			
		||||
                range1: ToTextualDescription.createRangeFor(ranges[i])
 | 
			
		||||
                range1: ToTextualDescription.createRangeFor(ranges[i]),
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
        return tr
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -365,7 +365,7 @@
 | 
			
		|||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    {:else}
 | 
			
		||||
      <Loading><Tr t={Translations.t.general.add.creating}/> </Loading>
 | 
			
		||||
      <Loading><Tr t={Translations.t.general.add.creating} /></Loading>
 | 
			
		||||
    {/if}
 | 
			
		||||
  </div>
 | 
			
		||||
</LoginToggle>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
  import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
 | 
			
		||||
 | 
			
		||||
  export let tags: UIEventSource<Record<string, any>>
 | 
			
		||||
  export let tagKeys = tags.map(tgs => tgs === undefined ? [] : Object.keys(tgs))
 | 
			
		||||
  export let tagKeys = tags.map((tgs) => (tgs === undefined ? [] : Object.keys(tgs)))
 | 
			
		||||
 | 
			
		||||
  export let layer: LayerConfig | undefined = undefined
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -110,7 +110,10 @@ class ApplyButton extends UIElement {
 | 
			
		|||
        mla.allowZooming.setData(false)
 | 
			
		||||
        mla.allowMoving.setData(false)
 | 
			
		||||
 | 
			
		||||
        const previewMap = new SvelteUIElement(MaplibreMap, { mapProperties: mla, map: mlmap }).SetClass("h-48")
 | 
			
		||||
        const previewMap = new SvelteUIElement(MaplibreMap, {
 | 
			
		||||
            mapProperties: mla,
 | 
			
		||||
            map: mlmap,
 | 
			
		||||
        }).SetClass("h-48")
 | 
			
		||||
 | 
			
		||||
        const features = this.target_feature_ids.map((id) =>
 | 
			
		||||
            this.state.indexedFeatures.featuresById.data.get(id)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -109,7 +109,11 @@ export class MinimapViz implements SpecialVisualization {
 | 
			
		|||
            state.layout.layers
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return new SvelteUIElement(MaplibreMap, { interactive: false, map: mlmap, mapProperties: mla })
 | 
			
		||||
        return new SvelteUIElement(MaplibreMap, {
 | 
			
		||||
            interactive: false,
 | 
			
		||||
            map: mlmap,
 | 
			
		||||
            mapProperties: mla,
 | 
			
		||||
        })
 | 
			
		||||
            .SetClass("h-40 rounded")
 | 
			
		||||
            .SetStyle("overflow: hidden; pointer-events: none;")
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,4 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
 | 
			
		||||
  import { UIEventSource } from "../../Logic/UIEventSource"
 | 
			
		||||
  import type { Feature, Point } from "geojson"
 | 
			
		||||
  import type { SpecialVisualizationState } from "../SpecialVisualization"
 | 
			
		||||
| 
						 | 
				
			
			@ -19,18 +18,26 @@
 | 
			
		|||
  export let state: SpecialVisualizationState
 | 
			
		||||
  export let id: WayId
 | 
			
		||||
  const t = Translations.t.split
 | 
			
		||||
  let step: "initial" | "loading_way" | "splitting" | "applying_split" | "has_been_split" | "deleted" = "initial"
 | 
			
		||||
  let step:
 | 
			
		||||
    | "initial"
 | 
			
		||||
    | "loading_way"
 | 
			
		||||
    | "splitting"
 | 
			
		||||
    | "applying_split"
 | 
			
		||||
    | "has_been_split"
 | 
			
		||||
    | "deleted" = "initial"
 | 
			
		||||
  // Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring
 | 
			
		||||
  let splitPoints = new UIEventSource<Feature<
 | 
			
		||||
    Point,
 | 
			
		||||
    {
 | 
			
		||||
      id: number
 | 
			
		||||
      index: number
 | 
			
		||||
      dist: number
 | 
			
		||||
      location: number
 | 
			
		||||
    }
 | 
			
		||||
  >[]>([])
 | 
			
		||||
  let splitpointsNotEmpty = splitPoints.map(sp => sp.length > 0)
 | 
			
		||||
  let splitPoints = new UIEventSource<
 | 
			
		||||
    Feature<
 | 
			
		||||
      Point,
 | 
			
		||||
      {
 | 
			
		||||
        id: number
 | 
			
		||||
        index: number
 | 
			
		||||
        dist: number
 | 
			
		||||
        location: number
 | 
			
		||||
      }
 | 
			
		||||
    >[]
 | 
			
		||||
  >([])
 | 
			
		||||
  let splitpointsNotEmpty = splitPoints.map((sp) => sp.length > 0)
 | 
			
		||||
 | 
			
		||||
  let osmWay: OsmWay
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -54,7 +61,7 @@
 | 
			
		|||
      {
 | 
			
		||||
        theme: state?.layout?.id,
 | 
			
		||||
      },
 | 
			
		||||
      5,
 | 
			
		||||
      5
 | 
			
		||||
    )
 | 
			
		||||
    await state.changes?.applyAction(splitAction)
 | 
			
		||||
    // We throw away the old map and splitpoints, and create a new map from scratch
 | 
			
		||||
| 
						 | 
				
			
			@ -64,10 +71,8 @@
 | 
			
		|||
    state.selectedElement?.setData(undefined)
 | 
			
		||||
    step = "has_been_split"
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<LoginToggle ignoreLoading={true} {state}>
 | 
			
		||||
  <Tr slot="not-logged-in" t={t.loginToSplit} />
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -75,38 +80,39 @@
 | 
			
		|||
    <!-- Empty -->
 | 
			
		||||
  {:else if step === "initial"}
 | 
			
		||||
    <button on:click={() => downloadWay()}>
 | 
			
		||||
      <Scissors class="w-6 h-6 shrink-0" />
 | 
			
		||||
      <Scissors class="h-6 w-6 shrink-0" />
 | 
			
		||||
      <Tr t={t.inviteToSplit} />
 | 
			
		||||
    </button>
 | 
			
		||||
  {:else if step === "loading_way"}
 | 
			
		||||
    <Loading />
 | 
			
		||||
 | 
			
		||||
  {:else if step === "splitting"}
 | 
			
		||||
    <div class="flex flex-col interactive border-interactive p-2">
 | 
			
		||||
      <div class="w-full h-80">
 | 
			
		||||
    <div class="interactive border-interactive flex flex-col p-2">
 | 
			
		||||
      <div class="h-80 w-full">
 | 
			
		||||
        <WaySplitMap {state} {splitPoints} {osmWay} />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="flex flex-wrap-reverse md:flex-nowrap w-full">
 | 
			
		||||
        <BackButton clss="w-full" on:click={() => {
 | 
			
		||||
          splitPoints.set([])
 | 
			
		||||
          step = "initial"
 | 
			
		||||
        }}>
 | 
			
		||||
      <div class="flex w-full flex-wrap-reverse md:flex-nowrap">
 | 
			
		||||
        <BackButton
 | 
			
		||||
          clss="w-full"
 | 
			
		||||
          on:click={() => {
 | 
			
		||||
            splitPoints.set([])
 | 
			
		||||
            step = "initial"
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Tr t={Translations.t.general.cancel} />
 | 
			
		||||
        </BackButton>
 | 
			
		||||
        <NextButton clss={ ($splitpointsNotEmpty ? "": "disabled ") + "w-full primary"} on:click={() => doSplit()}>
 | 
			
		||||
        <NextButton
 | 
			
		||||
          clss={($splitpointsNotEmpty ? "" : "disabled ") + "w-full primary"}
 | 
			
		||||
          on:click={() => doSplit()}
 | 
			
		||||
        >
 | 
			
		||||
          <Tr t={t.split} />
 | 
			
		||||
        </NextButton>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
  {:else if step === "has_been_split"}
 | 
			
		||||
    <Tr cls="thanks" t={   t.hasBeenSplit.Clone().SetClass("font-bold thanks block w-full")} />
 | 
			
		||||
    <Tr cls="thanks" t={t.hasBeenSplit.Clone().SetClass("font-bold thanks block w-full")} />
 | 
			
		||||
    <button on:click={() => downloadWay()}>
 | 
			
		||||
      <Scissors class="w-6 h-6" />
 | 
			
		||||
      <Scissors class="h-6 w-6" />
 | 
			
		||||
      <Tr t={t.splitAgain} />
 | 
			
		||||
    </button>
 | 
			
		||||
  {/if}
 | 
			
		||||
 | 
			
		||||
</LoginToggle>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -171,7 +171,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
 | 
			
		|||
        state: SpecialVisualizationState,
 | 
			
		||||
        tags: UIEventSource<Record<string, string>>,
 | 
			
		||||
        args: string[],
 | 
			
		||||
        feature: Feature,
 | 
			
		||||
        feature: Feature
 | 
			
		||||
    ): BaseUIElement {
 | 
			
		||||
        const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags)
 | 
			
		||||
        const msg = args[1]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -68,10 +68,14 @@
 | 
			
		|||
    },
 | 
			
		||||
    [skippedQuestions]
 | 
			
		||||
  )
 | 
			
		||||
  let firstQuestion: UIEventSource<TagRenderingConfig> = new UIEventSource<TagRenderingConfig>(undefined)
 | 
			
		||||
  let allQuestionsToAsk : UIEventSource<TagRenderingConfig[]> = new UIEventSource<TagRenderingConfig[]>([])
 | 
			
		||||
  let firstQuestion: UIEventSource<TagRenderingConfig> = new UIEventSource<TagRenderingConfig>(
 | 
			
		||||
    undefined
 | 
			
		||||
  )
 | 
			
		||||
  let allQuestionsToAsk: UIEventSource<TagRenderingConfig[]> = new UIEventSource<
 | 
			
		||||
    TagRenderingConfig[]
 | 
			
		||||
  >([])
 | 
			
		||||
 | 
			
		||||
  async function calculateQuestions(){
 | 
			
		||||
  async function calculateQuestions() {
 | 
			
		||||
    console.log("Applying questions to ask")
 | 
			
		||||
    const qta = questionsToAsk.data
 | 
			
		||||
    firstQuestion.setData(undefined)
 | 
			
		||||
| 
						 | 
				
			
			@ -81,12 +85,10 @@
 | 
			
		|||
    allQuestionsToAsk.setData(qta)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  onDestroy(questionsToAsk.addCallback(() =>calculateQuestions()))
 | 
			
		||||
  onDestroy(questionsToAsk.addCallback(() => calculateQuestions()))
 | 
			
		||||
  onDestroy(showAllQuestionsAtOnce.addCallback(() => calculateQuestions()))
 | 
			
		||||
  calculateQuestions()
 | 
			
		||||
 | 
			
		||||
  
 | 
			
		||||
  let answered: number = 0
 | 
			
		||||
  let skipped: number = 0
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -135,24 +135,25 @@
 | 
			
		|||
    // We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes
 | 
			
		||||
    initialize($tags, config)
 | 
			
		||||
  }
 | 
			
		||||
onDestroy(
 | 
			
		||||
  freeformInput.subscribe((freeformValue) => {
 | 
			
		||||
    if (!mappings || mappings?.length == 0 || config.freeform?.key === undefined) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    // If a freeform value is given, mark the 'mapping' as marked
 | 
			
		||||
    if (config.multiAnswer) {
 | 
			
		||||
      if (checkedMappings === undefined) {
 | 
			
		||||
        // Initialization didn't yet run
 | 
			
		||||
  onDestroy(
 | 
			
		||||
    freeformInput.subscribe((freeformValue) => {
 | 
			
		||||
      if (!mappings || mappings?.length == 0 || config.freeform?.key === undefined) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      checkedMappings[mappings.length] = freeformValue?.length > 0
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (freeformValue?.length > 0) {
 | 
			
		||||
      selectedMapping = mappings.length
 | 
			
		||||
    }
 | 
			
		||||
  }))
 | 
			
		||||
      // If a freeform value is given, mark the 'mapping' as marked
 | 
			
		||||
      if (config.multiAnswer) {
 | 
			
		||||
        if (checkedMappings === undefined) {
 | 
			
		||||
          // Initialization didn't yet run
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        checkedMappings[mappings.length] = freeformValue?.length > 0
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (freeformValue?.length > 0) {
 | 
			
		||||
        selectedMapping = mappings.length
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  $: {
 | 
			
		||||
    if (
 | 
			
		||||
| 
						 | 
				
			
			@ -243,7 +244,9 @@ onDestroy(
 | 
			
		|||
    <form
 | 
			
		||||
      class="interactive border-interactive relative flex flex-col overflow-y-auto px-2"
 | 
			
		||||
      style="max-height: 75vh"
 | 
			
		||||
      on:submit|preventDefault={() =>{ /*onSave(); This submit is not needed and triggers to early, causing bugs: see #1808*/}}
 | 
			
		||||
      on:submit|preventDefault={() => {
 | 
			
		||||
        /*onSave(); This submit is not needed and triggers to early, causing bugs: see #1808*/
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <legend>
 | 
			
		||||
| 
						 | 
				
			
			@ -399,7 +402,10 @@ onDestroy(
 | 
			
		|||
            <slot name="cancel" />
 | 
			
		||||
            <slot name="save-button" {selectedTags}>
 | 
			
		||||
              {#if allowDeleteOfFreeform && (mappings?.length ?? 0) === 0 && $freeformInput === undefined && $freeformInputUnvalidated === ""}
 | 
			
		||||
                <button class="primary flex" on:click|stopPropagation|preventDefault={() => onSave()}>
 | 
			
		||||
                <button
 | 
			
		||||
                  class="primary flex"
 | 
			
		||||
                  on:click|stopPropagation|preventDefault={() => onSave()}
 | 
			
		||||
                >
 | 
			
		||||
                  <TrashIcon class="h-6 w-6 text-red-500" />
 | 
			
		||||
                  <Tr t={Translations.t.general.eraseValue} />
 | 
			
		||||
                </button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,90 +1,93 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
    /**
 | 
			
		||||
     * Allows to import a 'mangrove' private key from backup
 | 
			
		||||
     */
 | 
			
		||||
    import LoginToggle from "../Base/LoginToggle.svelte"
 | 
			
		||||
    import FileSelector from "../Base/FileSelector.svelte"
 | 
			
		||||
    import type { SpecialVisualizationState } from "../SpecialVisualization"
 | 
			
		||||
    import Tr from "../Base/Tr.svelte"
 | 
			
		||||
    import Translations from "../i18n/Translations"
 | 
			
		||||
    import { UIEventSource } from "../../Logic/UIEventSource"
 | 
			
		||||
  /**
 | 
			
		||||
   * Allows to import a 'mangrove' private key from backup
 | 
			
		||||
   */
 | 
			
		||||
  import LoginToggle from "../Base/LoginToggle.svelte"
 | 
			
		||||
  import FileSelector from "../Base/FileSelector.svelte"
 | 
			
		||||
  import type { SpecialVisualizationState } from "../SpecialVisualization"
 | 
			
		||||
  import Tr from "../Base/Tr.svelte"
 | 
			
		||||
  import Translations from "../i18n/Translations"
 | 
			
		||||
  import { UIEventSource } from "../../Logic/UIEventSource"
 | 
			
		||||
 | 
			
		||||
    export let state: SpecialVisualizationState
 | 
			
		||||
    export let text: string
 | 
			
		||||
  export let state: SpecialVisualizationState
 | 
			
		||||
  export let text: string
 | 
			
		||||
 | 
			
		||||
    let error: string = undefined
 | 
			
		||||
    let success: string = undefined
 | 
			
		||||
  let error: string = undefined
 | 
			
		||||
  let success: string = undefined
 | 
			
		||||
 | 
			
		||||
    function importContent(str: string) {
 | 
			
		||||
        const parsed = JSON.parse(str)
 | 
			
		||||
  function importContent(str: string) {
 | 
			
		||||
    const parsed = JSON.parse(str)
 | 
			
		||||
 | 
			
		||||
        const format = {
 | 
			
		||||
            "crv": "P-256",
 | 
			
		||||
            "d": undefined,
 | 
			
		||||
            "ext": true,
 | 
			
		||||
            "key_ops": ["sign"],
 | 
			
		||||
            "kty": "EC",
 | 
			
		||||
            "x": undefined,
 | 
			
		||||
            "y": undefined,
 | 
			
		||||
            "metadata": "Mangrove private key",
 | 
			
		||||
        }
 | 
			
		||||
        const neededKeys = Object.keys(format)
 | 
			
		||||
        for (const neededKey of neededKeys) {
 | 
			
		||||
            const expected = format[neededKey]
 | 
			
		||||
            const actual = parsed[neededKey]
 | 
			
		||||
            if (actual === undefined) {
 | 
			
		||||
                error = "Not a valid key. The value for " + neededKey + " is missing"
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
            if (typeof expected === "string" && expected !== actual) {
 | 
			
		||||
                error = "Not a valid key. The value for " + neededKey + " should be " + expected + " but is " + actual
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        const current: UIEventSource<string> = state.userRelatedState.mangroveIdentity.mangroveIdentity
 | 
			
		||||
        const flattened = JSON.stringify(parsed, null, "")
 | 
			
		||||
        if (flattened === current.data) {
 | 
			
		||||
            success = "The imported key is identical to the existing key"
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        console.log("Got", flattened, current)
 | 
			
		||||
        current.setData(flattened)
 | 
			
		||||
        success = "Applied private key"
 | 
			
		||||
    const format = {
 | 
			
		||||
      crv: "P-256",
 | 
			
		||||
      d: undefined,
 | 
			
		||||
      ext: true,
 | 
			
		||||
      key_ops: ["sign"],
 | 
			
		||||
      kty: "EC",
 | 
			
		||||
      x: undefined,
 | 
			
		||||
      y: undefined,
 | 
			
		||||
      metadata: "Mangrove private key",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function onImport(files: FileList) {
 | 
			
		||||
        error = undefined
 | 
			
		||||
        success = undefined
 | 
			
		||||
        try {
 | 
			
		||||
            const reader = new FileReader()
 | 
			
		||||
            reader.readAsText(files[0], "UTF-8")
 | 
			
		||||
 | 
			
		||||
            // here we tell the reader what to do when it's done reading...
 | 
			
		||||
            const content = await new Promise<string>((resolve, reject) => {
 | 
			
		||||
                reader.onload = readerEvent => {
 | 
			
		||||
                    resolve(<string>readerEvent.target.result)
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            importContent(content)
 | 
			
		||||
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            error = e
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    const neededKeys = Object.keys(format)
 | 
			
		||||
    for (const neededKey of neededKeys) {
 | 
			
		||||
      const expected = format[neededKey]
 | 
			
		||||
      const actual = parsed[neededKey]
 | 
			
		||||
      if (actual === undefined) {
 | 
			
		||||
        error = "Not a valid key. The value for " + neededKey + " is missing"
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      if (typeof expected === "string" && expected !== actual) {
 | 
			
		||||
        error =
 | 
			
		||||
          "Not a valid key. The value for " +
 | 
			
		||||
          neededKey +
 | 
			
		||||
          " should be " +
 | 
			
		||||
          expected +
 | 
			
		||||
          " but is " +
 | 
			
		||||
          actual
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const current: UIEventSource<string> = state.userRelatedState.mangroveIdentity.mangroveIdentity
 | 
			
		||||
    const flattened = JSON.stringify(parsed, null, "")
 | 
			
		||||
    if (flattened === current.data) {
 | 
			
		||||
      success = "The imported key is identical to the existing key"
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    console.log("Got", flattened, current)
 | 
			
		||||
    current.setData(flattened)
 | 
			
		||||
    success = "Applied private key"
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function onImport(files: FileList) {
 | 
			
		||||
    error = undefined
 | 
			
		||||
    success = undefined
 | 
			
		||||
    try {
 | 
			
		||||
      const reader = new FileReader()
 | 
			
		||||
      reader.readAsText(files[0], "UTF-8")
 | 
			
		||||
 | 
			
		||||
      // here we tell the reader what to do when it's done reading...
 | 
			
		||||
      const content = await new Promise<string>((resolve, reject) => {
 | 
			
		||||
        reader.onload = (readerEvent) => {
 | 
			
		||||
          resolve(<string>readerEvent.target.result)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      importContent(content)
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      error = e
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<LoginToggle {state}>
 | 
			
		||||
  <div class="flex flex-col m-1">
 | 
			
		||||
 | 
			
		||||
    <FileSelector accept="application/json" multiple={false} on:submit={e => onImport(e.detail)}>
 | 
			
		||||
  <div class="m-1 flex flex-col">
 | 
			
		||||
    <FileSelector accept="application/json" multiple={false} on:submit={(e) => onImport(e.detail)}>
 | 
			
		||||
      {text}
 | 
			
		||||
    </FileSelector>
 | 
			
		||||
    {#if error}
 | 
			
		||||
      <div class="alert">
 | 
			
		||||
        <Tr t={Translations.t.general.error} /> {error}</div>
 | 
			
		||||
        <Tr t={Translations.t.general.error} />
 | 
			
		||||
        {error}
 | 
			
		||||
      </div>
 | 
			
		||||
    {/if}
 | 
			
		||||
    {#if success}
 | 
			
		||||
      <div class="thanks">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,12 +61,12 @@
 | 
			
		|||
      opinion: opinion.data,
 | 
			
		||||
      metadata: { nickname, is_affiliated: isAffiliated.data },
 | 
			
		||||
    }
 | 
			
		||||
      try {
 | 
			
		||||
        await reviews.createReview(review)
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.error("Could not create review due to", e)
 | 
			
		||||
        uploadFailed = "" + e
 | 
			
		||||
      }
 | 
			
		||||
    try {
 | 
			
		||||
      await reviews.createReview(review)
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error("Could not create review due to", e)
 | 
			
		||||
      uploadFailed = "" + e
 | 
			
		||||
    }
 | 
			
		||||
    _state = "done"
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,12 +42,13 @@
 | 
			
		|||
      {#if $allReviews?.length - $reviews?.length === 1}
 | 
			
		||||
        <Tr t={t.non_place_review} />
 | 
			
		||||
      {:else}
 | 
			
		||||
        <Tr t={t.non_place_reviews.Subs({n:$allReviews?.length - $reviews?.length })} />
 | 
			
		||||
        <Tr t={t.non_place_reviews.Subs({ n: $allReviews?.length - $reviews?.length })} />
 | 
			
		||||
      {/if}
 | 
			
		||||
      <a target="_blank"
 | 
			
		||||
         class="link-underline"
 | 
			
		||||
         rel="noopener nofollow"
 | 
			
		||||
         href={`https://mangrove.reviews/list?kid=${encodeURIComponent($kid)}`}
 | 
			
		||||
      <a
 | 
			
		||||
        target="_blank"
 | 
			
		||||
        class="link-underline"
 | 
			
		||||
        rel="noopener nofollow"
 | 
			
		||||
        href={`https://mangrove.reviews/list?kid=${encodeURIComponent($kid)}`}
 | 
			
		||||
      >
 | 
			
		||||
        <Tr t={t.see_all} />
 | 
			
		||||
      </a>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
				
			
			@ -42,7 +42,7 @@
 | 
			
		|||
  )
 | 
			
		||||
  let osmConnection = new OsmConnection({
 | 
			
		||||
    oauth_token,
 | 
			
		||||
    checkOnlineRegularly: true
 | 
			
		||||
    checkOnlineRegularly: true,
 | 
			
		||||
  })
 | 
			
		||||
  const expertMode = UIEventSource.asBoolean(
 | 
			
		||||
    osmConnection.GetPreference("studio-expert-mode", "false", {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,2 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@
 | 
			
		|||
    EyeIcon,
 | 
			
		||||
    HeartIcon,
 | 
			
		||||
    MenuIcon,
 | 
			
		||||
    XCircleIcon
 | 
			
		||||
    XCircleIcon,
 | 
			
		||||
  } from "@rgossiaux/svelte-heroicons/solid"
 | 
			
		||||
  import Tr from "./Base/Tr.svelte"
 | 
			
		||||
  import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
 | 
			
		||||
| 
						 | 
				
			
			@ -99,26 +99,31 @@
 | 
			
		|||
  })
 | 
			
		||||
 | 
			
		||||
  let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => {
 | 
			
		||||
      if (element.properties.id.startsWith("current_view")) {
 | 
			
		||||
        return currentViewLayer
 | 
			
		||||
      }
 | 
			
		||||
      if (element.properties.id === "new_point_dialog") {
 | 
			
		||||
        return layout.layers.find(l => l.id === "last_click")
 | 
			
		||||
      }
 | 
			
		||||
      if (element.properties.id === "location_track") {
 | 
			
		||||
        return layout.layers.find(l => l.id === "gps_track")
 | 
			
		||||
      }
 | 
			
		||||
      return state.layout.getMatchingLayer(element.properties)
 | 
			
		||||
    if (element.properties.id.startsWith("current_view")) {
 | 
			
		||||
      return currentViewLayer
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
    if (element.properties.id === "new_point_dialog") {
 | 
			
		||||
      return layout.layers.find((l) => l.id === "last_click")
 | 
			
		||||
    }
 | 
			
		||||
    if (element.properties.id === "location_track") {
 | 
			
		||||
      return layout.layers.find((l) => l.id === "gps_track")
 | 
			
		||||
    }
 | 
			
		||||
    return state.layout.getMatchingLayer(element.properties)
 | 
			
		||||
  })
 | 
			
		||||
  let currentZoom = state.mapProperties.zoom
 | 
			
		||||
  let showCrosshair = state.userRelatedState.showCrosshair
 | 
			
		||||
  let visualFeedback = state.visualFeedback
 | 
			
		||||
  let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined)
 | 
			
		||||
  let mapproperties: MapProperties = state.mapProperties
 | 
			
		||||
  state.mapProperties.installCustomKeyboardHandler(viewport)
 | 
			
		||||
  let canZoomIn = mapproperties.maxzoom.map(mz => mapproperties.zoom.data < mz, [mapproperties.zoom])
 | 
			
		||||
  let canZoomOut = mapproperties.minzoom.map(mz => mapproperties.zoom.data > mz, [mapproperties.zoom])
 | 
			
		||||
  let canZoomIn = mapproperties.maxzoom.map(
 | 
			
		||||
    (mz) => mapproperties.zoom.data < mz,
 | 
			
		||||
    [mapproperties.zoom]
 | 
			
		||||
  )
 | 
			
		||||
  let canZoomOut = mapproperties.minzoom.map(
 | 
			
		||||
    (mz) => mapproperties.zoom.data > mz,
 | 
			
		||||
    [mapproperties.zoom]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  function updateViewport() {
 | 
			
		||||
    const rect = viewport.data?.getBoundingClientRect()
 | 
			
		||||
| 
						 | 
				
			
			@ -133,7 +138,7 @@
 | 
			
		|||
    const bottomRight = mlmap.unproject([rect.right, rect.bottom])
 | 
			
		||||
    const bbox = new BBox([
 | 
			
		||||
      [topLeft.lng, topLeft.lat],
 | 
			
		||||
      [bottomRight.lng, bottomRight.lat]
 | 
			
		||||
      [bottomRight.lng, bottomRight.lat],
 | 
			
		||||
    ])
 | 
			
		||||
    state.visualFeedbackViewportBounds.setData(bbox)
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -149,7 +154,8 @@
 | 
			
		|||
  let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
 | 
			
		||||
  let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
 | 
			
		||||
  let rasterLayerName =
 | 
			
		||||
    rasterLayer.data?.properties?.name ?? AvailableRasterLayers.defaultBackgroundLayer.properties.name
 | 
			
		||||
    rasterLayer.data?.properties?.name ??
 | 
			
		||||
    AvailableRasterLayers.defaultBackgroundLayer.properties.name
 | 
			
		||||
  onDestroy(
 | 
			
		||||
    rasterLayer.addCallbackAndRunD((l) => {
 | 
			
		||||
      rasterLayerName = l.properties.name
 | 
			
		||||
| 
						 | 
				
			
			@ -224,7 +230,12 @@
 | 
			
		|||
      on:keydown={forwardEventToMap}
 | 
			
		||||
    >
 | 
			
		||||
      <div class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 md:mx-2">
 | 
			
		||||
        <img role="presentation" alt="" class="mr-0.5 block h-6 w-6 sm:mr-1 md:mr-2 md:h-8 md:w-8" src={layout.icon} />
 | 
			
		||||
        <img
 | 
			
		||||
          role="presentation"
 | 
			
		||||
          alt=""
 | 
			
		||||
          class="mr-0.5 block h-6 w-6 sm:mr-1 md:mr-2 md:h-8 md:w-8"
 | 
			
		||||
          src={layout.icon}
 | 
			
		||||
        />
 | 
			
		||||
        <b class="mr-1">
 | 
			
		||||
          <Tr t={layout.title} />
 | 
			
		||||
        </b>
 | 
			
		||||
| 
						 | 
				
			
			@ -385,8 +396,6 @@
 | 
			
		|||
  <svelte:fragment slot="error" />
 | 
			
		||||
</LoginToggle>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{#if $selectedElement !== undefined && $selectedLayer !== undefined && !$selectedLayer.popupInFloatover}
 | 
			
		||||
  <!-- right modal with the selected element view -->
 | 
			
		||||
  <ModalRight
 | 
			
		||||
| 
						 | 
				
			
			@ -409,10 +418,10 @@
 | 
			
		|||
      state.selectedElement.setData(undefined)
 | 
			
		||||
    }}
 | 
			
		||||
  >
 | 
			
		||||
    <div class="flex flex-col h-full w-full">
 | 
			
		||||
    <div class="flex h-full w-full flex-col">
 | 
			
		||||
      {#if $selectedLayer.popupInFloatover === "title"}
 | 
			
		||||
        <SelectedElementTitle {state} layer={$selectedLayer} selectedElement={$selectedElement} >
 | 
			
		||||
          <span slot="close-button"/>
 | 
			
		||||
        <SelectedElementTitle {state} layer={$selectedLayer} selectedElement={$selectedElement}>
 | 
			
		||||
          <span slot="close-button" />
 | 
			
		||||
        </SelectedElementTitle>
 | 
			
		||||
      {/if}
 | 
			
		||||
      <SelectedElementView {state} layer={$selectedLayer} selectedElement={$selectedElement} />
 | 
			
		||||
| 
						 | 
				
			
			@ -423,7 +432,7 @@
 | 
			
		|||
<If condition={state.previewedImage.map((i) => i !== undefined)}>
 | 
			
		||||
  <FloatOver extraClasses="p-1" on:close={() => state.previewedImage.setData(undefined)}>
 | 
			
		||||
    <button
 | 
			
		||||
      class="absolute p-0 right-4 top-4 h-8 w-8 rounded-full"
 | 
			
		||||
      class="absolute right-4 top-4 h-8 w-8 rounded-full p-0"
 | 
			
		||||
      on:click={() => previewedImage.setData(undefined)}
 | 
			
		||||
      slot="close-button"
 | 
			
		||||
    >
 | 
			
		||||
| 
						 | 
				
			
			@ -595,7 +604,7 @@
 | 
			
		|||
            highlightedRendering={state.guistate.highlightedUserSetting}
 | 
			
		||||
            selectedElement={{
 | 
			
		||||
              type: "Feature",
 | 
			
		||||
              properties: {id:"settings"},
 | 
			
		||||
              properties: { id: "settings" },
 | 
			
		||||
              geometry: { type: "Point", coordinates: [0, 0] },
 | 
			
		||||
            }}
 | 
			
		||||
            {state}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,10 @@ export default class WikidataSearchBox extends InputElement<string> {
 | 
			
		|||
        new Table(
 | 
			
		||||
            ["name", "doc"],
 | 
			
		||||
            [
 | 
			
		||||
                ["key", "the value of this tag will initialize search (default: name). This can be a ';'-separated list in which case every key will be inspected. The non-null value will be used as search"],
 | 
			
		||||
                [
 | 
			
		||||
                    "key",
 | 
			
		||||
                    "the value of this tag will initialize search (default: name). This can be a ';'-separated list in which case every key will be inspected. The non-null value will be used as search",
 | 
			
		||||
                ],
 | 
			
		||||
                [
 | 
			
		||||
                    "options",
 | 
			
		||||
                    new Combine([
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,7 +25,9 @@
 | 
			
		|||
{/if}
 | 
			
		||||
 | 
			
		||||
{#if $wikipediaDetails.wikidata}
 | 
			
		||||
  <ToSvelte construct={() => WikidataPreviewBox.WikidataResponsePreview($wikipediaDetails.wikidata)} />
 | 
			
		||||
  <ToSvelte
 | 
			
		||||
    construct={() => WikidataPreviewBox.WikidataResponsePreview($wikipediaDetails.wikidata)}
 | 
			
		||||
  />
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
{#if $wikipediaDetails.articleUrl}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										26
									
								
								src/Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										26
									
								
								src/Utils.ts
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -922,7 +922,10 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
 | 
			
		|||
        Utils.injectedDownloads[url] = data
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async download(url: string, headers?: Record<string, string>): Promise<string | undefined> {
 | 
			
		||||
    public static async download(
 | 
			
		||||
        url: string,
 | 
			
		||||
        headers?: Record<string, string>
 | 
			
		||||
    ): Promise<string | undefined> {
 | 
			
		||||
        const result = await Utils.downloadAdvanced(url, headers)
 | 
			
		||||
        if (result["error"] !== undefined) {
 | 
			
		||||
            throw result["error"]
 | 
			
		||||
| 
						 | 
				
			
			@ -975,7 +978,11 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
 | 
			
		|||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static upload(url: string, data: string | Blob, headers?: Record<string, string>): Promise<string> {
 | 
			
		||||
    public static upload(
 | 
			
		||||
        url: string,
 | 
			
		||||
        data: string | Blob,
 | 
			
		||||
        headers?: Record<string, string>
 | 
			
		||||
    ): Promise<string> {
 | 
			
		||||
        return new Promise((resolve, reject) => {
 | 
			
		||||
            const xhr = new XMLHttpRequest()
 | 
			
		||||
            xhr.onload = () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -1031,7 +1038,10 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
 | 
			
		|||
        return await promise
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async downloadJson(url: string, headers?: Record<string, string>): Promise<object | []> {
 | 
			
		||||
    public static async downloadJson(
 | 
			
		||||
        url: string,
 | 
			
		||||
        headers?: Record<string, string>
 | 
			
		||||
    ): Promise<object | []> {
 | 
			
		||||
        const result = await Utils.downloadJsonAdvanced(url, headers)
 | 
			
		||||
        if (result["content"]) {
 | 
			
		||||
            return result["content"]
 | 
			
		||||
| 
						 | 
				
			
			@ -1039,7 +1049,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
 | 
			
		|||
        throw result["error"]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static awaitAnimationFrame(): Promise<void>{
 | 
			
		||||
    public static awaitAnimationFrame(): Promise<void> {
 | 
			
		||||
        return new Promise<void>((resolve) => {
 | 
			
		||||
            window.requestAnimationFrame(() => {
 | 
			
		||||
                resolve()
 | 
			
		||||
| 
						 | 
				
			
			@ -1050,7 +1060,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
 | 
			
		|||
    public static async downloadJsonAdvanced(
 | 
			
		||||
        url: string,
 | 
			
		||||
        headers?: Record<string, string>
 | 
			
		||||
    ): Promise<{ content: object | object[] } | { error: string; url: string; statuscode?: number }> {
 | 
			
		||||
    ): Promise<
 | 
			
		||||
        { content: object | object[] } | { error: string; url: string; statuscode?: number }
 | 
			
		||||
    > {
 | 
			
		||||
        const injected = Utils.injectedDownloads[url]
 | 
			
		||||
        if (injected !== undefined) {
 | 
			
		||||
            console.log("Using injected resource for test for URL", url)
 | 
			
		||||
| 
						 | 
				
			
			@ -1066,8 +1078,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
 | 
			
		|||
        const data = result["content"]
 | 
			
		||||
        try {
 | 
			
		||||
            if (typeof data === "string") {
 | 
			
		||||
                if(data === ""){
 | 
			
		||||
                    return {content: {}}
 | 
			
		||||
                if (data === "") {
 | 
			
		||||
                    return { content: {} }
 | 
			
		||||
                }
 | 
			
		||||
                return { content: JSON.parse(data) }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,8 +65,10 @@ export class PngMapCreator {
 | 
			
		|||
 | 
			
		||||
            document.getElementById(freeComponentId).appendChild(div)
 | 
			
		||||
            const newZoom = settings.zoom.data + Math.log2(pixelRatio) - 1
 | 
			
		||||
            const rasterLayerProperties = settings.rasterLayer.data?.properties ?? AvailableRasterLayers.defaultBackgroundLayer.properties
 | 
			
		||||
            const style =  rasterLayerProperties?.style ?? rasterLayerProperties?.url
 | 
			
		||||
            const rasterLayerProperties =
 | 
			
		||||
                settings.rasterLayer.data?.properties ??
 | 
			
		||||
                AvailableRasterLayers.defaultBackgroundLayer.properties
 | 
			
		||||
            const style = rasterLayerProperties?.style ?? rasterLayerProperties?.url
 | 
			
		||||
            const mapElem = new MlMap({
 | 
			
		||||
                container: div.id,
 | 
			
		||||
                style,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,11 @@
 | 
			
		|||
{
 | 
			
		||||
  "contributors": [
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 6880,
 | 
			
		||||
      "commits": 7248,
 | 
			
		||||
      "contributor": "Pieter Vander Vennet"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 431,
 | 
			
		||||
      "commits": 451,
 | 
			
		||||
      "contributor": "Robin van der Linde"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +16,10 @@
 | 
			
		|||
      "commits": 38,
 | 
			
		||||
      "contributor": "Win Olario"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 34,
 | 
			
		||||
      "contributor": "dependabot[bot]"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 33,
 | 
			
		||||
      "contributor": "Hosted Weblate"
 | 
			
		||||
| 
						 | 
				
			
			@ -40,10 +44,6 @@
 | 
			
		|||
      "commits": 29,
 | 
			
		||||
      "contributor": "riQQ"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 27,
 | 
			
		||||
      "contributor": "dependabot[bot]"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 26,
 | 
			
		||||
      "contributor": "Joost"
 | 
			
		||||
| 
						 | 
				
			
			@ -116,6 +116,10 @@
 | 
			
		|||
      "commits": 10,
 | 
			
		||||
      "contributor": "LiamSimons"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 9,
 | 
			
		||||
      "contributor": "Flo Edelmann"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 9,
 | 
			
		||||
      "contributor": "Codain"
 | 
			
		||||
| 
						 | 
				
			
			@ -134,11 +138,11 @@
 | 
			
		|||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 7,
 | 
			
		||||
      "contributor": "OliNau"
 | 
			
		||||
      "contributor": "danieldegroot2"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 7,
 | 
			
		||||
      "contributor": "Flo Edelmann"
 | 
			
		||||
      "contributor": "OliNau"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 7,
 | 
			
		||||
| 
						 | 
				
			
			@ -148,10 +152,6 @@
 | 
			
		|||
      "commits": 6,
 | 
			
		||||
      "contributor": "David Haberthür"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 6,
 | 
			
		||||
      "contributor": "danieldegroot2"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 4,
 | 
			
		||||
      "contributor": "Daniele Santini"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,9 +50,6 @@
 | 
			
		|||
    "de",
 | 
			
		||||
    "nl"
 | 
			
		||||
  ],
 | 
			
		||||
  "BF": [
 | 
			
		||||
    "fr"
 | 
			
		||||
  ],
 | 
			
		||||
  "BG": [
 | 
			
		||||
    "bg"
 | 
			
		||||
  ],
 | 
			
		||||
| 
						 | 
				
			
			@ -149,6 +146,7 @@
 | 
			
		|||
    "el"
 | 
			
		||||
  ],
 | 
			
		||||
  "CZ": [
 | 
			
		||||
    "cs",
 | 
			
		||||
    "cs"
 | 
			
		||||
  ],
 | 
			
		||||
  "DE": [
 | 
			
		||||
| 
						 | 
				
			
			@ -215,7 +213,6 @@
 | 
			
		|||
    "fr"
 | 
			
		||||
  ],
 | 
			
		||||
  "GB": [
 | 
			
		||||
    "en",
 | 
			
		||||
    "en",
 | 
			
		||||
    "en"
 | 
			
		||||
  ],
 | 
			
		||||
| 
						 | 
				
			
			@ -271,8 +268,7 @@
 | 
			
		|||
    "hu"
 | 
			
		||||
  ],
 | 
			
		||||
  "ID": [
 | 
			
		||||
    "id",
 | 
			
		||||
    "jv"
 | 
			
		||||
    "id"
 | 
			
		||||
  ],
 | 
			
		||||
  "IE": [
 | 
			
		||||
    "en",
 | 
			
		||||
| 
						 | 
				
			
			@ -367,8 +363,8 @@
 | 
			
		|||
    "en"
 | 
			
		||||
  ],
 | 
			
		||||
  "LS": [
 | 
			
		||||
    "en",
 | 
			
		||||
    "st"
 | 
			
		||||
    "st",
 | 
			
		||||
    "en"
 | 
			
		||||
  ],
 | 
			
		||||
  "LT": [
 | 
			
		||||
    "lt",
 | 
			
		||||
| 
						 | 
				
			
			@ -434,8 +430,8 @@
 | 
			
		|||
    "dv"
 | 
			
		||||
  ],
 | 
			
		||||
  "MW": [
 | 
			
		||||
    "ny",
 | 
			
		||||
    "en"
 | 
			
		||||
    "en",
 | 
			
		||||
    "ny"
 | 
			
		||||
  ],
 | 
			
		||||
  "MX": [
 | 
			
		||||
    "es",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3664,7 +3664,6 @@
 | 
			
		|||
    "_meta": {
 | 
			
		||||
      "countries": [
 | 
			
		||||
        "BE",
 | 
			
		||||
        "BF",
 | 
			
		||||
        "BI",
 | 
			
		||||
        "BJ",
 | 
			
		||||
        "CA",
 | 
			
		||||
| 
						 | 
				
			
			@ -5229,9 +5228,6 @@
 | 
			
		|||
    "zh_Hans": "爪哇语",
 | 
			
		||||
    "zh_Hant": "爪哇語",
 | 
			
		||||
    "_meta": {
 | 
			
		||||
      "countries": [
 | 
			
		||||
        "ID"
 | 
			
		||||
      ],
 | 
			
		||||
      "dir": [
 | 
			
		||||
        "left-to-right",
 | 
			
		||||
        "right-to-left"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,11 @@
 | 
			
		|||
{
 | 
			
		||||
  "contributors": [
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 335,
 | 
			
		||||
      "commits": 337,
 | 
			
		||||
      "contributor": "kjon"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 309,
 | 
			
		||||
      "commits": 336,
 | 
			
		||||
      "contributor": "Pieter Vander Vennet"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -24,34 +24,34 @@
 | 
			
		|||
      "commits": 61,
 | 
			
		||||
      "contributor": "danieldegroot2"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 49,
 | 
			
		||||
      "contributor": "mcliquid"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 47,
 | 
			
		||||
      "contributor": "Anonymous"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 42,
 | 
			
		||||
      "contributor": "Supaplex"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 36,
 | 
			
		||||
      "contributor": "Iago"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 33,
 | 
			
		||||
      "contributor": "mcliquid"
 | 
			
		||||
      "commits": 34,
 | 
			
		||||
      "contributor": "Lucas"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 33,
 | 
			
		||||
      "contributor": "Jiří Podhorecký"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 32,
 | 
			
		||||
      "contributor": "Lucas"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 32,
 | 
			
		||||
      "contributor": "Babos Gábor"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 31,
 | 
			
		||||
      "contributor": "Supaplex"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 29,
 | 
			
		||||
      "contributor": "Artem"
 | 
			
		||||
| 
						 | 
				
			
			@ -72,6 +72,14 @@
 | 
			
		|||
      "commits": 18,
 | 
			
		||||
      "contributor": "el_libre como el chaval"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 17,
 | 
			
		||||
      "contributor": "Štefan Baebler"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 16,
 | 
			
		||||
      "contributor": "gallegonovato"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 15,
 | 
			
		||||
      "contributor": "macpac"
 | 
			
		||||
| 
						 | 
				
			
			@ -80,10 +88,6 @@
 | 
			
		|||
      "commits": 15,
 | 
			
		||||
      "contributor": "WaldiS"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 14,
 | 
			
		||||
      "contributor": "gallegonovato"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 14,
 | 
			
		||||
      "contributor": "LeJun"
 | 
			
		||||
| 
						 | 
				
			
			@ -144,6 +148,10 @@
 | 
			
		|||
      "commits": 10,
 | 
			
		||||
      "contributor": "Irina"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 9,
 | 
			
		||||
      "contributor": "Lasse Liehu"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 9,
 | 
			
		||||
      "contributor": "快乐的老鼠宝宝"
 | 
			
		||||
| 
						 | 
				
			
			@ -168,10 +176,6 @@
 | 
			
		|||
      "commits": 8,
 | 
			
		||||
      "contributor": "Vinicius"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 7,
 | 
			
		||||
      "contributor": "Lasse Liehu"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 7,
 | 
			
		||||
      "contributor": "NetworkedPoncho"
 | 
			
		||||
| 
						 | 
				
			
			@ -188,6 +192,10 @@
 | 
			
		|||
      "commits": 7,
 | 
			
		||||
      "contributor": "Niels Elgaard Larsen"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 6,
 | 
			
		||||
      "contributor": "Jeff Huang"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 6,
 | 
			
		||||
      "contributor": "Juele juele"
 | 
			
		||||
| 
						 | 
				
			
			@ -212,10 +220,6 @@
 | 
			
		|||
      "commits": 6,
 | 
			
		||||
      "contributor": "ⵣⵓⵀⵉⵔ ⴰⵎⴰⵣⵉⵖ زهير أمازيغ"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 6,
 | 
			
		||||
      "contributor": "Štefan Baebler"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 6,
 | 
			
		||||
      "contributor": "seppesantens"
 | 
			
		||||
| 
						 | 
				
			
			@ -232,6 +236,10 @@
 | 
			
		|||
      "commits": 6,
 | 
			
		||||
      "contributor": "lvgx"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 5,
 | 
			
		||||
      "contributor": "Emory Shaw"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 5,
 | 
			
		||||
      "contributor": "nilocram"
 | 
			
		||||
| 
						 | 
				
			
			@ -272,10 +280,6 @@
 | 
			
		|||
      "commits": 4,
 | 
			
		||||
      "contributor": "Krzysztof Chorzempa"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 4,
 | 
			
		||||
      "contributor": "Emory Shaw"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 4,
 | 
			
		||||
      "contributor": "André Marcelo Alvarenga"
 | 
			
		||||
| 
						 | 
				
			
			@ -288,10 +292,6 @@
 | 
			
		|||
      "commits": 4,
 | 
			
		||||
      "contributor": "Hiroshi Miura"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 4,
 | 
			
		||||
      "contributor": "Jeff Huang"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 4,
 | 
			
		||||
      "contributor": "Adolfo Jayme Barrientos"
 | 
			
		||||
| 
						 | 
				
			
			@ -380,6 +380,10 @@
 | 
			
		|||
      "commits": 3,
 | 
			
		||||
      "contributor": "SiegbjornSitumeang"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 2,
 | 
			
		||||
      "contributor": "CaldeiraG"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 2,
 | 
			
		||||
      "contributor": "Henry00572"
 | 
			
		||||
| 
						 | 
				
			
			@ -490,7 +494,11 @@
 | 
			
		|||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 1,
 | 
			
		||||
      "contributor": "CaldeiraG"
 | 
			
		||||
      "contributor": "Roosa Huovinen"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 1,
 | 
			
		||||
      "contributor": "Berete Baba"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "commits": 1,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										25
									
								
								src/index.ts
									
										
									
									
									
								
							
							
						
						
									
										25
									
								
								src/index.ts
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -31,7 +31,7 @@ async function getAvailableLayers(): Promise<Set<string>> {
 | 
			
		|||
        const host = new URL(Constants.VectorTileServer).host
 | 
			
		||||
        const status: { layers: string[] } = await Promise.any([
 | 
			
		||||
            // Utils.downloadJson("https://" + host + "/summary/status.json"),
 | 
			
		||||
            timeout(0)
 | 
			
		||||
            timeout(0),
 | 
			
		||||
        ])
 | 
			
		||||
        return new Set<string>(status.layers)
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
| 
						 | 
				
			
			@ -48,13 +48,13 @@ async function main() {
 | 
			
		|||
        }
 | 
			
		||||
        const [layout, availableLayers] = await Promise.all([
 | 
			
		||||
            DetermineLayout.GetLayout(),
 | 
			
		||||
            await getAvailableLayers()
 | 
			
		||||
            await getAvailableLayers(),
 | 
			
		||||
        ])
 | 
			
		||||
        console.log("The available layers on server are", Array.from(availableLayers))
 | 
			
		||||
        const state = new ThemeViewState(layout, availableLayers)
 | 
			
		||||
        const main = new SvelteUIElement(ThemeViewGUI, { state })
 | 
			
		||||
        main.AttachTo("maindiv")
 | 
			
		||||
        Array.from(document.getElementsByClassName("delete-on-load")).forEach(el => {
 | 
			
		||||
        Array.from(document.getElementsByClassName("delete-on-load")).forEach((el) => {
 | 
			
		||||
            el.parentElement.removeChild(el)
 | 
			
		||||
        })
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
| 
						 | 
				
			
			@ -65,17 +65,16 @@ async function main() {
 | 
			
		|||
 | 
			
		||||
            customDefinition?.length > 0
 | 
			
		||||
                ? new SubtleButton(new SvelteUIElement(Download), "Download the raw file").onClick(
 | 
			
		||||
                    () =>
 | 
			
		||||
                        Utils.offerContentsAsDownloadableFile(
 | 
			
		||||
                            DetermineLayout.getCustomDefinition(),
 | 
			
		||||
                            "mapcomplete-theme.json",
 | 
			
		||||
                            { mimetype: "application/json" }
 | 
			
		||||
                        )
 | 
			
		||||
                )
 | 
			
		||||
                : undefined
 | 
			
		||||
                      () =>
 | 
			
		||||
                          Utils.offerContentsAsDownloadableFile(
 | 
			
		||||
                              DetermineLayout.getCustomDefinition(),
 | 
			
		||||
                              "mapcomplete-theme.json",
 | 
			
		||||
                              { mimetype: "application/json" }
 | 
			
		||||
                          )
 | 
			
		||||
                  )
 | 
			
		||||
                : undefined,
 | 
			
		||||
        ]).AttachTo("maindiv")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main().then((_) => {
 | 
			
		||||
})
 | 
			
		||||
main().then((_) => {})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,4 +2,3 @@ import SvelteUIElement from "./UI/Base/SvelteUIElement"
 | 
			
		|||
import Test from "./UI/Test.svelte"
 | 
			
		||||
 | 
			
		||||
new SvelteUIElement(Test).AttachTo("maindiv")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue