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