chore: automated housekeeping...

This commit is contained in:
Pieter Vander Vennet 2024-10-19 14:44:55 +02:00
parent c9ce29f206
commit 40e894df8b
294 changed files with 14209 additions and 4192 deletions

View file

@ -5,10 +5,7 @@ import { Feature } from "geojson"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
export default class TitleHandler {
constructor(
selectedElement: Store<Feature>,
state: SpecialVisualizationState
) {
constructor(selectedElement: Store<Feature>, state: SpecialVisualizationState) {
const currentTitle: Store<string> = selectedElement.map(
(selected) => {
const lng = Locale.language.data
@ -34,7 +31,6 @@ export default class TitleHandler {
const el = document.createElement("span")
el.innerHTML = title
return el.textContent + " | " + defaultTitle
},
[Locale.language]
)

View file

@ -6,7 +6,7 @@ import { Feature, Polygon } from "geojson"
export class BBox {
static global: BBox = new BBox([
[-180, -90],
[180, 90]
[180, 90],
])
readonly maxLat: number
readonly maxLon: number
@ -53,7 +53,7 @@ export class BBox {
static fromLeafletBounds(bounds) {
return new BBox([
[bounds.getWest(), bounds.getNorth()],
[bounds.getEast(), bounds.getSouth()]
[bounds.getEast(), bounds.getSouth()],
])
}
@ -74,7 +74,7 @@ export class BBox {
// Note: x is longitude
f["bbox"] = new BBox([
[minX, minY],
[maxX, maxY]
[maxX, maxY],
])
}
return f["bbox"]
@ -94,7 +94,7 @@ export class BBox {
}
return new BBox([
[maxLon, maxLat],
[minLon, minLat]
[minLon, minLat],
])
}
@ -121,7 +121,7 @@ export class BBox {
public unionWith(other: BBox) {
return new BBox([
[Math.max(this.maxLon, other.maxLon), Math.max(this.maxLat, other.maxLat)],
[Math.min(this.minLon, other.minLon), Math.min(this.minLat, other.minLat)]
[Math.min(this.minLon, other.minLon), Math.min(this.minLat, other.minLat)],
])
}
@ -174,7 +174,7 @@ export class BBox {
return new BBox([
[lon - s / 2, lat - s / 2],
[lon + s / 2, lat + s / 2]
[lon + s / 2, lat + s / 2],
])
}
@ -231,29 +231,26 @@ export class BBox {
const lonDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor)
return new BBox([
[this.minLon - lonDiff, this.minLat - latDiff],
[this.maxLon + lonDiff, this.maxLat + latDiff]
[this.maxLon + lonDiff, this.maxLat + latDiff],
])
}
padAbsolute(degrees: number): BBox {
return new BBox([
[this.minLon - degrees, this.minLat - degrees],
[this.maxLon + degrees, this.maxLat + degrees]
[this.maxLon + degrees, this.maxLat + degrees],
])
}
toLngLat(): [[number, number], [number, number]] {
return [
[this.minLon, this.minLat],
[this.maxLon, this.maxLat]
[this.maxLon, this.maxLat],
]
}
toLngLatFlat(): [number, number, number, number] {
return [
this.minLon, this.minLat,
this.maxLon, this.maxLat,
]
return [this.minLon, this.minLat, this.maxLon, this.maxLat]
}
public asGeojsonCached() {
@ -267,7 +264,7 @@ export class BBox {
return {
type: "Feature",
properties: properties,
geometry: this.asGeometry()
geometry: this.asGeometry(),
}
}
@ -280,9 +277,9 @@ export class BBox {
[this.maxLon, this.minLat],
[this.maxLon, this.maxLat],
[this.minLon, this.maxLat],
[this.minLon, this.minLat]
]
]
[this.minLon, this.minLat],
],
],
}
}
@ -309,7 +306,7 @@ export class BBox {
minLon,
maxLon,
minLat,
maxLat
maxLat,
}
}

View file

@ -109,7 +109,7 @@ export default class DetermineTheme {
const dict = new Map<string, QuestionableTagRenderingConfigJson>()
for (const tagRendering of questions.tagRenderings) {
dict.set(tagRendering.id, <QuestionableTagRenderingConfigJson> tagRendering)
dict.set(tagRendering.id, <QuestionableTagRenderingConfigJson>tagRendering)
}
return dict

View file

@ -9,7 +9,8 @@ import { OsmFeature } from "../../../Models/OsmFeature"
* If multiple sources contain the same object (as determined by 'id'), only one copy of them is retained
*/
export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSource>
implements IndexedFeatureSource {
implements IndexedFeatureSource
{
public features: UIEventSource<Feature[]> = new UIEventSource([])
public readonly featuresById: Store<Map<string, Feature>>
protected readonly _featuresById: UIEventSource<Map<string, Feature>>
@ -118,10 +119,11 @@ export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSour
}
export class UpdatableFeatureSourceMerger<
Src extends UpdatableFeatureSource = UpdatableFeatureSource
>
Src extends UpdatableFeatureSource = UpdatableFeatureSource
>
extends FeatureSourceMerger<Src>
implements IndexedFeatureSource, UpdatableFeatureSource {
implements IndexedFeatureSource, UpdatableFeatureSource
{
constructor(...sources: Src[]) {
super(...sources)
}

View file

@ -4,7 +4,6 @@ import { Store, UIEventSource } from "../../UIEventSource"
import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource"
import { MvtToGeojson } from "mvt-to-geojson"
export default class MvtSource implements FeatureSourceForTile, UpdatableFeatureSource {
public readonly features: Store<GeojsonFeature<Geometry, { [name: string]: any }>[]>
public readonly x: number
@ -28,7 +27,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
y: number,
z: number,
layerName?: string,
isActive?: Store<boolean>,
isActive?: Store<boolean>
) {
this._url = url
this._layerName = layerName
@ -43,7 +42,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
}
return fs
},
[isActive],
[isActive]
)
}
@ -54,7 +53,6 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
await this.currentlyRunning
}
private async download(): Promise<void> {
try {
const result = await fetch(this._url)
@ -66,7 +64,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
const features = MvtToGeojson.fromBuffer(buffer, this.x, this.y, this.z)
for (const feature of features) {
const properties = feature.properties
if(!properties["osm_type"]){
if (!properties["osm_type"]) {
continue
}
let type: string = "node"
@ -90,6 +88,4 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
console.error("Could not download MVT " + this._url + " tile due to", e)
}
}
}

View file

@ -23,8 +23,7 @@ export class SummaryTileSourceRewriter implements FeatureSource {
filteredLayers: ReadonlyMap<string, FilteredLayer>
) {
this.filteredLayers = Array.from(filteredLayers.values()).filter(
(l) =>
Constants.priviliged_layers.indexOf(<any>l.layerDef.id) < 0
(l) => Constants.priviliged_layers.indexOf(<any>l.layerDef.id) < 0
)
this._summarySource = summarySource
filteredLayers.forEach((v) => {

View file

@ -95,8 +95,14 @@ export class GeoOperations {
/**
* Starting on `from`, travels `distance` meters in the direction of the `bearing` (default: 90)
*/
static destination(from: Coord | [number,number],distance: number, bearing: number = 90): [number,number]{
return <[number,number]> turf.destination(from, distance, bearing, {units: "meters"}).geometry.coordinates
static destination(
from: Coord | [number, number],
distance: number,
bearing: number = 90
): [number, number] {
return <[number, number]>(
turf.destination(from, distance, bearing, { units: "meters" }).geometry.coordinates
)
}
static convexHull(featureCollection, options: { concavity?: number }) {
@ -928,13 +934,13 @@ export class GeoOperations {
if (meters === undefined) {
return ""
}
meters = Utils.roundHuman( Math.round(meters))
meters = Utils.roundHuman(Math.round(meters))
if (meters < 1000) {
return Utils.roundHuman(meters) + "m"
}
if (meters >= 10000) {
const km = Utils.roundHuman(Math.round(meters / 1000))
const km = Utils.roundHuman(Math.round(meters / 1000))
return km + "km"
}

View file

@ -34,10 +34,10 @@ export default class AllImageProviders {
AllImageProviders.genericImageProvider,
]
public static apiUrls: string[] = [].concat(
...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls()),
...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls())
)
public static defaultKeys = [].concat(
AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes),
AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes)
)
private static providersByName = {
imgur: Imgur.singleton,
@ -71,13 +71,12 @@ export default class AllImageProviders {
*/
public static LoadImagesFor(
tags: Store<Record<string, string>>,
tagKey?: string[],
tagKey?: string[]
): Store<ProvidedImage[]> {
if (tags?.data?.id === undefined) {
return undefined
}
const source = new UIEventSource([])
const allSources: Store<ProvidedImage[]>[] = []
for (const imageProvider of AllImageProviders.ImageAttributionSource) {
@ -86,11 +85,11 @@ export default class AllImageProviders {
However, we override them if a custom image tag is set, e.g. 'image:menu'
*/
const prefixes = tagKey ?? imageProvider.defaultKeyPrefixes
const singleSource = tags.bindD(tags => imageProvider.getRelevantUrls(tags, prefixes))
const singleSource = tags.bindD((tags) => imageProvider.getRelevantUrls(tags, prefixes))
allSources.push(singleSource)
singleSource.addCallbackAndRunD((_) => {
const all: ProvidedImage[] = [].concat(...allSources.map((source) => source.data))
const dedup = Utils.DedupOnId(all, i => i?.id ?? i?.url)
const dedup = Utils.DedupOnId(all, (i) => i?.id ?? i?.url)
source.set(dedup)
})
}
@ -103,7 +102,7 @@ export default class AllImageProviders {
*/
public static loadImagesFrom(urls: string[]): Store<ProvidedImage[]> {
const tags = {
id:"na"
id: "na",
}
for (let i = 0; i < urls.length; i++) {
const url = urls[i]

View file

@ -27,12 +27,14 @@ export default class GenericImageProvider extends ImageProvider {
return undefined
}
return [{
key: key,
url: value,
provider: this,
id: value,
}]
return [
{
key: key,
url: value,
provider: this,
id: value,
},
]
}
SourceIcon() {

View file

@ -9,15 +9,15 @@ export interface ProvidedImage {
key: string
provider: ImageProvider
id: string
date?: Date,
date?: Date
status?: string | "ready"
/**
* Compass angle of the taken image
* 0 = north, 90° = East
*/
rotation?: number
lat?: number,
lon?: number,
lat?: number
lon?: number
host?: string
}
@ -26,8 +26,10 @@ export default abstract class ImageProvider {
public abstract readonly name: string
public abstract SourceIcon(img?: {id: string, url: string, host?: string}, location?: { lon: number; lat: number }): BaseUIElement
public abstract SourceIcon(
img?: { id: string; url: string; host?: string },
location?: { lon: number; lat: number }
): BaseUIElement
/**
* Gets all the relevant URLS for the given tags and for the given prefixes;
@ -35,12 +37,19 @@ export default abstract class ImageProvider {
* @param tags
* @param prefixes
*/
public async getRelevantUrlsFor(tags: Record<string, string>, prefixes: string[]): Promise<ProvidedImage[]> {
public async getRelevantUrlsFor(
tags: Record<string, string>,
prefixes: string[]
): Promise<ProvidedImage[]> {
const relevantUrls: ProvidedImage[] = []
const seenValues = new Set<string>()
for (const key in tags) {
if (!prefixes.some((prefix) => key === prefix || key.match(new RegExp(prefix+":[0-9]+")))) {
if (
!prefixes.some(
(prefix) => key === prefix || key.match(new RegExp(prefix + ":[0-9]+"))
)
) {
continue
}
const values = Utils.NoEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? [])
@ -50,10 +59,10 @@ export default abstract class ImageProvider {
}
seenValues.add(value)
let images = this.ExtractUrls(key, value)
if(!Array.isArray(images)){
images = await images
if (!Array.isArray(images)) {
images = await images
}
if(images){
if (images) {
relevantUrls.push(...images)
}
}
@ -61,12 +70,17 @@ export default abstract class ImageProvider {
return relevantUrls
}
public getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
public getRelevantUrls(
tags: Record<string, string>,
prefixes: string[]
): Store<ProvidedImage[]> {
return Stores.FromPromise(this.getRelevantUrlsFor(tags, prefixes))
}
public abstract ExtractUrls(key: string, value: string): undefined | ProvidedImage[] | Promise<ProvidedImage[]>
public abstract ExtractUrls(
key: string,
value: string
): undefined | ProvidedImage[] | Promise<ProvidedImage[]>
public abstract DownloadAttribution(providedImage: {
url: string

View file

@ -28,7 +28,10 @@ export class ImageUploadManager {
private readonly _osmConnection: OsmConnection
private readonly _changes: Changes
public readonly isUploading: Store<boolean>
private readonly _reportError: (message: (string | Error | XMLHttpRequest), extramessage?: string) => Promise<void>
private readonly _reportError: (
message: string | Error | XMLHttpRequest,
extramessage?: string
) => Promise<void>
constructor(
layout: ThemeConfig,
@ -38,7 +41,10 @@ export class ImageUploadManager {
changes: Changes,
gpsLocation: Store<GeolocationCoordinates | undefined>,
allFeatures: IndexedFeatureSource,
reportError: (message: string | Error | XMLHttpRequest, extramessage?: string ) => Promise<void>
reportError: (
message: string | Error | XMLHttpRequest,
extramessage?: string
) => Promise<void>
) {
this._uploader = uploader
this._featureProperties = featureProperties
@ -56,7 +62,7 @@ export class ImageUploadManager {
(startedCount) => {
return startedCount > failed.data + done.data
},
[failed, done],
[failed, done]
)
}
@ -105,7 +111,7 @@ export class ImageUploadManager {
file: File,
tagsStore: UIEventSource<OsmTags>,
targetKey: string,
noblur: boolean,
noblur: boolean
): Promise<void> {
const canBeUploaded = this.canBeUploaded(file)
if (canBeUploaded !== true) {
@ -130,10 +136,16 @@ export class ImageUploadManager {
}
const properties = this._featureProperties.getStore(featureId)
const action = new LinkImageAction(featureId, uploadResult. key, uploadResult . value, properties, {
theme: tags?.data?.["_orig_theme"] ?? this._theme.id,
changeType: "add-image",
})
const action = new LinkImageAction(
featureId,
uploadResult.key,
uploadResult.value,
properties,
{
theme: tags?.data?.["_orig_theme"] ?? this._theme.id,
changeType: "add-image",
}
)
await this._changes.applyAction(action)
}
@ -153,34 +165,51 @@ export class ImageUploadManager {
if (this._gps.data) {
location = [this._gps.data.longitude, this._gps.data.latitude]
}
if (location === undefined || location?.some(l => l === undefined)) {
if (location === undefined || location?.some((l) => l === undefined)) {
const feature = this._indexedFeatures.featuresById.data.get(featureId)
location = GeoOperations.centerpointCoordinates(feature)
}
try {
({ key, value, absoluteUrl } = await this._uploader.uploadImage(blob, location, author, noblur))
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(
blob,
location,
author,
noblur
))
} catch (e) {
this.increaseCountFor(this._uploadRetried, featureId)
console.error("Could not upload image, trying again:", e)
try {
({ key, value , absoluteUrl} = await this._uploader.uploadImage(blob, location, author, noblur))
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(
blob,
location,
author,
noblur
))
this.increaseCountFor(this._uploadRetriedSuccess, featureId)
} catch (e) {
console.error("Could again not upload image due to", e)
this.increaseCountFor(this._uploadFailed, featureId)
await this._reportError(e, JSON.stringify({ctx:"While uploading an image in the Image Upload Manager", featureId, author, targetKey}))
await this._reportError(
e,
JSON.stringify({
ctx: "While uploading an image in the Image Upload Manager",
featureId,
author,
targetKey,
})
)
return undefined
}
}
console.log("Uploading image done, creating action for", featureId)
key = targetKey ?? key
if(targetKey){
if (targetKey) {
// This is a non-standard key, so we use the image link directly
value = absoluteUrl
}
this.increaseCountFor(this._uploadFinished, featureId)
return {key, absoluteUrl, value}
return { key, absoluteUrl, value }
}
private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {

View file

@ -6,10 +6,14 @@ export interface ImageUploader {
*/
uploadImage(
blob: File,
currentGps: [number,number],
currentGps: [number, number],
author: string,
noblur: boolean
): Promise<UploadResult>
}
export interface UploadResult{ key: string; value: string, absoluteUrl: string }
export interface UploadResult {
key: string
value: string
absoluteUrl: string
}

View file

@ -19,7 +19,6 @@ export class Imgur extends ImageProvider {
return [Imgur.apiUrl]
}
SourceIcon(): BaseUIElement {
return undefined
}
@ -32,7 +31,7 @@ export class Imgur extends ImageProvider {
key: key,
provider: this,
id: value,
}
},
]
}
return undefined

View file

@ -118,7 +118,7 @@ export class Mapillary extends ImageProvider {
}
SourceIcon(
img: {id: string, url: string},
img: { id: string; url: string },
location?: {
lon: number
lat: number
@ -182,7 +182,7 @@ export class Mapillary extends ImageProvider {
key,
rotation,
lat: geometry.coordinates[1],
lon: geometry.coordinates[0]
lon: geometry.coordinates[0],
}
}
}

View file

@ -11,27 +11,35 @@ import SvelteUIElement from "../../UI/Base/SvelteUIElement"
import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte"
import Link from "../../UI/Base/Link"
export default class PanoramaxImageProvider extends ImageProvider {
public static readonly singleton = new PanoramaxImageProvider()
private static readonly xyz = new PanoramaxXYZ()
private static defaultPanoramax = new AuthorizedPanoramax(Constants.panoramax.url, Constants.panoramax.token)
private static defaultPanoramax = new AuthorizedPanoramax(
Constants.panoramax.url,
Constants.panoramax.token
)
public defaultKeyPrefixes: string[] = ["panoramax"]
public readonly name: string = "panoramax"
private static knownMeta: Record<string, { data: ImageData, time: Date }> = {}
private static knownMeta: Record<string, { data: ImageData; time: Date }> = {}
public SourceIcon(img?: { id: string, url: string, host?: string }, location?: {
lon: number;
lat: number;
}): BaseUIElement {
public SourceIcon(
img?: { id: string; url: string; host?: string },
location?: {
lon: number
lat: number
}
): BaseUIElement {
const p = new Panoramax(img.host)
return new Link(new SvelteUIElement(Panoramax_bw), p.createViewLink({
imageId: img?.id,
location,
}), true)
return new Link(
new SvelteUIElement(Panoramax_bw),
p.createViewLink({
imageId: img?.id,
location,
}),
true
)
}
public addKnownMeta(meta: ImageData) {
@ -43,25 +51,24 @@ export default class PanoramaxImageProvider extends ImageProvider {
* @param id
* @private
*/
private async getInfoFromMapComplete(id: string): Promise<{ data: ImageData, url: string }> {
private async getInfoFromMapComplete(id: string): Promise<{ data: ImageData; url: string }> {
const sequence = "6e702976-580b-419c-8fb3-cf7bd364e6f8" // We always reuse this sequence
const url = `https://panoramax.mapcomplete.org/`
const data = await PanoramaxImageProvider.defaultPanoramax.imageInfo(id, sequence)
return { url, data }
}
private async getInfoFromXYZ(imageId: string): Promise<{ data: ImageData, url: string }> {
private async getInfoFromXYZ(imageId: string): Promise<{ data: ImageData; url: string }> {
const data = await PanoramaxImageProvider.xyz.imageInfo(imageId)
return { data, url: "https://api.panoramax.xyz/" }
}
/**
* Reads a geovisio-somewhat-looking-like-geojson object and converts it to a provided image
* @param meta
* @private
*/
private featureToImage(info: { data: ImageData, url: string }) {
private featureToImage(info: { data: ImageData; url: string }) {
const meta = info?.data
if (!meta) {
return undefined
@ -82,8 +89,9 @@ export default class PanoramaxImageProvider extends ImageProvider {
id: meta.id,
url: makeAbsolute(meta.assets.sd.href),
url_hd: makeAbsolute(meta.assets.hd.href),
host: meta["links"].find(l => l.rel === "root")?.href,
lon, lat,
host: meta["links"].find((l) => l.rel === "root")?.href,
lon,
lat,
key: "panoramax",
provider: this,
status: meta.properties["geovisio:status"],
@ -92,14 +100,13 @@ export default class PanoramaxImageProvider extends ImageProvider {
}
}
private async getInfoFor(id: string): Promise<{ data: ImageData, url: string }> {
private async getInfoFor(id: string): Promise<{ data: ImageData; url: string }> {
if (!id.match(/^[a-zA-Z0-9-]+$/)) {
return undefined
}
const cached = PanoramaxImageProvider.knownMeta[id]
if (cached) {
if (new Date().getTime() - cached.time.getTime() < 1000) {
return { data: cached.data, url: undefined }
}
}
@ -120,10 +127,9 @@ export default class PanoramaxImageProvider extends ImageProvider {
if (!Panoramax.isId(value)) {
return undefined
}
return [await this.getInfoFor(value).then(r => this.featureToImage(<any>r))]
return [await this.getInfoFor(value).then((r) => this.featureToImage(<any>r))]
}
getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
const source = UIEventSource.FromPromise(super.getRelevantUrlsFor(tags, prefixes))
@ -131,14 +137,15 @@ export default class PanoramaxImageProvider extends ImageProvider {
if (data === undefined) {
return true
}
return data?.some(img => img?.status !== undefined && img?.status !== "ready" && img?.status !== "broken")
return data?.some(
(img) =>
img?.status !== undefined && img?.status !== "ready" && img?.status !== "broken"
)
}
Stores.Chronic(1500, () =>
hasLoading(source.data),
).addCallback(_ => {
Stores.Chronic(1500, () => hasLoading(source.data)).addCallback((_) => {
console.log("UPdating... ")
super.getRelevantUrlsFor(tags, prefixes).then(data => {
super.getRelevantUrlsFor(tags, prefixes).then((data) => {
console.log("New panoramax data is", data, hasLoading(data))
source.set(data)
return !hasLoading(data)
@ -148,7 +155,10 @@ export default class PanoramaxImageProvider extends ImageProvider {
return source
}
public async DownloadAttribution(providedImage: { url: string; id: string; }): Promise<LicenseInfo> {
public async DownloadAttribution(providedImage: {
url: string
id: string
}): Promise<LicenseInfo> {
const meta = await this.getInfoFor(providedImage.id)
return {
@ -171,9 +181,14 @@ export class PanoramaxUploader implements ImageUploader {
this._panoramax = new AuthorizedPanoramax(url, token)
}
async uploadImage(blob: File, currentGps: [number, number], author: string, noblur: boolean = false): Promise<{
key: string;
value: string;
async uploadImage(
blob: File,
currentGps: [number, number],
author: string,
noblur: boolean = false
): Promise<{
key: string
value: string
absoluteUrl: string
}> {
// https://panoramax.openstreetmap.fr/api/docs/swagger#/
@ -183,7 +198,7 @@ export class PanoramaxUploader implements ImageUploader {
let hasGPS = false
try {
const tags = await ExifReader.load(blob)
hasDate = tags?.DateTime !== undefined
hasDate = tags?.DateTime !== undefined
hasGPS = tags?.GPSLatitude !== undefined && tags?.GPSLongitude !== undefined
} catch (e) {
console.error("Could not read EXIF-tags")
@ -203,7 +218,6 @@ export class PanoramaxUploader implements ImageUploader {
exifOverride: {
Artist: author,
},
})
PanoramaxImageProvider.singleton.addKnownMeta(img)
return {
@ -212,5 +226,4 @@ export class PanoramaxUploader implements ImageUploader {
absoluteUrl: img.assets.hd.href,
}
}
}

View file

@ -11,8 +11,10 @@ export class WikidataImageProvider extends ImageProvider {
public static readonly singleton = new WikidataImageProvider()
public readonly defaultKeyPrefixes = ["wikidata"]
public readonly name = "Wikidata"
private static readonly keyBlacklist: ReadonlySet<string> = new Set(
["mapillary", ...Utils.Times(i => "mapillary:" + i, 10)])
private static readonly keyBlacklist: ReadonlySet<string> = new Set([
"mapillary",
...Utils.Times((i) => "mapillary:" + i, 10),
])
private constructor() {
super()
@ -26,7 +28,7 @@ export class WikidataImageProvider extends ImageProvider {
return new SvelteUIElement(Wikidata_icon)
}
public async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> {
public async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> {
if (WikidataImageProvider.keyBlacklist.has(key)) {
return undefined
}

View file

@ -37,7 +37,7 @@ export class WikimediaImageProvider extends ImageProvider {
return value
}
const baseUrl = `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(
value,
value
)}`
if (useHd) {
return baseUrl
@ -106,7 +106,8 @@ export class WikimediaImageProvider extends ImageProvider {
value = WikimediaImageProvider.removeCommonsPrefix(value)
if (value.startsWith("Category:")) {
const urls = await Wikimedia.GetCategoryContents(value)
return urls.filter((url) => url.startsWith("File:"))
return urls
.filter((url) => url.startsWith("File:"))
.map((image) => this.UrlForImage(image))
}
if (value.startsWith("File:")) {
@ -117,7 +118,7 @@ export class WikimediaImageProvider extends ImageProvider {
return undefined
}
// We do a last effort and assume this is a file
return [(this.UrlForImage("File:" + value))]
return [this.UrlForImage("File:" + value)]
}
public async DownloadAttribution(img: { url: string }): Promise<LicenseInfo> {
@ -147,7 +148,7 @@ export class WikimediaImageProvider extends ImageProvider {
console.warn(
"The file",
filename,
"has no usable metedata or license attached... Please fix the license info file yourself!",
"has no usable metedata or license attached... Please fix the license info file yourself!"
)
return undefined
}

View file

@ -33,7 +33,7 @@ export default class ChangeLocationAction extends OsmChangeAction {
meta: {
theme: string
reason: string
},
}
) {
super(id, true)
this.state = state
@ -66,12 +66,10 @@ export default class ChangeLocationAction extends OsmChangeAction {
return [d]
}
const insertIntoWay = new InsertPointIntoWayAction(
lat, lon, this._id, snapToWay, {
allowReuseOfPreviouslyCreatedPoints: false,
reusePointWithinMeters: 0.25,
},
).prepareChangeDescription()
const insertIntoWay = new InsertPointIntoWayAction(lat, lon, this._id, snapToWay, {
allowReuseOfPreviouslyCreatedPoints: false,
reusePointWithinMeters: 0.25,
}).prepareChangeDescription()
return [d, { ...insertIntoWay, meta: d.meta }]
}

View file

@ -38,7 +38,7 @@ export default class CreateNewNodeAction extends OsmCreateAction {
theme: string
changeType: "create" | "import" | null
specialMotivation?: string
},
}
) {
super(null, basicTags !== undefined && basicTags.length > 0)
this._basicTags = basicTags
@ -102,21 +102,12 @@ export default class CreateNewNodeAction extends OsmCreateAction {
return [newPointChange]
}
const change = new InsertPointIntoWayAction(
this._lat,
this._lon,
id,
this._snapOnto,
{
reusePointWithinMeters: this._reusePointDistance,
allowReuseOfPreviouslyCreatedPoints: this._reusePreviouslyCreatedPoint,
},
).prepareChangeDescription()
const change = new InsertPointIntoWayAction(this._lat, this._lon, id, this._snapOnto, {
reusePointWithinMeters: this._reusePointDistance,
allowReuseOfPreviouslyCreatedPoints: this._reusePreviouslyCreatedPoint,
}).prepareChangeDescription()
return [
newPointChange,
{ ...change, meta: this.meta },
]
return [newPointChange, { ...change, meta: this.meta }]
}
private setElementId(id: number) {

View file

@ -2,7 +2,7 @@ import { ChangeDescription } from "./ChangeDescription"
import { GeoOperations } from "../../GeoOperations"
import { OsmWay } from "../OsmObject"
export default class InsertPointIntoWayAction {
export default class InsertPointIntoWayAction {
private readonly _lat: number
private readonly _lon: number
private readonly _idToInsert: number
@ -21,22 +21,19 @@ export default class InsertPointIntoWayAction {
allowReuseOfPreviouslyCreatedPoints?: boolean
reusePointWithinMeters?: number
}
){
) {
this._lat = lat
this._lon = lon
this._idToInsert = idToInsert
this._snapOnto = snapOnto
this._options = options
}
/**
* Tries to create the changedescription of the way where the point is inserted
* Returns `undefined` if inserting failed
*/
public prepareChangeDescription(): Omit<ChangeDescription, "meta"> | undefined {
public prepareChangeDescription(): Omit<ChangeDescription, "meta"> | undefined {
// Project the point onto the way
console.log("Snapping a node onto an existing way...")
const geojson = this._snapOnto.asGeoJson()
@ -59,13 +56,19 @@ export default class InsertPointIntoWayAction {
}
const prev = outerring[index]
if (GeoOperations.distanceBetween(prev, projectedCoor) < this._options.reusePointWithinMeters) {
if (
GeoOperations.distanceBetween(prev, projectedCoor) <
this._options.reusePointWithinMeters
) {
// We reuse this point instead!
reusedPointId = this._snapOnto.nodes[index]
reusedPointCoordinates = this._snapOnto.coordinates[index]
}
const next = outerring[index + 1]
if (GeoOperations.distanceBetween(next, projectedCoor) < this._options.reusePointWithinMeters) {
if (
GeoOperations.distanceBetween(next, projectedCoor) <
this._options.reusePointWithinMeters
) {
// We reuse this point instead!
reusedPointId = this._snapOnto.nodes[index + 1]
reusedPointCoordinates = this._snapOnto.coordinates[index + 1]
@ -82,15 +85,13 @@ export default class InsertPointIntoWayAction {
locations.splice(index + 1, 0, [this._lon, this._lat])
ids.splice(index + 1, 0, this._idToInsert)
return {
return {
type: "way",
id: this._snapOnto.id,
changes: {
coordinates: locations,
nodes: ids,
}
},
}
}
}

View file

@ -217,7 +217,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr
const url = `${
this.state.osmConnection?._oauth_config?.url ?? "https://api.openstreetmap.org"
}/api/0.6/${this.wayToReplaceId}/full`
const rawData = await Utils.downloadJsonCached<{elements: any[]}>(url, 1000)
const rawData = await Utils.downloadJsonCached<{ elements: any[] }>(url, 1000)
parsed = OsmObject.ParseObjects(rawData.elements)
}
const allNodes = parsed.filter((o) => o.type === "node")

View file

@ -49,11 +49,11 @@ export class Changes {
featureSwitches: {
featureSwitchMorePrivacy?: Store<boolean>
featureSwitchIsTesting?: Store<boolean>
},
osmConnection: OsmConnection,
reportError?: (error: string) => void,
featureProperties?: FeaturePropertiesStore,
historicalUserLocations?: FeatureSource,
}
osmConnection: OsmConnection
reportError?: (error: string) => void
featureProperties?: FeaturePropertiesStore
historicalUserLocations?: FeatureSource
allElements?: IndexedFeatureSource
},
leftRightSensitive: boolean = false,
@ -64,8 +64,11 @@ export class Changes {
this.allChanges.setData([...this.pendingChanges.data])
// If a pending change contains a negative ID, we save that
this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id ?? 0) ?? []))
if(isNaN(this._nextId) && state.reportError !== undefined){
state.reportError("Got a NaN as nextID. Pending changes IDs are:" +this.pendingChanges.data?.map(pch => pch?.id).join("."))
if (isNaN(this._nextId) && state.reportError !== undefined) {
state.reportError(
"Got a NaN as nextID. Pending changes IDs are:" +
this.pendingChanges.data?.map((pch) => pch?.id).join(".")
)
this._nextId = -100
}
this.state = state
@ -84,12 +87,12 @@ export class Changes {
// This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
}
public static createTestObject(): Changes{
public static createTestObject(): Changes {
return new Changes({
osmConnection: new OsmConnection(),
featureSwitches:{
featureSwitchIsTesting: new ImmutableStore(true)
}
featureSwitches: {
featureSwitchIsTesting: new ImmutableStore(true),
},
})
}
@ -837,12 +840,16 @@ export class Changes {
)
// We keep all the refused changes to try them again
this.pendingChanges.setData(refusedChanges.flatMap((c) => c).filter(c => {
if(c.id === null || c.id === undefined){
return false
}
return true
}))
this.pendingChanges.setData(
refusedChanges
.flatMap((c) => c)
.filter((c) => {
if (c.id === null || c.id === undefined) {
return false
}
return true
})
)
} catch (e) {
console.error(
"Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those",

View file

@ -205,12 +205,12 @@ export class ChangesetHandler {
try {
return await this.UploadWithNew(generateChangeXML, openChangeset, extraMetaTags)
} catch (e) {
const req = (<XMLHttpRequest>e)
const req = <XMLHttpRequest>e
if (req.status === 403) {
// Someone got the banhammer
// This is the message that OSM returned, will be something like "you have an important message, go to osm.org"
const msg = req.responseText
alert(msg+"\n\nWe'll take you to openstreetmap.org now")
const msg = req.responseText
alert(msg + "\n\nWe'll take you to openstreetmap.org now")
window.location.replace(this.osmConnection.Backend())
return
}

View file

@ -45,14 +45,14 @@ export class OsmConnection {
public userDetails: UIEventSource<UserDetails>
public isLoggedIn: Store<boolean>
public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
"unknown",
"unknown"
)
public apiIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
"unknown",
"unknown"
)
public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">(
"not-attempted",
"not-attempted"
)
public preferencesHandler: OsmPreferences
public readonly _oauth_config: AuthConfig
@ -96,7 +96,7 @@ export class OsmConnection {
this.userDetails = new UIEventSource<UserDetails>(
new UserDetails(this._oauth_config.url),
"userDetails",
"userDetails"
)
if (options.fakeUser) {
const ud = this.userDetails.data
@ -117,7 +117,7 @@ export class OsmConnection {
(user) =>
user.loggedIn &&
(this.apiIsOnline.data === "unknown" || this.apiIsOnline.data === "online"),
[this.apiIsOnline],
[this.apiIsOnline]
)
this.isLoggedIn.addCallback((isLoggedIn) => {
if (this.userDetails.data.loggedIn == false && isLoggedIn == true) {
@ -160,17 +160,16 @@ export class OsmConnection {
defaultValue: string = undefined,
options?: {
prefix?: string
},
}
): UIEventSource<T | undefined> {
const prefix = options?.prefix ?? "mapcomplete-"
return <UIEventSource<T>>this.preferencesHandler.getPreference(key, defaultValue, prefix)
}
public getPreference<T extends string = string>(
key: string,
defaultValue: string = undefined,
prefix: string = "mapcomplete-",
prefix: string = "mapcomplete-"
): UIEventSource<T | undefined> {
return <UIEventSource<T>>this.preferencesHandler.getPreference(key, defaultValue, prefix)
}
@ -214,7 +213,7 @@ export class OsmConnection {
this.updateAuthObject()
LocalStorageSource.get("location_before_login").setData(
Utils.runningFromConsole ? undefined : window.location.href,
Utils.runningFromConsole ? undefined : window.location.href
)
this.auth.xhr(
{
@ -252,13 +251,13 @@ export class OsmConnection {
data.account_created = userInfo.getAttribute("account_created")
data.uid = Number(userInfo.getAttribute("id"))
data.languages = Array.from(
userInfo.getElementsByTagName("languages")[0].getElementsByTagName("lang"),
userInfo.getElementsByTagName("languages")[0].getElementsByTagName("lang")
).map((l) => l.textContent)
data.csCount = Number.parseInt(
userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? "0",
userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? "0"
)
data.tracesCount = Number.parseInt(
userInfo.getElementsByTagName("traces")[0].getAttribute("count") ?? "0",
userInfo.getElementsByTagName("traces")[0].getAttribute("count") ?? "0"
)
data.img = undefined
@ -290,7 +289,7 @@ export class OsmConnection {
action(this.userDetails.data)
}
this._onLoggedIn = []
},
}
)
}
@ -308,7 +307,7 @@ export class OsmConnection {
method: "GET" | "POST" | "PUT" | "DELETE",
header?: Record<string, string>,
content?: string,
allowAnonymous: boolean = false,
allowAnonymous: boolean = false
): Promise<string> {
const connection: osmAuth = this.auth
if (allowAnonymous && !this.auth.authenticated()) {
@ -316,7 +315,7 @@ export class OsmConnection {
`${this.Backend()}/api/0.6/${path}`,
header,
method,
content,
content
)
if (possibleResult["content"]) {
return possibleResult["content"]
@ -333,13 +332,13 @@ export class OsmConnection {
content,
path: `/api/0.6/${path}`,
},
function(err, response) {
function (err, response) {
if (err !== null) {
error(err)
} else {
ok(response)
}
},
}
)
})
}
@ -348,7 +347,7 @@ export class OsmConnection {
path: string,
content?: string,
header?: Record<string, string>,
allowAnonymous: boolean = false,
allowAnonymous: boolean = false
): Promise<T> {
return <T>await this.interact(path, "POST", header, content, allowAnonymous)
}
@ -356,7 +355,7 @@ export class OsmConnection {
public async put<T extends string>(
path: string,
content?: string,
header?: Record<string, string>,
header?: Record<string, string>
): Promise<T> {
return <T>await this.interact(path, "PUT", header, content)
}
@ -364,7 +363,7 @@ export class OsmConnection {
public async get(
path: string,
header?: Record<string, string>,
allowAnonymous: boolean = false,
allowAnonymous: boolean = false
): Promise<string> {
return await this.interact(path, "GET", header, undefined, allowAnonymous)
}
@ -403,7 +402,7 @@ export class OsmConnection {
return new Promise<{ id: number }>((ok) => {
window.setTimeout(
() => ok({ id: Math.floor(Math.random() * 1000) }),
Math.random() * 5000,
Math.random() * 5000
)
})
}
@ -415,7 +414,7 @@ export class OsmConnection {
{
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
},
true,
true
)
const parsed = JSON.parse(response)
console.log("Got result:", parsed)
@ -438,14 +437,14 @@ export class OsmConnection {
* Note: these are called 'tags' on the wiki, but I opted to name them 'labels' instead as they aren't "key=value" tags, but just words.
*/
labels: string[]
},
}
): Promise<{ id: number }> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually uploading GPX ", gpx)
return new Promise<{ id: number }>((ok) => {
window.setTimeout(
() => ok({ id: Math.floor(Math.random() * 1000) }),
Math.random() * 5000,
Math.random() * 5000
)
})
}
@ -462,9 +461,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"
@ -472,7 +471,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]
}
@ -506,13 +505,13 @@ export class OsmConnection {
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`,
},
function(err) {
function (err) {
if (err !== null) {
error(err)
} else {
ok()
}
},
}
)
})
}
@ -521,7 +520,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")
@ -538,8 +537,8 @@ export class OsmConnection {
? "https://mapcomplete.org/land.html"
: window.location.protocol + "//" + window.location.host + "/land.html",
/* We use 'singlePage' as much as possible, it is the most stable - including in PWA.
* However, this breaks in iframes so we open a popup in that case
*/
* However, this breaks in iframes so we open a popup in that case
*/
singlepage: !this._iframeMode,
auto: true,
apiUrl: this._oauth_config.api_url ?? this._oauth_config.url,

View file

@ -5,7 +5,6 @@ import OSMAuthInstance = OSMAuth.osmAuth
import { Utils } from "../../Utils"
export class OsmPreferences {
/**
* A 'cache' of all the preference stores
* @private
@ -39,7 +38,6 @@ export class OsmPreferences {
})
}
private setPreferencesAll(key: string, value: string) {
if (this._allPreferences.data[key] !== value) {
this._allPreferences.data[key] = value
@ -54,11 +52,11 @@ export class OsmPreferences {
}
return this.preferences[key]
}
const pref = this.preferences[key] = new UIEventSource(value, "preference: " + key)
const pref = (this.preferences[key] = new UIEventSource(value, "preference: " + key))
if (value) {
this.setPreferencesAll(key, value)
}
pref.addCallback(v => {
pref.addCallback((v) => {
console.log("Got an update:", key, "--->", v)
this.uploadKvSplit(key, v)
this.setPreferencesAll(key, v)
@ -71,7 +69,7 @@ export class OsmPreferences {
this.seenKeys = Object.keys(prefs)
const legacy = OsmPreferences.getLegacyCombinedItems(prefs)
const merged = OsmPreferences.mergeDict(prefs)
if(Object.keys(legacy).length > 0){
if (Object.keys(legacy).length > 0) {
await this.removeLegacy(legacy)
}
for (const key in merged) {
@ -82,12 +80,7 @@ export class OsmPreferences {
}
}
public getPreference(
key: string,
defaultValue: string = undefined,
prefix?: string,
) {
public getPreference(key: string, defaultValue: string = undefined, prefix?: string) {
return this.getPreferenceSeedFromlocal(key, defaultValue, { prefix })
}
@ -100,27 +93,29 @@ export class OsmPreferences {
key: string,
defaultValue: string = undefined,
options?: {
prefix?: string,
prefix?: string
saveToLocalStorage?: true | boolean
},
}
): UIEventSource<string> {
if (options?.prefix) {
key = options.prefix + key
}
key = key.replace(/[:/"' {}.%\\]/g, "")
const localStorage = LocalStorageSource.get(key) // cached
if (localStorage.data === "null" || localStorage.data === "undefined") {
localStorage.set(undefined)
}
const pref: UIEventSource<string> = this.initPreference(key, localStorage.data ?? defaultValue) // cached
const pref: UIEventSource<string> = this.initPreference(
key,
localStorage.data ?? defaultValue
) // cached
if (this.localStorageInited.has(key)) {
return pref
}
if (options?.saveToLocalStorage ?? true) {
pref.addCallback(v => localStorage.set(v)) // Keep a local copy
pref.addCallback((v) => localStorage.set(v)) // Keep a local copy
}
this.localStorageInited.add(key)
return pref
@ -134,7 +129,7 @@ export class OsmPreferences {
public async removeLegacy(legacyDict: Record<string, string>) {
for (const k in legacyDict) {
const v = legacyDict[k]
console.log("Upgrading legacy preference",k )
console.log("Upgrading legacy preference", k)
await this.removeAllWithPrefix(k)
this.osmConnection.getPreference(k).set(v)
}
@ -148,20 +143,19 @@ export class OsmPreferences {
const newDict = {}
const allKeys: string[] = Object.keys(dict)
const normalKeys = allKeys.filter(k => !k.match(/[a-z-_0-9A-Z]*:[0-9]+/))
const normalKeys = allKeys.filter((k) => !k.match(/[a-z-_0-9A-Z]*:[0-9]+/))
for (const normalKey of normalKeys) {
if (normalKey.match(/-combined-[0-9]*$/) || normalKey.match(/-combined-length$/)) {
// Ignore legacy keys
continue
}
const partKeys = OsmPreferences.keysStartingWith(allKeys, normalKey)
const parts = partKeys.map(k => dict[k])
const parts = partKeys.map((k) => dict[k])
newDict[normalKey] = parts.join("")
}
return newDict
}
/**
* Gets all items which have a 'combined'-string, the legacy long preferences
*
@ -180,7 +174,9 @@ export class OsmPreferences {
public static getLegacyCombinedItems(dict: Record<string, string>): Record<string, string> {
const merged: Record<string, string> = {}
const keys = Object.keys(dict)
const toCheck = Utils.NoNullInplace(Utils.Dedup(keys.map(k => k.match(/(.*)-combined-[0-9]+$/)?.[1])))
const toCheck = Utils.NoNullInplace(
Utils.Dedup(keys.map((k) => k.match(/(.*)-combined-[0-9]+$/)?.[1]))
)
for (const key of toCheck) {
let i = 0
let str = ""
@ -195,7 +191,6 @@ export class OsmPreferences {
return merged
}
/**
* Bulk-downloads all preferences
* @private
@ -221,10 +216,9 @@ export class OsmPreferences {
dict[k] = pref.getAttribute("v")
}
resolve(dict)
},
}
)
})
}
/**
@ -238,7 +232,7 @@ export class OsmPreferences {
*
*/
private static keysStartingWith(allKeys: string[], key: string): string[] {
const keys = allKeys.filter(k => k === key || k.match(new RegExp(key + ":[0-9]+")))
const keys = allKeys.filter((k) => k === key || k.match(new RegExp(key + ":[0-9]+")))
keys.sort()
return keys
}
@ -249,14 +243,12 @@ export class OsmPreferences {
*
*/
private async uploadKvSplit(k: string, v: string) {
if (v === null || v === undefined || v === "" || v === "undefined" || v === "null") {
const keysToDelete = OsmPreferences.keysStartingWith(this.seenKeys, k)
await Promise.all(keysToDelete.map(k => this.deleteKeyDirectly(k)))
await Promise.all(keysToDelete.map((k) => this.deleteKeyDirectly(k)))
return
}
await this.uploadKeyDirectly(k, v.slice(0, 255))
v = v.slice(255)
let i = 0
@ -265,7 +257,6 @@ export class OsmPreferences {
v = v.slice(255)
i++
}
}
/**
@ -283,25 +274,23 @@ export class OsmPreferences {
return
}
return new Promise<void>((resolve, reject) => {
this.auth.xhr(
{
method: "DELETE",
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
headers: { "Content-Type": "text/plain" },
},
(error) => {
if (error) {
console.warn("Could not remove preference", error)
reject(error)
return
}
console.debug("Preference ", k, "removed!")
resolve()
},
)
},
)
this.auth.xhr(
{
method: "DELETE",
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
headers: { "Content-Type": "text/plain" },
},
(error) => {
if (error) {
console.warn("Could not remove preference", error)
reject(error)
return
}
console.debug("Preference ", k, "removed!")
resolve()
}
)
})
}
/**
@ -328,7 +317,6 @@ export class OsmPreferences {
}
return new Promise<void>((resolve, reject) => {
this.auth.xhr(
{
method: "PUT",
@ -343,7 +331,7 @@ export class OsmPreferences {
return
}
resolve()
},
}
)
})
}
@ -351,12 +339,10 @@ export class OsmPreferences {
async removeAllWithPrefix(prefix: string) {
const keys = this.seenKeys
for (const key of keys) {
if(!key.startsWith(prefix)){
if (!key.startsWith(prefix)) {
continue
}
await this.deleteKeyDirectly(key)
}
}
}

View file

@ -1,4 +1,8 @@
import GeocodingProvider, { SearchResult, GeocodingOptions, GeocodeResult } from "./GeocodingProvider"
import GeocodingProvider, {
SearchResult,
GeocodingOptions,
GeocodeResult,
} from "./GeocodingProvider"
import { Utils } from "../../Utils"
import { Store, Stores } from "../UIEventSource"
@ -8,7 +12,7 @@ export default class CombinedSearcher implements GeocodingProvider {
constructor(...providers: ReadonlyArray<GeocodingProvider>) {
this._providers = Utils.NoNull(providers)
this._providersWithSuggest = this._providers.filter(pr => pr.suggest !== undefined)
this._providersWithSuggest = this._providers.filter((pr) => pr.suggest !== undefined)
}
/**
@ -21,12 +25,10 @@ export default class CombinedSearcher implements GeocodingProvider {
const results: GeocodeResult[] = []
const seenIds = new Set<string>()
for (const geocodedElement of geocoded) {
if(geocodedElement === undefined){
if (geocodedElement === undefined) {
continue
}
for (const entry of geocodedElement) {
if (entry.osm_id === undefined) {
throw "Invalid search result: a search result always must have an osm_id to be able to merge results from different sources"
}
@ -42,14 +44,13 @@ export default class CombinedSearcher implements GeocodingProvider {
}
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
const results = (await Promise.all(this._providers.map(pr => pr.search(query, options))))
const results = await Promise.all(this._providers.map((pr) => pr.search(query, options)))
return CombinedSearcher.merge(results)
}
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
return Stores.concat(
this._providersWithSuggest.map(pr => pr.suggest(query, options)))
.map(gcrss => CombinedSearcher.merge(gcrss))
this._providersWithSuggest.map((pr) => pr.suggest(query, options))
).map((gcrss) => CombinedSearcher.merge(gcrss))
}
}

View file

@ -19,7 +19,6 @@ export default class CoordinateSearch implements GeocodingProvider {
/^(-?[0-9]+\.[0-9]+)[ ,;/\\]+(-?[0-9]+\.[0-9]+)/,
/lon[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?[ ,;&]+lat[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?/,
/lng[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?[ ,;&]+lat[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?/,
]
/**
@ -60,16 +59,18 @@ export default class CoordinateSearch implements GeocodingProvider {
* results[0] // => {lat: -57.5802905, lon: -12.7202538, "display_name": "lon: -12.720254, lat: -57.58029", "category": "coordinate","osm_id": "-12.720254/-57.58029", "source": "coordinate:latlon"}
*/
private directSearch(query: string): GeocodeResult[] {
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r)))
.map(m => CoordinateSearch.asResult(m[2], m[1], "latlon") )
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map((r) => query.match(r))).map(
(m) => CoordinateSearch.asResult(m[2], m[1], "latlon")
)
const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r)))
.map(m => CoordinateSearch.asResult(m[1], m[2], "lonlat"))
const matchesLonLat = Utils.NoNull(
CoordinateSearch.lonLatRegexes.map((r) => query.match(r))
).map((m) => CoordinateSearch.asResult(m[1], m[2], "lonlat"))
return matches.concat(matchesLonLat)
}
private static round6(n: number): string {
return "" + (Math.round(n * 1000000) / 1000000)
return "" + Math.round(n * 1000000) / 1000000
}
private static asResult(lonIn: string, latIn: string, source: string): GeocodeResult {
@ -82,7 +83,7 @@ export default class CoordinateSearch implements GeocodingProvider {
lon,
display_name: "lon: " + lonStr + ", lat: " + latStr,
category: "coordinate",
source: "coordinate:"+source,
source: "coordinate:" + source,
osm_id: lonStr + "/" + latStr,
}
}
@ -94,5 +95,4 @@ export default class CoordinateSearch implements GeocodingProvider {
async search(query: string): Promise<GeocodeResult[]> {
return this.directSearch(query)
}
}

View file

@ -6,16 +6,20 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import LayerState from "../State/LayerState"
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
export type FilterSearchResult = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number }
export type FilterSearchResult = {
option: FilterConfigOption
filter: FilterConfig
layer: LayerConfig
index: number
}
/**
* Searches matching filters
*/
export default class FilterSearch {
private readonly _state: {layerState: LayerState, theme: ThemeConfig}
private readonly _state: { layerState: LayerState; theme: ThemeConfig }
constructor(state: {layerState: LayerState, theme: ThemeConfig}) {
constructor(state: { layerState: LayerState; theme: ThemeConfig }) {
this._state = state
}
@ -23,12 +27,15 @@ export default class FilterSearch {
if (query.length === 0) {
return []
}
const queries = query.split(" ").map(query => {
if (!Utils.isEmoji(query)) {
return Utils.simplifyStringForSearch(query)
}
return query
}).filter(q => q.length > 0)
const queries = query
.split(" ")
.map((query) => {
if (!Utils.isEmoji(query)) {
return Utils.simplifyStringForSearch(query)
}
return query
})
.filter((q) => q.length > 0)
const possibleFilters: FilterSearchResult[] = []
for (const layer of this._state.theme.layers) {
if (!Array.isArray(layer.filters)) {
@ -46,29 +53,36 @@ export default class FilterSearch {
if (!option.osmTags) {
continue
}
if(option.fields.length > 0){
if (option.fields.length > 0) {
// Filters with a search field are not supported as of now, see #2141
continue
}
let terms = ([option.question.txt,
...(option.searchTerms?.[Locale.language.data] ?? option.searchTerms?.["en"] ?? [])]
.flatMap(term => [term, ...(term?.split(" ") ?? [])]))
terms = terms.map(t => Utils.simplifyStringForSearch(t))
let terms = [
option.question.txt,
...(option.searchTerms?.[Locale.language.data] ??
option.searchTerms?.["en"] ??
[]),
].flatMap((term) => [term, ...(term?.split(" ") ?? [])])
terms = terms.map((t) => Utils.simplifyStringForSearch(t))
terms.push(option.emoji)
Utils.NoNullInplace(terms)
const distances = queries.flatMap(query => terms.map(entry => {
const d = Utils.levenshteinDistance(query, entry.slice(0, query.length))
const dRelative = d / query.length
return dRelative
}))
const distances = queries.flatMap((query) =>
terms.map((entry) => {
const d = Utils.levenshteinDistance(query, entry.slice(0, query.length))
const dRelative = d / query.length
return dRelative
})
)
const levehnsteinD = Math.min(...distances)
if (levehnsteinD > 0.25) {
continue
}
possibleFilters.push({
option, layer, filter, index:
i,
option,
layer,
filter,
index: i,
})
}
}
@ -85,7 +99,7 @@ export default class FilterSearch {
if (!Array.isArray(filteredLayer.layerDef.filters)) {
continue
}
if (Constants.priviliged_layers.indexOf(<any> id) >= 0) {
if (Constants.priviliged_layers.indexOf(<any>id) >= 0) {
continue
}
for (const filter of filteredLayer.layerDef.filters) {
@ -116,13 +130,16 @@ export default class FilterSearch {
* Note that this depends on the language and the displayed text. For example, two filters {"en": "A", "nl": "B"} and {"en": "X", "nl": "B"} will be joined for dutch but not for English
*
*/
static mergeSemiIdenticalLayers<T extends FilterSearchResult = FilterSearchResult>(filters: ReadonlyArray<T>, language: string):T[][] {
const results : Record<string, T[]> = {}
static mergeSemiIdenticalLayers<T extends FilterSearchResult = FilterSearchResult>(
filters: ReadonlyArray<T>,
language: string
): T[][] {
const results: Record<string, T[]> = {}
for (const filter of filters) {
const txt = filter.option.question.textFor(language)
if(results[txt]){
if (results[txt]) {
results[txt].push(filter)
}else{
} else {
results[txt] = [filter]
}
}

View file

@ -7,8 +7,8 @@ export default class GeocodingFeatureSource implements FeatureSource {
public features: Store<Feature<Geometry, Record<string, string>>[]>
constructor(provider: Store<SearchResult[]>) {
this.features = provider.map(geocoded => {
if(geocoded === undefined){
this.features = provider.map((geocoded) => {
if (geocoded === undefined) {
return []
}
const features: Feature[] = []
@ -28,18 +28,16 @@ export default class GeocodingFeatureSource implements FeatureSource {
osm_id: gc.osm_type + "/" + gc.osm_id,
osm_key: gc.feature?.properties?.osm_key,
osm_value: gc.feature?.properties?.osm_value,
source: gc.source
source: gc.source,
},
geometry: {
type: "Point",
coordinates: [gc.lon, gc.lat]
}
coordinates: [gc.lon, gc.lat],
},
})
}
return features
})
}
}

View file

@ -8,7 +8,7 @@ import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { GeoOperations } from "../GeoOperations"
export type GeocodingCategory =
"coordinate"
| "coordinate"
| "city"
| "house"
| "street"
@ -19,7 +19,7 @@ export type GeocodingCategory =
| "airport"
| "shop"
export type GeocodeResult = {
export type GeocodeResult = {
/**
* The name of the feature being displayed
*/
@ -27,8 +27,8 @@ export type GeocodeResult = {
/**
* Some optional, extra information
*/
description?: string | Promise<string>,
feature?: Feature,
description?: string | Promise<string>
feature?: Feature
lat: number
lon: number
/**
@ -37,22 +37,18 @@ export type GeocodeResult = {
*/
boundingbox?: number[]
osm_type?: "node" | "way" | "relation"
osm_id: string,
category?: GeocodingCategory,
payload?: object,
osm_id: string
category?: GeocodingCategory
payload?: object
source?: string
}
export type SearchResult =
| GeocodeResult
export type SearchResult = GeocodeResult
export interface GeocodingOptions {
bbox?: BBox
}
export default interface GeocodingProvider {
search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]>
/**
@ -62,26 +58,28 @@ export default interface GeocodingProvider {
suggest?(query: string, options?: GeocodingOptions): Store<GeocodeResult[]>
}
export type ReverseGeocodingResult = Feature<Geometry, {
osm_id: number,
osm_type: "node" | "way" | "relation",
country: string,
city: string,
countrycode: string,
type: GeocodingCategory,
street: string
}>
export type ReverseGeocodingResult = Feature<
Geometry,
{
osm_id: number
osm_type: "node" | "way" | "relation"
country: string
city: string
countrycode: string
type: GeocodingCategory
street: string
}
>
export interface ReverseGeocodingProvider {
reverseSearch(
coordinate: { lon: number; lat: number },
zoom: number,
language?: string,
): Promise<ReverseGeocodingResult[]>;
language?: string
): Promise<ReverseGeocodingResult[]>
}
export class GeocodingUtils {
public static searchLayer = GeocodingUtils.initSearchLayer()
private static initSearchLayer(): LayerConfig {
@ -103,16 +101,14 @@ export class GeocodingUtils {
train_station: 14,
airport: 13,
shop: 16,
}
public static mergeSimilarResults(results: GeocodeResult[]){
public static mergeSimilarResults(results: GeocodeResult[]) {
const byName: Record<string, GeocodeResult[]> = {}
for (const result of results) {
const nm = result.display_name
if(!byName[nm]) {
if (!byName[nm]) {
byName[nm] = []
}
byName[nm].push(result)
@ -123,11 +119,13 @@ export class GeocodingUtils {
const options = byName[nm]
const added = options[0]
merged.push(added)
const centers: [number,number][] = [[added.lon, added.lat]]
const centers: [number, number][] = [[added.lon, added.lat]]
for (const other of options) {
const otherCenter:[number,number] = [other.lon, other.lat]
const nearbyFound= centers.some(center => GeoOperations.distanceBetween(center, otherCenter) < 500)
if(!nearbyFound){
const otherCenter: [number, number] = [other.lon, other.lat]
const nearbyFound = centers.some(
(center) => GeoOperations.distanceBetween(center, otherCenter) < 500
)
if (!nearbyFound) {
merged.push(other)
centers.push(otherCenter)
}
@ -136,7 +134,6 @@ export class GeocodingUtils {
return merged
}
public static categoryToIcon: Record<GeocodingCategory, DefaultPinIcon> = {
city: "building_office_2",
coordinate: "globe_alt",
@ -148,8 +145,5 @@ export class GeocodingUtils {
county: "building_office_2",
airport: "airport",
shop: "building_storefront",
}
}

View file

@ -5,22 +5,26 @@ import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
import { Utils } from "../../Utils"
export default class LayerSearch {
private readonly _theme: ThemeConfig
private readonly _layerWhitelist: Set<string>
constructor(theme: ThemeConfig) {
this._theme = theme
this._layerWhitelist = new Set(theme.layers
.filter(l => l.isNormal())
.map(l => l.id))
this._layerWhitelist = new Set(theme.layers.filter((l) => l.isNormal()).map((l) => l.id))
}
static scoreLayers(query: string, options: {
whitelist?: Set<string>, blacklist?: Set<string>
}): Record<string, number> {
static scoreLayers(
query: string,
options: {
whitelist?: Set<string>
blacklist?: Set<string>
}
): Record<string, number> {
const result: Record<string, number> = {}
const queryParts = query.trim().split(" ").map(q => Utils.simplifyStringForSearch(q))
const queryParts = query
.trim()
.split(" ")
.map((q) => Utils.simplifyStringForSearch(q))
for (const id in ThemeSearch.officialThemes.layers) {
if (options?.whitelist && !options?.whitelist.has(id)) {
continue
@ -29,18 +33,17 @@ export default class LayerSearch {
continue
}
const keywords = ThemeSearch.officialThemes.layers[id]
result[id] = Math.min(...queryParts.map(q => SearchUtils.scoreKeywords(q, keywords)))
result[id] = Math.min(...queryParts.map((q) => SearchUtils.scoreKeywords(q, keywords)))
}
return result
}
public search(query: string, limit: number, scoreThreshold: number = 2): LayerConfig[] {
if (query.length < 1) {
return []
}
const scores = LayerSearch.scoreLayers(query, { whitelist: this._layerWhitelist })
const asList: ({ layer: LayerConfig, score: number })[] = []
const asList: { layer: LayerConfig; score: number }[] = []
for (const layer in scores) {
asList.push({
layer: this._theme.getLayer(layer),
@ -50,10 +53,8 @@ export default class LayerSearch {
asList.sort((a, b) => a.score - b.score)
return asList
.filter(sorted => sorted.score < scoreThreshold)
.filter((sorted) => sorted.score < scoreThreshold)
.slice(0, limit)
.map(l => l.layer)
.map((l) => l.layer)
}
}

View file

@ -7,14 +7,14 @@ import { ImmutableStore, Store, Stores } from "../UIEventSource"
import OpenStreetMapIdSearch from "./OpenStreetMapIdSearch"
type IntermediateResult = {
feature: Feature,
feature: Feature
/**
* Lon, lat
*/
center: [number, number],
levehnsteinD: number,
physicalDistance: number,
searchTerms: string[],
center: [number, number]
levehnsteinD: number
physicalDistance: number
searchTerms: string[]
description: string
}
export default class LocalElementSearch implements GeocodingProvider {
@ -24,37 +24,50 @@ export default class LocalElementSearch implements GeocodingProvider {
constructor(state: ThemeViewState, limit: number) {
this._state = state
this._limit = limit
}
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
return this.searchEntries(query, options, false).data
}
private getPartialResult(query: string, candidateId: string | undefined, matchStart: boolean, centerpoint: [number, number], features: Feature[]): IntermediateResult[] {
const results: IntermediateResult [] = []
private getPartialResult(
query: string,
candidateId: string | undefined,
matchStart: boolean,
centerpoint: [number, number],
features: Feature[]
): IntermediateResult[] {
const results: IntermediateResult[] = []
for (const feature of features) {
const props = feature.properties
const searchTerms: string[] = Utils.NoNull([props.name, props.alt_name, props.local_name,
(props["addr:street"] && props["addr:number"]) ?
props["addr:street"] + props["addr:number"] : undefined])
const searchTerms: string[] = Utils.NoNull([
props.name,
props.alt_name,
props.local_name,
props["addr:street"] && props["addr:number"]
? props["addr:street"] + props["addr:number"]
: undefined,
])
let levehnsteinD: number
if (candidateId === props.id) {
levehnsteinD = 0
} else {
levehnsteinD = Math.min(...searchTerms.flatMap(entry => entry.split(/ /)).map(entry => {
let simplified = Utils.simplifyStringForSearch(entry)
if (matchStart) {
simplified = simplified.slice(0, query.length)
}
return Utils.levenshteinDistance(query, simplified)
}))
levehnsteinD = Math.min(
...searchTerms
.flatMap((entry) => entry.split(/ /))
.map((entry) => {
let simplified = Utils.simplifyStringForSearch(entry)
if (matchStart) {
simplified = simplified.slice(0, query.length)
}
return Utils.levenshteinDistance(query, simplified)
})
)
}
const center = GeoOperations.centerpointCoordinates(feature)
if ((levehnsteinD / query.length) <= 0.3) {
if (levehnsteinD / query.length <= 0.3) {
let description = ""
if (feature.properties["addr:street"]) {
description += "" + feature.properties["addr:street"]
@ -75,7 +88,11 @@ export default class LocalElementSearch implements GeocodingProvider {
return results
}
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<SearchResult[]> {
searchEntries(
query: string,
options?: GeocodingOptions,
matchStart?: boolean
): Store<SearchResult[]> {
if (query.length < 3) {
return new ImmutableStore([])
}
@ -88,17 +105,26 @@ export default class LocalElementSearch implements GeocodingProvider {
const partials: Store<IntermediateResult[]>[] = []
for (const [_, geoIndexedStore] of properties) {
const partialResult = geoIndexedStore.features.map(features => this.getPartialResult(query, candidateId, matchStart, centerPoint, features))
const partialResult = geoIndexedStore.features.map((features) =>
this.getPartialResult(query, candidateId, matchStart, centerPoint, features)
)
partials.push(partialResult)
}
const listed: Store<IntermediateResult[]> = Stores.concat(partials).map(l => l.flatMap(x => x))
return listed.mapD(results => {
results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25))
const listed: Store<IntermediateResult[]> = Stores.concat(partials).map((l) =>
l.flatMap((x) => x)
)
return listed.mapD((results) => {
results.sort(
(a, b) =>
a.physicalDistance +
a.levehnsteinD * 25 -
(b.physicalDistance + b.levehnsteinD * 25)
)
if (this._limit) {
results = results.slice(0, this._limit)
}
return results.map(entry => {
return results.map((entry) => {
const [osm_type, osm_id] = entry.feature.properties.id.split("/")
return <SearchResult>{
lon: entry.center[0],
@ -113,12 +139,9 @@ export default class LocalElementSearch implements GeocodingProvider {
}
})
})
}
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
return this.searchEntries(query, options, true)
}
}

View file

@ -6,26 +6,24 @@ import Locale from "../../UI/i18n/Locale"
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
export class NominatimGeocoding implements GeocodingProvider {
private readonly _host ;
private readonly _host
private readonly limit: number
constructor(limit: number = 3, host: string = Constants.nominatimEndpoint) {
constructor(limit: number = 3, host: string = Constants.nominatimEndpoint) {
this.limit = limit
this._host = host
}
public search(query: string, options?:GeocodingOptions): Promise<SearchResult[]> {
public search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
const b = options?.bbox ?? BBox.global
const url = `${
this._host
}search?format=json&limit=${this.limit}&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${
const url = `${this._host}search?format=json&limit=${
this.limit
}&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${
Locale.language.data
}&q=${query}`
return Utils.downloadJson(url)
}
async reverseSearch(
coordinate: { lon: number; lat: number },
zoom: number = 17,

View file

@ -5,12 +5,13 @@ import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
import { Utils } from "../../Utils"
export default class OpenStreetMapIdSearch implements GeocodingProvider {
private static readonly regex = /((https?:\/\/)?(www.)?(osm|openstreetmap).org\/)?(n|node|w|way|r|relation)[/ ]?([0-9]+)/
private static readonly regex =
/((https?:\/\/)?(www.)?(osm|openstreetmap).org\/)?(n|node|w|way|r|relation)[/ ]?([0-9]+)/
private static readonly types: Readonly<Record<string, "node" | "way" | "relation">> = {
"n": "node",
"w": "way",
"r": "relation",
n: "node",
w: "way",
r: "relation",
}
private readonly _state: SpecialVisualizationState
@ -55,15 +56,17 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
category: "coordinate",
osm_type: <"node" | "way" | "relation">osm_type,
osm_id,
lat: 0, lon: 0,
lat: 0,
lon: 0,
source: "osmid",
}
}
const [lat, lon] = obj.centerpoint()
return {
lat, lon,
display_name: obj.tags.name ?? obj.tags.alt_name ?? obj.tags.local_name ?? obj.tags.ref ?? id,
lat,
lon,
display_name:
obj.tags.name ?? obj.tags.alt_name ?? obj.tags.local_name ?? obj.tags.ref ?? id,
description: osm_type,
osm_type: <"node" | "way" | "relation">osm_type,
osm_id,
@ -72,14 +75,15 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
}
async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
if (!isNaN(Number(query))) {
const n = Number(query)
return Utils.NoNullInplace(await Promise.all([
this.getInfoAbout(`node/${n}`).catch(x => undefined),
this.getInfoAbout(`way/${n}`).catch(x => undefined),
this.getInfoAbout(`relation/${n}`).catch(() => undefined),
]))
return Utils.NoNullInplace(
await Promise.all([
this.getInfoAbout(`node/${n}`).catch((x) => undefined),
this.getInfoAbout(`way/${n}`).catch((x) => undefined),
this.getInfoAbout(`relation/${n}`).catch(() => undefined),
])
)
}
const id = OpenStreetMapIdSearch.extractId(query)
@ -92,5 +96,4 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
suggest?(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
return UIEventSource.FromPromise(this.search(query, options))
}
}

View file

@ -2,7 +2,8 @@ import Constants from "../../Models/Constants"
import GeocodingProvider, {
GeocodeResult,
GeocodingCategory,
GeocodingOptions, GeocodingUtils,
GeocodingOptions,
GeocodingUtils,
ReverseGeocodingProvider,
ReverseGeocodingResult,
} from "./GeocodingProvider"
@ -16,31 +17,35 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
private _endpoint: string
private supportedLanguages = ["en", "de", "fr"]
private static readonly types = {
"R": "relation",
"W": "way",
"N": "node",
R: "relation",
W: "way",
N: "node",
}
private readonly suggestionLimit: number = 5
private readonly searchLimit: number = 1
constructor(suggestionLimit:number = 5, searchLimit:number = 1, endpoint?: string) {
constructor(suggestionLimit: number = 5, searchLimit: number = 1, endpoint?: string) {
this.suggestionLimit = suggestionLimit
this.searchLimit = searchLimit
this._endpoint = endpoint ?? Constants.photonEndpoint ?? "https://photon.komoot.io/"
}
async reverseSearch(coordinate: {
lon: number;
lat: number
}, zoom: number, language?: string): Promise<ReverseGeocodingResult[]> {
const url = `${this._endpoint}/reverse?lon=${coordinate.lon}&lat=${coordinate.lat}&${this.getLanguage(language)}`
async reverseSearch(
coordinate: {
lon: number
lat: number
},
zoom: number,
language?: string
): Promise<ReverseGeocodingResult[]> {
const url = `${this._endpoint}/reverse?lon=${coordinate.lon}&lat=${
coordinate.lat
}&${this.getLanguage(language)}`
const result = await Utils.downloadJsonCached<FeatureCollection>(url, 1000 * 60 * 60)
for (const f of result.features) {
f.properties.osm_type = PhotonSearch.types[f.properties.osm_type]
}
return <ReverseGeocodingResult[]>result.features
}
/**
@ -49,13 +54,11 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
* @private
*/
private getLanguage(language?: string): string {
language ??= Locale.language.data
if (this.supportedLanguages.indexOf(language) < 0) {
return ""
}
return `&lang=${language}`
}
suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
@ -75,7 +78,6 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
switch (type) {
case "house": {
const addr = ifdef("", p.street) + ifdef(" ", p.housenumber)
if (!addr) {
return p.city
@ -94,7 +96,6 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
case "country":
return undefined
}
}
private getCategory(entry: Feature) {
@ -111,7 +112,11 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
return p.type
}
async search(query: string, options?: GeocodingOptions, limit?: number): Promise<GeocodeResult[]> {
async search(
query: string,
options?: GeocodingOptions,
limit?: number
): Promise<GeocodeResult[]> {
if (query.length < 3) {
return []
}
@ -121,9 +126,11 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
const [lon, lat] = options.bbox.center()
bbox = `&lon=${lon}&lat=${lat}`
}
const url = `${this._endpoint}/api/?q=${encodeURIComponent(query)}&limit=${limit}${this.getLanguage()}${bbox}`
const url = `${this._endpoint}/api/?q=${encodeURIComponent(
query
)}&limit=${limit}${this.getLanguage()}${bbox}`
const results = await Utils.downloadJsonCached<FeatureCollection>(url, 1000 * 60 * 60)
const encoded= results.features.map(f => {
const encoded = results.features.map((f) => {
const [lon, lat] = GeoOperations.centerpointCoordinates(f)
let boundingbox: number[] = undefined
if (f.properties.extent) {
@ -138,11 +145,11 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
osm_type: PhotonSearch.types[f.properties.osm_type],
category: this.getCategory(f),
boundingbox,
lon, lat,
lon,
lat,
source: this._endpoint,
}
})
return GeocodingUtils.mergeSimilarResults(encoded)
}
}

View file

@ -3,13 +3,11 @@ import { Utils } from "../../Utils"
import ThemeSearch from "./ThemeSearch"
export default class SearchUtils {
/** Applies special search terms, such as 'studio', 'osmcha', ...
* Returns 'false' if nothing is matched.
* Doesn't return control flow if a match is found (navigates to another page in this case)
*/
public static applySpecialSearch(searchTerm: string, ) {
public static applySpecialSearch(searchTerm: string) {
searchTerm = searchTerm.toLowerCase()
if (!searchTerm) {
return false
@ -33,10 +31,8 @@ export default class SearchUtils {
window.location.href = "./studio.html"
}
return false
}
/**
* Searches for the smallest distance in words; will split both the query and the terms
*
@ -44,19 +40,26 @@ export default class SearchUtils {
* SearchUtils.scoreKeywords("waste", {"en": ["A layer with drinking water points"]}, "en") // => 2
*
*/
public static scoreKeywords(query: string, keywords: Record<string, string[]> | string[], language?: string): number {
if(!keywords){
public static scoreKeywords(
query: string,
keywords: Record<string, string[]> | string[],
language?: string
): number {
if (!keywords) {
return Infinity
}
language ??= Locale.language.data
const queryParts = query.trim().split(" ").map(q => Utils.simplifyStringForSearch(q))
const queryParts = query
.trim()
.split(" ")
.map((q) => Utils.simplifyStringForSearch(q))
let terms: string[]
if (Array.isArray(keywords)) {
terms = keywords
} else {
terms = (keywords[language] ?? []).concat(keywords["*"])
}
const termsAll = Utils.NoNullInplace(terms).flatMap(t => t.split(" "))
const termsAll = Utils.NoNullInplace(terms).flatMap((t) => t.split(" "))
let distanceSummed = 0
for (let i = 0; i < queryParts.length; i++) {

View file

@ -8,57 +8,59 @@ import LayerSearch from "./LayerSearch"
import SearchUtils from "./SearchUtils"
import { OsmConnection } from "../Osm/OsmConnection"
type ThemeSearchScore = {
theme: MinimalThemeInformation,
lowest: number,
perLayer?: Record<string, number>,
other: number,
theme: MinimalThemeInformation
lowest: number
perLayer?: Record<string, number>
other: number
}
export default class ThemeSearch {
public static readonly officialThemes: {
themes: MinimalThemeInformation[],
themes: MinimalThemeInformation[]
layers: Record<string, Record<string, string[]>>
} = <any> themeOverview
public static readonly officialThemesById: Map<string, MinimalThemeInformation> = new Map<string, MinimalThemeInformation>()
} = <any>themeOverview
public static readonly officialThemesById: Map<string, MinimalThemeInformation> = new Map<
string,
MinimalThemeInformation
>()
static {
for (const th of ThemeSearch.officialThemes.themes ?? []) {
ThemeSearch.officialThemesById.set(th.id, th)
}
}
private readonly _knownHiddenThemes: Store<Set<string>>
private readonly _layersToIgnore: string[]
private readonly _otherThemes: MinimalThemeInformation[]
constructor(state: {osmConnection: OsmConnection, theme: ThemeConfig}) {
this._layersToIgnore = state.theme.layers.filter(l => l.isNormal()).map(l => l.id)
this._knownHiddenThemes = UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection).map(list => new Set(list))
this._otherThemes = ThemeSearch.officialThemes.themes
.filter(th => th.id !== state.theme.id)
constructor(state: { osmConnection: OsmConnection; theme: ThemeConfig }) {
this._layersToIgnore = state.theme.layers.filter((l) => l.isNormal()).map((l) => l.id)
this._knownHiddenThemes = UserRelatedState.initDiscoveredHiddenThemes(
state.osmConnection
).map((list) => new Set(list))
this._otherThemes = ThemeSearch.officialThemes.themes.filter(
(th) => th.id !== state.theme.id
)
}
public search(query: string, limit: number, threshold: number = 3): MinimalThemeInformation[] {
if (query.length < 1) {
return []
}
const sorted = ThemeSearch.sortedByLowestScores(query, this._otherThemes, this._layersToIgnore)
const sorted = ThemeSearch.sortedByLowestScores(
query,
this._otherThemes,
this._layersToIgnore
)
return sorted
.filter(sorted => sorted.lowest < threshold)
.map(th => th.theme)
.filter(th => !th.hideFromOverview || this._knownHiddenThemes.data.has(th.id))
.filter((sorted) => sorted.lowest < threshold)
.map((th) => th.theme)
.filter((th) => !th.hideFromOverview || this._knownHiddenThemes.data.has(th.id))
.slice(0, limit)
}
public static createUrlFor(
layout: { id: string },
state?: { layoutToUse?: { id } },
): string {
public static createUrlFor(layout: { id: string }, state?: { layoutToUse?: { id } }): string {
if (layout === undefined) {
return undefined
}
@ -88,7 +90,6 @@ export default class ThemeSearch {
linkPrefix = `${path}/theme.html?userlayout=${layout.id}&`
}
return `${linkPrefix}`
}
@ -101,17 +102,21 @@ export default class ThemeSearch {
* @param ignoreLayers
* @private
*/
private static scoreThemes(query: string, themes: MinimalThemeInformation[], ignoreLayers: string[] = undefined): Record<string, ThemeSearchScore> {
private static scoreThemes(
query: string,
themes: MinimalThemeInformation[],
ignoreLayers: string[] = undefined
): Record<string, ThemeSearchScore> {
if (query?.length < 1) {
return undefined
}
themes = Utils.NoNullInplace(themes)
let options : {blacklist: Set<string>} = undefined
if(ignoreLayers?.length > 0){
let options: { blacklist: Set<string> } = undefined
if (ignoreLayers?.length > 0) {
options = { blacklist: new Set(ignoreLayers) }
}
const layerScores = query.length < 3 ? {} : LayerSearch.scoreLayers(query, options)
const layerScores = query.length < 3 ? {} : LayerSearch.scoreLayers(query, options)
const results: Record<string, ThemeSearchScore> = {}
for (const layoutInfo of themes) {
const theme = layoutInfo.id
@ -122,20 +127,22 @@ export default class ThemeSearch {
results[theme] = {
theme: layoutInfo,
lowest: -1,
other: 0
other: 0,
}
continue
}
const perLayer = Utils.asRecord(
layoutInfo.layers ?? [], layer => layerScores[layer],
)
const perLayer = Utils.asRecord(layoutInfo.layers ?? [], (layer) => layerScores[layer])
const language = Locale.language.data
const keywords = Utils.NoNullInplace([layoutInfo.shortDescription, layoutInfo.title])
.map(item => typeof item === "string" ? item : (item[language] ?? item["*"]))
const keywords = Utils.NoNullInplace([
layoutInfo.shortDescription,
layoutInfo.title,
]).map((item) => (typeof item === "string" ? item : item[language] ?? item["*"]))
const other = Math.min(SearchUtils.scoreKeywords(query, keywords), SearchUtils.scoreKeywords(query, layoutInfo.keywords))
const other = Math.min(
SearchUtils.scoreKeywords(query, keywords),
SearchUtils.scoreKeywords(query, layoutInfo.keywords)
)
const lowest = Math.min(other, ...Object.values(perLayer))
results[theme] = {
theme: layoutInfo,
@ -147,15 +154,21 @@ export default class ThemeSearch {
return results
}
public static sortedByLowestScores(search: string, themes: MinimalThemeInformation[], ignoreLayers: string[] = []): ThemeSearchScore[] {
public static sortedByLowestScores(
search: string,
themes: MinimalThemeInformation[],
ignoreLayers: string[] = []
): ThemeSearchScore[] {
const scored = Object.values(this.scoreThemes(search, themes, ignoreLayers))
scored.sort((a, b) => a.lowest - b.lowest)
return scored
}
public static sortedByLowest(search: string, themes: MinimalThemeInformation[], ignoreLayers: string[] = []): MinimalThemeInformation[] {
return this.sortedByLowestScores(search, themes, ignoreLayers)
.map(th => th.theme)
public static sortedByLowest(
search: string,
themes: MinimalThemeInformation[],
ignoreLayers: string[] = []
): MinimalThemeInformation[] {
return this.sortedByLowestScores(search, themes, ignoreLayers).map((th) => th.theme)
}
}

View file

@ -45,7 +45,6 @@ export class OsmConnectionFeatureSwitches {
}
export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
public readonly featureSwitchEnableLogin: UIEventSource<boolean>
public readonly featureSwitchSearch: UIEventSource<boolean>
public readonly featureSwitchBackgroundSelection: UIEventSource<boolean>

View file

@ -11,8 +11,8 @@ import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
import Constants from "../../Models/Constants"
export type ActiveFilter = {
layer: LayerConfig,
filter: FilterConfig,
layer: LayerConfig
filter: FilterConfig
control: UIEventSource<string | number | undefined>
}
/**
@ -36,9 +36,13 @@ export default class LayerState {
private readonly _activeFilters: UIEventSource<ActiveFilter[]> = new UIEventSource([])
public readonly activeFilters: Store<ActiveFilter[]> = this._activeFilters
private readonly _activeLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>(undefined)
private readonly _activeLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<
FilteredLayer[]
>(undefined)
public readonly activeLayers: Store<FilteredLayer[]> = this._activeLayers
private readonly _nonactiveLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>(undefined)
private readonly _nonactiveLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<
FilteredLayer[]
>(undefined)
public readonly nonactiveLayers: Store<FilteredLayer[]> = this._nonactiveLayers
private readonly osmConnection: OsmConnection
@ -71,7 +75,7 @@ export default class LayerState {
this.filteredLayers = filteredLayers
layers.forEach((l) => LayerState.linkFilterStates(l, filteredLayers))
this.filteredLayers.forEach(fl => {
this.filteredLayers.forEach((fl) => {
fl.isDisplayed.addCallback(() => this.updateActiveFilters())
for (const [_, appliedFilter] of fl.appliedFilters) {
appliedFilter.addCallback(() => this.updateActiveFilters())
@ -80,27 +84,27 @@ export default class LayerState {
this.updateActiveFilters()
}
private updateActiveFilters(){
private updateActiveFilters() {
const filters: ActiveFilter[] = []
const activeLayers: FilteredLayer[] = []
const nonactiveLayers: FilteredLayer[] = []
this.filteredLayers.forEach(fl => {
if(!fl.isDisplayed.data){
const nonactiveLayers: FilteredLayer[] = []
this.filteredLayers.forEach((fl) => {
if (!fl.isDisplayed.data) {
nonactiveLayers.push(fl)
return
}
activeLayers.push(fl)
if(fl.layerDef.filterIsSameAs){
if (fl.layerDef.filterIsSameAs) {
return
}
for (const [filtername, appliedFilter] of fl.appliedFilters) {
if (appliedFilter.data === undefined) {
continue
}
const filter = fl.layerDef.filters.find(f => f.id === filtername)
if(typeof appliedFilter.data === "number"){
if(filter.options[appliedFilter.data].osmTags === undefined){
const filter = fl.layerDef.filters.find((f) => f.id === filtername)
if (typeof appliedFilter.data === "number") {
if (filter.options[appliedFilter.data].osmTags === undefined) {
// This is probably the first, generic option which doesn't _actually_ filter
continue
}

View file

@ -17,7 +17,6 @@ import { FeatureSource } from "../FeatureSource/FeatureSource"
import { Feature } from "geojson"
export default class SearchState {
public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("")
public readonly searchIsFocused = new UIEventSource(false)
@ -39,57 +38,66 @@ export default class SearchState {
new LocalElementSearch(state, 5),
new CoordinateSearch(),
new OpenStreetMapIdSearch(state),
new PhotonSearch() // new NominatimGeocoding(),
new PhotonSearch(), // new NominatimGeocoding(),
]
const bounds = state.mapProperties.bounds
const suggestionsList = this.searchTerm.stabilized(250).mapD(search => {
const suggestionsList = this.searchTerm.stabilized(250).mapD(
(search) => {
if (search.length === 0) {
return undefined
}
return this.locationSearchers.map(ls => ls.suggest(search, { bbox: bounds.data }))
}, [bounds]
return this.locationSearchers.map((ls) => ls.suggest(search, { bbox: bounds.data }))
},
[bounds]
)
this.suggestionsSearchRunning = suggestionsList.bind(suggestions => {
this.suggestionsSearchRunning = suggestionsList.bind((suggestions) => {
if (suggestions === undefined) {
return new ImmutableStore(true)
}
return Stores.concat(suggestions).map(suggestions => suggestions.some(list => list === undefined))
return Stores.concat(suggestions).map((suggestions) =>
suggestions.some((list) => list === undefined)
)
})
this.suggestions = suggestionsList.bindD(suggestions =>
Stores.concat(suggestions).map(suggestions => CombinedSearcher.merge(suggestions))
this.suggestions = suggestionsList.bindD((suggestions) =>
Stores.concat(suggestions).map((suggestions) => CombinedSearcher.merge(suggestions))
)
const themeSearch = new ThemeSearch(state)
this.themeSuggestions = this.searchTerm.mapD(query => themeSearch.search(query, 3))
this.themeSuggestions = this.searchTerm.mapD((query) => themeSearch.search(query, 3))
const layerSearch = new LayerSearch(state.theme)
this.layerSuggestions = this.searchTerm.mapD(query => layerSearch.search(query, 5))
this.layerSuggestions = this.searchTerm.mapD((query) => layerSearch.search(query, 5))
const filterSearch = new FilterSearch(state)
this.filterSuggestions = this.searchTerm.stabilized(50)
.mapD(query => filterSearch.search(query))
.mapD(filterResult => {
const active = state.layerState.activeFilters.data
return filterResult.filter(({ filter, index, layer }) => {
const foundMatch = active.some(active =>
active.filter.id === filter.id && layer.id === active.layer.id && active.control.data === index)
this.filterSuggestions = this.searchTerm
.stabilized(50)
.mapD((query) => filterSearch.search(query))
.mapD(
(filterResult) => {
const active = state.layerState.activeFilters.data
return filterResult.filter(({ filter, index, layer }) => {
const foundMatch = active.some(
(active) =>
active.filter.id === filter.id &&
layer.id === active.layer.id &&
active.control.data === index
)
return !foundMatch
})
}, [state.layerState.activeFilters])
return !foundMatch
})
},
[state.layerState.activeFilters]
)
this.locationResults = new GeocodingFeatureSource(this.suggestions.stabilized(250))
this.showSearchDrawer = new UIEventSource(false)
this.searchIsFocused.addCallbackAndRunD(sugg => {
this.searchIsFocused.addCallbackAndRunD((sugg) => {
if (sugg) {
this.showSearchDrawer.set(true)
}
})
}
public async apply(result: FilterSearchResult[] | LayerConfig) {
@ -108,7 +116,7 @@ export default class SearchState {
private async applyFilter(payload: FilterSearchResult[]) {
const state = this.state
const layersToShow = payload.map(fsr => fsr.layer.id)
const layersToShow = payload.map((fsr) => fsr.layer.id)
console.log("Layers to show are", layersToShow)
for (const [name, otherLayer] of state.layerState.filteredLayers) {
const layer = otherLayer.layerDef
@ -144,7 +152,7 @@ export default class SearchState {
}
// This feature might not be loaded because we zoomed out
const object = await this.state.osmObjectDownloader.DownloadObjectAsync(osmid)
if(object === "deleted"){
if (object === "deleted") {
return
}
const f = object.asGeoJson()

View file

@ -22,9 +22,7 @@ import Showdown from "showdown"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { GeocodeResult } from "../Search/GeocodingProvider"
export class OptionallySyncedHistory<T> {
public readonly syncPreference: UIEventSource<"sync" | "local" | "no">
public readonly value: Store<T[]>
private readonly synced: UIEventSource<T[]>
@ -34,18 +32,26 @@ export class OptionallySyncedHistory<T> {
private readonly _isSame: (a: T, b: T) => boolean
private osmconnection: OsmConnection
constructor(key: string, osmconnection: OsmConnection, maxHistory: number = 20, isSame?: (a: T, b: T) => boolean) {
constructor(
key: string,
osmconnection: OsmConnection,
maxHistory: number = 20,
isSame?: (a: T, b: T) => boolean
) {
this.osmconnection = osmconnection
this._maxHistory = maxHistory
this._isSame = isSame
this.syncPreference = osmconnection.getPreference(
"preference-" + key + "-history",
"sync",
)
const synced = this.synced = UIEventSource.asObject<T[]>(osmconnection.getPreference(key + "-history"), [])
const local = this.local = LocalStorageSource.getParsed<T[]>(key + "-history", [])
const thisSession = this.thisSession = new UIEventSource<T[]>([], "optionally-synced:" + key + "(session only)")
this.syncPreference.addCallback(syncmode => {
this.syncPreference = osmconnection.getPreference("preference-" + key + "-history", "sync")
const synced = (this.synced = UIEventSource.asObject<T[]>(
osmconnection.getPreference(key + "-history"),
[]
))
const local = (this.local = LocalStorageSource.getParsed<T[]>(key + "-history", []))
const thisSession = (this.thisSession = new UIEventSource<T[]>(
[],
"optionally-synced:" + key + "(session only)"
))
this.syncPreference.addCallback((syncmode) => {
if (syncmode === "sync") {
let list = [...thisSession.data, ...synced.data].slice(0, maxHistory)
if (this._isSame) {
@ -67,9 +73,7 @@ export class OptionallySyncedHistory<T> {
}
})
this.value = this.syncPreference.bind(syncPref => this.getAppropriateStore(syncPref))
this.value = this.syncPreference.bind((syncPref) => this.getAppropriateStore(syncPref))
}
private getAppropriateStore(syncPref?: string) {
@ -87,7 +91,7 @@ export class OptionallySyncedHistory<T> {
const store = this.getAppropriateStore()
let oldList = store.data ?? []
if (this._isSame) {
oldList = oldList.filter(x => !this._isSame(t, x))
oldList = oldList.filter((x) => !this._isSame(t, x))
}
store.set([t, ...oldList].slice(0, this._maxHistory))
}
@ -100,14 +104,13 @@ export class OptionallySyncedHistory<T> {
if (t === undefined) {
return
}
this.osmconnection.isLoggedIn.addCallbackAndRun(loggedIn => {
this.osmconnection.isLoggedIn.addCallbackAndRun((loggedIn) => {
if (!loggedIn) {
return
}
this.add(t)
return true
})
}
clear() {
@ -157,7 +160,7 @@ export default class UserRelatedState {
*/
public readonly gpsLocationHistoryRetentionTime = new UIEventSource(
7 * 24 * 60 * 60,
"gps_location_retention",
"gps_location_retention"
)
public readonly addNewFeatureMode = new UIEventSource<
@ -180,18 +183,17 @@ export default class UserRelatedState {
public readonly recentlyVisitedThemes: OptionallySyncedHistory<string>
public readonly recentlyVisitedSearch: OptionallySyncedHistory<GeocodeResult>
constructor(
osmConnection: OsmConnection,
layout?: ThemeConfig,
featureSwitches?: FeatureSwitchState,
mapProperties?: MapProperties,
mapProperties?: MapProperties
) {
this.osmConnection = osmConnection
this._mapProperties = mapProperties
this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
this.osmConnection.getPreference("show-all-questions", "false"),
this.osmConnection.getPreference("show-all-questions", "false")
)
this.language = this.osmConnection.getPreference("language")
this.showTags = this.osmConnection.getPreference("show_tags")
@ -203,15 +205,19 @@ export default class UserRelatedState {
this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.getPreference("identity", undefined, "mangrove"),
this.osmConnection.getPreference("identity-creation-date", undefined, "mangrove"),
this.osmConnection.getPreference("identity-creation-date", undefined, "mangrove")
)
this.preferredBackgroundLayer = this.osmConnection.getPreference(
"preferred-background-layer"
)
this.preferredBackgroundLayer = this.osmConnection.getPreference("preferred-background-layer")
this.addNewFeatureMode = this.osmConnection.getPreference(
"preferences-add-new-mode",
"button_click_right",
"button_click_right"
)
this.showScale = UIEventSource.asBoolean(
this.osmConnection.GetPreference("preference-show-scale", "false")
)
this.showScale = UIEventSource.asBoolean(this.osmConnection.GetPreference("preference-show-scale", "false"))
this.imageLicense = this.osmConnection.getPreference("pictures-license", "CC0")
this.installedUserThemes = UserRelatedState.initInstalledUserThemes(osmConnection)
@ -224,12 +230,13 @@ export default class UserRelatedState {
"theme",
this.osmConnection,
10,
(a, b) => a === b,
(a, b) => a === b
)
this.recentlyVisitedSearch = new OptionallySyncedHistory<GeocodeResult>("places",
this.recentlyVisitedSearch = new OptionallySyncedHistory<GeocodeResult>(
"places",
this.osmConnection,
15,
(a, b) => a.osm_id === b.osm_id && a.osm_type === b.osm_type,
(a, b) => a.osm_id === b.osm_id && a.osm_type === b.osm_type
)
this.syncLanguage()
this.recentlyVisitedThemes.addDefferred(layout?.id)
@ -279,9 +286,7 @@ export default class UserRelatedState {
*/
public addUnofficialTheme(themeInfo: MinimalThemeInformation) {
const pref = this.osmConnection.getPreference("unofficial-theme-" + themeInfo.id)
this.osmConnection.isLoggedIn.when(
() => pref.set(JSON.stringify(themeInfo))
)
this.osmConnection.isLoggedIn.when(() => pref.set(JSON.stringify(themeInfo)))
}
public getUnofficialTheme(id: string): MinimalThemeInformation | undefined {
@ -298,9 +303,9 @@ export default class UserRelatedState {
} catch (e) {
console.warn(
"Removing theme " +
id +
" as it could not be parsed from the preferences; the content is:",
str,
id +
" as it could not be parsed from the preferences; the content is:",
str
)
pref.setData(null)
return undefined
@ -330,7 +335,7 @@ export default class UserRelatedState {
title: layout.title.translations,
shortDescription: layout.shortDescription.translations,
definition: layout["definition"],
}),
})
)
}
}
@ -340,7 +345,7 @@ export default class UserRelatedState {
return osmConnection.preferencesHandler.allPreferences.map((prefs) =>
Object.keys(prefs)
.filter((k) => k.startsWith(prefix))
.map((k) => k.substring(prefix.length)),
.map((k) => k.substring(prefix.length))
)
}
@ -354,7 +359,7 @@ export default class UserRelatedState {
return userPreferences.map((preferences) =>
Object.keys(preferences)
.filter((key) => key.startsWith(prefix))
.map((key) => key.substring(prefix.length, key.length - "-enabled".length)),
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
)
}
@ -370,7 +375,7 @@ export default class UserRelatedState {
return undefined
}
return [home.lon, home.lat]
}),
})
).map((homeLonLat) => {
if (homeLonLat === undefined) {
return empty
@ -400,7 +405,7 @@ export default class UserRelatedState {
* */
private initAmendedPrefs(
layout?: ThemeConfig,
featureSwitches?: FeatureSwitchState,
featureSwitches?: FeatureSwitchState
): UIEventSource<Record<string, string>> {
const amendedPrefs = new UIEventSource<Record<string, string>>({
_theme: layout?.id,
@ -446,19 +451,19 @@ export default class UserRelatedState {
const missingLayers = Utils.Dedup(
untranslated
.filter((k) => k.startsWith("layers:"))
.map((k) => k.slice("layers:".length).split(".")[0]),
.map((k) => k.slice("layers:".length).split(".")[0])
)
const zenLinks: { link: string; id: string }[] = Utils.NoNull([
hasMissingTheme
? {
id: "theme:" + layout.id,
link: LinkToWeblate.hrefToWeblateZen(
language,
"themes",
layout.id,
),
}
id: "theme:" + layout.id,
link: LinkToWeblate.hrefToWeblateZen(
language,
"themes",
layout.id
),
}
: undefined,
...missingLayers.map((id) => ({
id: "layer:" + id,
@ -475,7 +480,7 @@ export default class UserRelatedState {
}
amendedPrefs.ping()
},
[this.translationMode],
[this.translationMode]
)
this.mangroveIdentity.getKeyId().addCallbackAndRun((kid) => {
@ -494,7 +499,7 @@ export default class UserRelatedState {
.makeHtml(userDetails.description)
?.replace(/&gt;/g, ">")
?.replace(/&lt;/g, "<")
?.replace(/\n/g, ""),
?.replace(/\n/g, "")
)
}
@ -505,7 +510,7 @@ export default class UserRelatedState {
(c: { contributor: string; commits: number }) => {
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
return replaced === simplifiedName
},
}
)
if (isTranslator) {
amendedPrefs.data["_translation_contributions"] = "" + isTranslator.commits
@ -514,7 +519,7 @@ export default class UserRelatedState {
(c: { contributor: string; commits: number }) => {
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
return replaced === simplifiedName
},
}
)
if (isCodeContributor) {
amendedPrefs.data["_code_contributions"] = "" + isCodeContributor.commits

View file

@ -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(/&lt;/g,'<')?.replace(/&gt;/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(/&lt;/g, "<")?.replace(/&gt;/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"
}
}

View file

@ -19,7 +19,7 @@ export class Tag extends TagsFilter {
if (value === undefined) {
throw `Invalid value while constructing a Tag with key '${key}': value is undefined`
}
if(value.length > 255 || key.length > 255){
if (value.length > 255 || key.length > 255) {
throw "Invalid tag: length is over 255"
}
if (value === "*") {

View file

@ -23,7 +23,7 @@ export class Stores {
}
public static FromPromiseWithErr<T>(
promise: Promise<T>,
promise: Promise<T>
): Store<{ success: T } | { error: any }> {
return UIEventSource.FromPromiseWithErr(promise)
}
@ -99,7 +99,7 @@ export class Stores {
*/
static holdDefined<T>(store: Store<T | undefined>): Store<T | undefined> {
const newStore = new UIEventSource(store.data)
store.addCallbackD(t => {
store.addCallbackD((t) => {
newStore.setData(t)
})
return newStore
@ -133,23 +133,27 @@ export abstract class Store<T> implements Readable<T> {
abstract map<J>(
f: (t: T) => J,
extraStoresToWatch: Store<any>[],
callbackDestroyFunction: (f: () => void) => void,
callbackDestroyFunction: (f: () => void) => void
): Store<J>
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraStoresToWatch?: Store<any>[],
callbackDestroyFunction?: (f: () => void) => void,
callbackDestroyFunction?: (f: () => void) => void
): Store<J> {
return this.map((t) => {
if (t === undefined) {
return undefined
}
if (t === null) {
return null
}
return f(<Exclude<T, undefined | null>>t)
}, extraStoresToWatch, callbackDestroyFunction)
return this.map(
(t) => {
if (t === undefined) {
return undefined
}
if (t === null) {
return null
}
return f(<Exclude<T, undefined | null>>t)
},
extraStoresToWatch,
callbackDestroyFunction
)
}
/**
@ -176,7 +180,7 @@ export abstract class Store<T> implements Readable<T> {
abstract addCallbackAndRun(callback: (data: T) => void): () => void
public withEqualityStabilized(
comparator: (t: T | undefined, t1: T | undefined) => boolean,
comparator: (t: T | undefined, t1: T | undefined) => boolean
): Store<T> {
let oldValue = undefined
return this.map((v) => {
@ -269,7 +273,10 @@ export abstract class Store<T> implements Readable<T> {
return sink
}
public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>, extraSources: UIEventSource<object>[] = []): Store<X> {
public bindD<X>(
f: (t: Exclude<T, undefined | null>) => Store<X>,
extraSources: UIEventSource<object>[] = []
): Store<X> {
return this.bind((t) => {
if (t === null) {
return null
@ -343,10 +350,10 @@ export abstract class Store<T> implements Readable<T> {
public abstract destroy()
when(callback: () => void, condition?: (v:T) => boolean) {
condition ??= v => v === true
this.addCallbackAndRunD(v => {
if ( condition(v)) {
when(callback: () => void, condition?: (v: T) => boolean) {
condition ??= (v) => v === true
this.addCallbackAndRunD((v) => {
if (condition(v)) {
callback()
return true
}
@ -364,8 +371,7 @@ export class ImmutableStore<T> extends Store<T> {
this.data = data
}
private static readonly pass: () => void = () => {
}
private static readonly pass: () => void = () => {}
addCallback(_: (data: T) => void): () => void {
// pass: data will never change
@ -394,7 +400,7 @@ export class ImmutableStore<T> extends Store<T> {
map<J>(
f: (t: T) => J,
extraStores: Store<any>[] = undefined,
ondestroyCallback?: (f: () => void) => void,
ondestroyCallback?: (f: () => void) => void
): ImmutableStore<J> {
if (extraStores?.length > 0) {
return new MappedStore(this, f, extraStores, undefined, f(this.data), ondestroyCallback)
@ -464,7 +470,7 @@ class ListenerTracker<T> {
let endTime = new Date().getTime() / 1000
if (endTime - startTime > 500) {
console.trace(
"Warning: a ping took more then 500ms; this is probably a performance issue",
"Warning: a ping took more then 500ms; this is probably a performance issue"
)
}
if (toDelete !== undefined) {
@ -506,7 +512,7 @@ class MappedStore<TIn, T> extends Store<T> {
extraStores: Store<any>[],
upstreamListenerHandler: ListenerTracker<TIn> | undefined,
initialState: T,
onDestroy?: (f: () => void) => void,
onDestroy?: (f: () => void) => void
) {
super()
this._upstream = upstream
@ -546,7 +552,7 @@ class MappedStore<TIn, T> extends Store<T> {
map<J>(
f: (t: T) => J,
extraStores: Store<any>[] = undefined,
ondestroyCallback?: (f: () => void) => void,
ondestroyCallback?: (f: () => void) => void
): Store<J> {
let stores: Store<any>[] = undefined
if (extraStores?.length > 0 || this._extraStores?.length > 0) {
@ -568,7 +574,7 @@ class MappedStore<TIn, T> extends Store<T> {
stores,
this._callbacks,
f(this.data),
ondestroyCallback,
ondestroyCallback
)
}
@ -624,7 +630,7 @@ class MappedStore<TIn, T> extends Store<T> {
this._unregisterFromUpstream = this._upstream.addCallback((_) => self.update())
this._unregisterFromExtraStores = this._extraStores?.map((store) =>
store?.addCallback((_) => self.update()),
store?.addCallback((_) => self.update())
)
this._callbacksAreRegistered = true
}
@ -645,8 +651,7 @@ class MappedStore<TIn, T> extends Store<T> {
}
export class UIEventSource<T> extends Store<T> implements Writable<T> {
private static readonly pass: (() => void) = () => {
}
private static readonly pass: () => void = () => {}
public data: T
_callbacks: ListenerTracker<T> = new ListenerTracker<T>()
@ -661,7 +666,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
public static flatten<X>(
source: Store<Store<X>>,
possibleSources?: Store<object>[],
possibleSources?: Store<object>[]
): UIEventSource<X> {
const sink = new UIEventSource<X>(source.data?.data)
@ -690,7 +695,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
*/
public static FromPromise<T>(
promise: Promise<T>,
onError: (e) => void = undefined,
onError: (e) => void = undefined
): UIEventSource<T> {
const src = new UIEventSource<T>(undefined)
promise?.then((d) => src.setData(d))
@ -711,7 +716,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
* @constructor
*/
public static FromPromiseWithErr<T>(
promise: Promise<T>,
promise: Promise<T>
): UIEventSource<{ success: T } | { error: any } | undefined> {
const src = new UIEventSource<{ success: T } | { error: any }>(undefined)
promise
@ -743,7 +748,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return undefined
}
return "" + fl
},
}
)
}
@ -774,7 +779,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return undefined
}
return "" + fl
},
}
)
}
@ -782,11 +787,14 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return stringUIEventSource.sync(
(str) => str === "true",
[],
(b) => "" + b,
(b) => "" + b
)
}
static asObject<T extends object>(stringUIEventSource: UIEventSource<string>, defaultV: T): UIEventSource<T> {
static asObject<T extends object>(
stringUIEventSource: UIEventSource<string>,
defaultV: T
): UIEventSource<T> {
return stringUIEventSource.sync(
(str) => {
if (str === undefined || str === null || str === "") {
@ -800,7 +808,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
}
},
[],
(b) => JSON.stringify(b) ?? "",
(b) => JSON.stringify(b) ?? ""
)
}
@ -890,7 +898,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
public map<J>(
f: (t: T) => J,
extraSources: Store<any>[] = [],
onDestroy?: (f: () => void) => void,
onDestroy?: (f: () => void) => void
): Store<J> {
return new MappedStore(this, f, extraSources, this._callbacks, f(this.data), onDestroy)
}
@ -902,7 +910,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraSources: Store<any>[] = [],
callbackDestroyFunction?: (f: () => void) => void,
callbackDestroyFunction?: (f: () => void) => void
): Store<J | undefined> {
return new MappedStore(
this,
@ -920,7 +928,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
this.data === undefined || this.data === null
? <undefined | null>this.data
: f(<any>this.data),
callbackDestroyFunction,
callbackDestroyFunction
)
}
@ -940,7 +948,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
f: (t: T) => J,
extraSources: Store<any>[],
g: (j: J, t: T) => T,
allowUnregister = false,
allowUnregister = false
): UIEventSource<J> {
const self = this
@ -949,7 +957,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
const newSource = new UIEventSource<J>(f(this.data), "map(" + this.tag + ")@" + callee)
const update = function() {
const update = function () {
newSource.setData(f(self.data))
return allowUnregister && newSource._callbacks.length() === 0
}

View file

@ -5,7 +5,6 @@ import { Utils } from "../../Utils"
* UIEventsource-wrapper around localStorage
*/
export class LocalStorageSource {
private static readonly _cache: Record<string, UIEventSource<string>> = {}
static getParsed<T>(key: string, defaultValue: T): UIEventSource<T> {
@ -21,7 +20,7 @@ export class LocalStorageSource {
}
},
[],
(value) => JSON.stringify(value),
(value) => JSON.stringify(value)
)
}
@ -32,7 +31,6 @@ export class LocalStorageSource {
}
let saved = defaultValue
if (!Utils.runningFromConsole) {
try {
saved = localStorage.getItem(key)
if (saved === "undefined") {

View file

@ -22,7 +22,7 @@ export class MangroveIdentity {
this.mangroveIdentity = mangroveIdentity
this._mangroveIdentityCreationDate = mangroveIdentityCreationDate
mangroveIdentity.addCallbackAndRunD(async (data) => {
if(data === ""){
if (data === "") {
return
}
await this.setKeypair(data)

View file

@ -297,15 +297,13 @@ export default class NameSuggestionIndex {
return true
}
if (
i.locationSet.include.some((c) => countries.indexOf(c) >= 0)
) {
if (i.locationSet.include.some((c) => countries.indexOf(c) >= 0)) {
// We prefer the countries provided by lonlat2country, they are more precise and are loaded already anyway (cheap)
// Country might contain multiple countries, separated by ';'
return true
}
if (i.locationSet.exclude?.some(c => countries.indexOf(c) >= 0)) {
if (i.locationSet.exclude?.some((c) => countries.indexOf(c) >= 0)) {
return false
}
@ -313,18 +311,20 @@ export default class NameSuggestionIndex {
return true
}
const hasSpecial = i.locationSet.include?.some(i => i.endsWith(".geojson") || Array.isArray(i)) || i.locationSet.exclude?.some(i => i.endsWith(".geojson") || Array.isArray(i))
const hasSpecial =
i.locationSet.include?.some((i) => i.endsWith(".geojson") || Array.isArray(i)) ||
i.locationSet.exclude?.some((i) => i.endsWith(".geojson") || Array.isArray(i))
if (!hasSpecial) {
return false
}
const key = i.locationSet.include?.join(";") + "-" + i.locationSet.exclude?.join(";")
const fromCache = NameSuggestionIndex.resolvedSets[key]
const resolvedSet = fromCache ?? NameSuggestionIndex.loco.resolveLocationSet(i.locationSet)
const resolvedSet =
fromCache ?? NameSuggestionIndex.loco.resolveLocationSet(i.locationSet)
if (!fromCache) {
NameSuggestionIndex.resolvedSets[key] = resolvedSet
}
if (resolvedSet) {
// We actually have a location set, so we can check if the feature is in it, by determining if our point is inside the MultiPolygon using @turf/boolean-point-in-polygon
// This might occur for some extra boundaries, such as counties, ...

View file

@ -58,7 +58,7 @@ export interface P4CPicture {
author?
license?
detailsUrl?: string
direction?: number,
direction?: number
osmTags?: object /*To copy straight into OSM!*/
thumbUrl: string
details: {
@ -103,7 +103,7 @@ class P4CImageFetcher implements ImageFetcher {
{
mindate: new Date().getTime() - maxAgeSeconds,
towardscenter: false,
},
}
)
} catch (e) {
console.log("P4C image fetcher failed with", e)
@ -172,16 +172,13 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher {
constructor(url?: string, radius: number = 100) {
this._radius = radius
if (url) {
this._panoramax = new Panoramax(url)
} else {
this._panoramax = new PanoramaxXYZ()
}
}
public async fetchImages(lat: number, lon: number): Promise<P4CPicture[]> {
const bboxObj = new BBox([
GeoOperations.destination([lon, lat], this._radius * Math.sqrt(2), -45),
GeoOperations.destination([lon, lat], this._radius * Math.sqrt(2), 135),
@ -189,16 +186,16 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher {
const bbox: [number, number, number, number] = bboxObj.toLngLatFlat()
const images = await this._panoramax.search({ bbox, limit: 1000 })
return images.map(i => {
return images.map((i) => {
const [lng, lat] = i.geometry.coordinates
return ({
return {
pictureUrl: i.assets.sd.href,
coordinates: { lng, lat },
provider: "panoramax",
direction: i.properties["view:azimuth"],
osmTags: {
"panoramax": i.id,
panoramax: i.id,
},
thumbUrl: i.assets.thumb.href,
date: new Date(i.properties.datetime).getTime(),
@ -206,9 +203,10 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher {
author: i.providers.at(-1).name,
detailsUrl: i.id,
details: {
isSpherical: i.properties["exif"]["Xmp.GPano.ProjectionType"] === "equirectangular",
isSpherical:
i.properties["exif"]["Xmp.GPano.ProjectionType"] === "equirectangular",
},
})
}
})
}
}
@ -236,7 +234,7 @@ class ImagesFromCacheServerFetcher implements ImageFetcher {
async fetchImagesForType(
targetlat: number,
targetlon: number,
type: "lines" | "pois" | "polygons",
type: "lines" | "pois" | "polygons"
): Promise<P4CPicture[]> {
const { x, y, z } = Tiles.embedded_tile(targetlat, targetlon, 14)
@ -253,7 +251,7 @@ class ImagesFromCacheServerFetcher implements ImageFetcher {
}),
x,
y,
z,
z
)
await src.updateAsync()
return src.features.data
@ -427,7 +425,7 @@ export class CombinedFetcher {
lat: number,
lon: number,
state: UIEventSource<Record<string, "loading" | "done" | "error">>,
sink: UIEventSource<P4CPicture[]>,
sink: UIEventSource<P4CPicture[]>
): Promise<void> {
try {
const pics = await source.fetchImages(lat, lon)
@ -460,7 +458,7 @@ export class CombinedFetcher {
public getImagesAround(
lon: number,
lat: number,
lat: number
): {
images: Store<P4CPicture[]>
state: Store<Record<string, "loading" | "done" | "error">>

View file

@ -1,6 +1,6 @@
import { Utils } from "../../Utils"
import { Store, UIEventSource } from "../UIEventSource"
import { WBK} from "wikibase-sdk"
import { WBK } from "wikibase-sdk"
export class WikidataResponse {
public readonly id: string
@ -128,10 +128,9 @@ interface SparqlResult {
* Utility functions around wikidata
*/
export default class Wikidata {
public static wds = WBK({
instance: "https://wikidata.org",
sparqlEndpoint: "https://query.wikidata.org/bigdata/namespace/wdq/sparql"
sparqlEndpoint: "https://query.wikidata.org/bigdata/namespace/wdq/sparql",
})
public static readonly neededUrls = [
@ -211,7 +210,7 @@ export default class Wikidata {
${instanceOf}
${minusPhrases.join("\n ")}
} ORDER BY ASC(?num) LIMIT ${options?.maxCount ?? 20}`
const url = Wikidata. wds.sparqlQuery(sparql)
const url = Wikidata.wds.sparqlQuery(sparql)
const result = await Utils.downloadJson<SparqlResult>(url)
/*The full uri of the wikidata-item*/
@ -252,7 +251,7 @@ export default class Wikidata {
lang +
"&type=item&origin=*" +
"&props=" // props= removes some unused values in the result
const response = await Utils.downloadJsonCached<{search: any[]}>(url, 10000)
const response = await Utils.downloadJsonCached<{ search: any[] }>(url, 10000)
const result = response.search
@ -401,7 +400,7 @@ export default class Wikidata {
"}"
const url = Wikidata.wds.sparqlQuery(query)
const result = await Utils.downloadJsonCached<SparqlResult>(url, 24 * 60 * 60 * 1000)
return <any> result.results.bindings
return <any>result.results.bindings
}
private static _cache = new Map<string, Promise<WikidataResponse>>()

View file

@ -27,7 +27,7 @@ export default class Constants {
"favourite",
"summary",
"search",
"geocoded_image"
"geocoded_image",
] as const
/**
* Special layers which are not included in a theme by default
@ -50,7 +50,7 @@ export default class Constants {
...Constants.no_include,
] as const
public static panoramax: { url: string, token: string } = packagefile.config.panoramax
public static panoramax: { url: string; token: string } = packagefile.config.panoramax
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {

View file

@ -36,7 +36,7 @@ export default class FilteredLayer {
constructor(
layer: LayerConfig,
appliedFilters?: ReadonlyMap<string, UIEventSource<undefined | number | string>>,
isDisplayed?: UIEventSource<boolean>,
isDisplayed?: UIEventSource<boolean>
) {
this.layerDef = layer
this.isDisplayed = isDisplayed ?? new UIEventSource(true)
@ -82,25 +82,25 @@ export default class FilteredLayer {
layer: LayerConfig,
context: string,
osmConnection: OsmConnection,
enabledByDefault?: Store<boolean>,
enabledByDefault?: Store<boolean>
) {
let isDisplayed: UIEventSource<boolean>
if (layer.syncSelection === "local") {
isDisplayed = LocalStorageSource.getParsed(
context + "-layer-" + layer.id + "-enabled",
layer.shownByDefault,
layer.shownByDefault
)
} else if (layer.syncSelection === "theme-only") {
isDisplayed = FilteredLayer.getPref(
osmConnection,
context + "-layer-" + layer.id + "-enabled",
layer,
layer
)
} else if (layer.syncSelection === "global") {
isDisplayed = FilteredLayer.getPref(
osmConnection,
"layer-" + layer.id + "-enabled",
layer,
layer
)
} else {
let isShown = layer.shownByDefault
@ -110,7 +110,7 @@ export default class FilteredLayer {
isDisplayed = QueryParameters.GetBooleanQueryParameter(
FilteredLayer.queryParameterKey(layer),
isShown,
"Whether or not layer " + layer.id + " is shown",
"Whether or not layer " + layer.id + " is shown"
)
}
@ -145,7 +145,7 @@ export default class FilteredLayer {
*/
private static fieldsToTags(
option: FilterConfigOption,
fieldstate: string | Record<string, string>,
fieldstate: string | Record<string, string>
): TagsFilter | undefined {
let properties: Record<string, string>
if (typeof fieldstate === "string") {
@ -181,7 +181,7 @@ export default class FilteredLayer {
private static getPref(
osmConnection: OsmConnection,
key: string,
layer: LayerConfig,
layer: LayerConfig
): UIEventSource<boolean> {
return osmConnection.GetPreference(key, layer.shownByDefault + "").sync(
(v) => {
@ -196,7 +196,7 @@ export default class FilteredLayer {
return undefined
}
return "" + b
},
}
)
}

View file

@ -32,7 +32,6 @@ export interface MapProperties {
onKeyNavigationEvent(f: (event: KeyNavigationEvent) => void | boolean): () => void
flyTo(lon: number, lat: number, zoom: number): void
}
export interface ExportableMap {

View file

@ -23,25 +23,25 @@ export class AvailableRasterLayers {
}
console.debug("Downloading ELI")
const eli = await Utils.downloadJson<{ features: EditorLayerIndex }>(
"./assets/data/editor-layer-index.json",
"./assets/data/editor-layer-index.json"
)
this._editorLayerIndex = eli.features?.filter((l) => l.properties.id !== "Bing") ?? []
this._editorLayerIndexStore.set(this._editorLayerIndex)
return this._editorLayerIndex
}
public static readonly globalLayers: ReadonlyArray<RasterLayerPolygon> = AvailableRasterLayers.initGlobalLayers()
public static readonly globalLayers: ReadonlyArray<RasterLayerPolygon> =
AvailableRasterLayers.initGlobalLayers()
private static initGlobalLayers(): RasterLayerPolygon[] {
const gl: RasterLayerProperties[] = (globallayers["default"] ?? globallayers ).layers
.filter(
(properties) =>
properties.id !== "osm.carto" && properties.id !== "Bing", /*Added separately*/
)
const gl: RasterLayerProperties[] = (globallayers["default"] ?? globallayers).layers.filter(
(properties) =>
properties.id !== "osm.carto" && properties.id !== "Bing" /*Added separately*/
)
const glEli: RasterLayerProperties[] = globallayersEli["default"] ?? globallayersEli
const joined = gl.concat(glEli)
if (joined.some(j => !j.id)) {
console.log("Invalid layers:", JSON.stringify(joined .filter(l => !l.id)))
if (joined.some((j) => !j.id)) {
console.log("Invalid layers:", JSON.stringify(joined.filter((l) => !l.id)))
throw "Detected invalid global layer with invalid id"
}
return joined.map(
@ -50,7 +50,7 @@ export class AvailableRasterLayers {
type: "Feature",
properties,
geometry: BBox.global.asGeometry(),
},
}
)
}
@ -85,18 +85,18 @@ export class AvailableRasterLayers {
public static layersAvailableAt(
location: Store<{ lon: number; lat: number }>,
enableBing?: Store<boolean>,
enableBing?: Store<boolean>
): { store: Store<RasterLayerPolygon[]> } {
const store = { store: undefined }
Utils.AddLazyProperty(store, "store", () =>
AvailableRasterLayers._layersAvailableAt(location, enableBing),
AvailableRasterLayers._layersAvailableAt(location, enableBing)
)
return store
}
private static _layersAvailableAt(
location: Store<{ lon: number; lat: number }>,
enableBing?: Store<boolean>,
enableBing?: Store<boolean>
): Store<RasterLayerPolygon[]> {
this.editorLayerIndex() // start the download
const availableLayersBboxes = Stores.ListStabilized(
@ -109,8 +109,8 @@ export class AvailableRasterLayers {
const lonlat: [number, number] = [loc.lon, loc.lat]
return eli.filter((eliPolygon) => BBox.get(eliPolygon).contains(lonlat))
},
[AvailableRasterLayers._editorLayerIndexStore],
),
[AvailableRasterLayers._editorLayerIndexStore]
)
)
return Stores.ListStabilized(
availableLayersBboxes.map(
@ -132,15 +132,15 @@ export class AvailableRasterLayers {
if (
!matching.some(
(l) =>
l.id === AvailableRasterLayers.defaultBackgroundLayer.properties.id,
l.id === AvailableRasterLayers.defaultBackgroundLayer.properties.id
)
) {
matching.push(AvailableRasterLayers.defaultBackgroundLayer)
}
return matching
},
[enableBing],
),
[enableBing]
)
)
}
}
@ -159,7 +159,7 @@ export class RasterLayerUtils {
available: RasterLayerPolygon[],
preferredCategory: string,
ignoreLayer?: RasterLayerPolygon,
skipLayers: number = 0,
skipLayers: number = 0
): RasterLayerPolygon {
const inCategory = available.filter((l) => l.properties.category === preferredCategory)
const best: RasterLayerPolygon[] = inCategory.filter((l) => l.properties.best)
@ -167,7 +167,7 @@ export class RasterLayerUtils {
let all = best.concat(others)
console.log(
"Selected layers are:",
all.map((l) => l.properties.id),
all.map((l) => l.properties.id)
)
if (others.length > skipLayers) {
all = all.slice(skipLayers)

View file

@ -10,7 +10,10 @@ import {
SetDefault,
} from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import {
MinimalTagRenderingConfigJson,
TagRenderingConfigJson,
} from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils"
import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
@ -31,23 +34,26 @@ import { ConversionContext } from "./ConversionContext"
import { ExpandRewrite } from "./ExpandRewrite"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
constructor() {
super("Inspects all the tagRenderings. If some tagRenderings have the `filter` attribute set, introduce those filters. This step might introduce shorthand filter names, thus 'ExpandFilter' should be run afterwards. Can be disabled with \"#filter\":\"no-auto\"", ["filter"], "AddFiltersFromTagRenderings")
super(
'Inspects all the tagRenderings. If some tagRenderings have the `filter` attribute set, introduce those filters. This step might introduce shorthand filter names, thus \'ExpandFilter\' should be run afterwards. Can be disabled with "#filter":"no-auto"',
["filter"],
"AddFiltersFromTagRenderings"
)
}
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
const noAutoFilters = json["#filter"] === "no-auto"
if(noAutoFilters){
if (noAutoFilters) {
return json
}
if(json.filter?.["sameAs"]){
if (json.filter?.["sameAs"]) {
return json
}
const filters: (FilterConfigJson | string)[] = [...<any>json.filter ?? []]
const filters: (FilterConfigJson | string)[] = [...(<any>json.filter ?? [])]
function filterExists(filterName: string): boolean {
return filters.some((existing) => {
@ -59,8 +65,6 @@ class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
})
}
for (let i = 0; i < json.tagRenderings?.length; i++) {
const tagRendering = <TagRenderingConfigJson>json.tagRenderings[i]
if (!tagRendering?.filter) {
@ -70,7 +74,12 @@ class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
if (filterExists(tagRendering["id"])) {
continue
}
filters.push(ExpandFilter.buildFilterFromTagRendering(tagRendering, context.enters("tagRenderings", i, "filter")))
filters.push(
ExpandFilter.buildFilterFromTagRendering(
tagRendering,
context.enters("tagRenderings", i, "filter")
)
)
continue
}
for (const filterName of tagRendering.filter ?? []) {
@ -89,7 +98,7 @@ class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
}
}
if(filters.length === 0){
if (filters.length === 0) {
return json
}
@ -102,10 +111,12 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
constructor(state: DesugaringContext) {
super(
["Expands filters: replaces a shorthand by the value found in 'filters.json'.",
"If the string is formatted 'layername.filtername, it will be looked up into that layer instead."].join(" "),
[
"Expands filters: replaces a shorthand by the value found in 'filters.json'.",
"If the string is formatted 'layername.filtername, it will be looked up into that layer instead.",
].join(" "),
["filter"],
"ExpandFilter",
"ExpandFilter"
)
this._state = state
}
@ -118,10 +129,13 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
return filters
}
public static buildFilterFromTagRendering(tr: TagRenderingConfigJson, context: ConversionContext): FilterConfigJson {
public static buildFilterFromTagRendering(
tr: TagRenderingConfigJson,
context: ConversionContext
): FilterConfigJson {
if (!(tr.mappings?.length >= 1)) {
context.err(
"Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings",
"Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings"
)
}
const options = (<QuestionableTagRenderingConfigJson>tr).mappings.map((mapping) => {
@ -131,12 +145,13 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
emoji = icon
icon = undefined
}
return (<FilterConfigOptionJson>{
return <FilterConfigOptionJson>{
question: mapping.then,
osmTags: mapping.if,
searchTerms: mapping.searchTerms,
icon, emoji,
})
icon,
emoji,
}
})
// Add default option
options.unshift({
@ -144,10 +159,10 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
osmTags: undefined,
searchTerms: undefined,
})
return ({
return {
id: tr["id"],
options,
})
}
}
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
@ -159,11 +174,9 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
return json // Nothing to change here
}
const newFilters: FilterConfigJson[] = []
const filters = <(FilterConfigJson | string)[]>json.filter
/**
* Create filters based on builtin filters or create them based on the tagRendering
*/
@ -181,7 +194,10 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
json.tagRenderings.find((tr) => !!tr && tr["id"] === filter)
)
if (matchingTr) {
const filter = ExpandFilter.buildFilterFromTagRendering(matchingTr, context.enters("filter", i))
const filter = ExpandFilter.buildFilterFromTagRendering(
matchingTr,
context.enters("filter", i)
)
newFilters.push(filter)
continue
}
@ -194,7 +210,7 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
const split = filter.split(".")
if (split.length > 2) {
context.err(
"invalid filter name: " + filter + ", expected `layername.filterid`",
"invalid filter name: " + filter + ", expected `layername.filterid`"
)
}
const layer = this._state.sharedLayers.get(split[0])
@ -203,7 +219,7 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
}
const expectedId = split[1]
const expandedFilter = (<(FilterConfigJson | string)[]>layer.filter).find(
(f) => typeof f !== "string" && f.id === expectedId,
(f) => typeof f !== "string" && f.id === expectedId
)
if (expandedFilter === undefined) {
context.err("Did not find filter with name " + filter)
@ -218,15 +234,15 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
const suggestions = Utils.sortedByLevenshteinDistance(
filter,
Array.from(ExpandFilter.predefinedFilters.keys()),
(t) => t,
(t) => t
)
context
.enter(filter)
.err(
"While searching for predefined filter " +
filter +
": this filter is not found. Perhaps you meant one of: " +
suggestions,
filter +
": this filter is not found. Perhaps you meant one of: " +
suggestions
)
}
newFilters.push(found)
@ -239,9 +255,9 @@ class ExpandTagRendering extends Conversion<
| string
| TagRenderingConfigJson
| {
builtin: string | string[]
override: any
},
builtin: string | string[]
override: any
},
TagRenderingConfigJson[]
> {
private readonly _state: DesugaringContext
@ -263,12 +279,12 @@ class ExpandTagRendering extends Conversion<
noHardcodedStrings?: false | boolean
// If set, a question will be added to the 'sharedTagRenderings'. Should only be used for 'questions.json'
addToContext?: false | boolean
},
}
) {
super(
"Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question and reusing the builtins",
[],
"ExpandTagRendering",
"ExpandTagRendering"
)
this._state = state
this._self = self
@ -288,7 +304,7 @@ class ExpandTagRendering extends Conversion<
public convert(
spec: string | any,
ctx: ConversionContext,
ctx: ConversionContext
): QuestionableTagRenderingConfigJson[] {
const trs = this.convertOnce(spec, ctx)
@ -401,8 +417,8 @@ class ExpandTagRendering extends Conversion<
found,
ConversionContext.construct(
[layer.id, "tagRenderings", found["id"]],
["AddContextToTranslations"],
),
["AddContextToTranslations"]
)
)
matchingTrs[i] = found
}
@ -430,17 +446,17 @@ class ExpandTagRendering extends Conversion<
ctx.warn(
`A literal rendering was detected: ${tr}
Did you perhaps forgot to add a layer name as 'layername.${tr}'? ` +
Array.from(state.sharedLayers.keys()).join(", "),
Array.from(state.sharedLayers.keys()).join(", ")
)
}
if (this._options?.noHardcodedStrings && this._state?.sharedLayers?.size > 0) {
ctx.err(
"Detected an invocation to a builtin tagRendering, but this tagrendering was not found: " +
tr +
" \n Did you perhaps forget to add the layer as prefix, such as `icons." +
tr +
"`? ",
tr +
" \n Did you perhaps forget to add the layer as prefix, such as `icons." +
tr +
"`? "
)
}
@ -475,9 +491,9 @@ class ExpandTagRendering extends Conversion<
}
ctx.err(
"An object calling a builtin can only have keys `builtin` or `override`, but a key with name `" +
key +
"` was found. This won't be picked up! The full object is: " +
JSON.stringify(tr),
key +
"` was found. This won't be picked up! The full object is: " +
JSON.stringify(tr)
)
}
@ -496,39 +512,39 @@ class ExpandTagRendering extends Conversion<
const candidates = Utils.sortedByLevenshteinDistance(
layerName,
Array.from(state.sharedLayers.keys()),
(s) => s,
(s) => s
)
if (state.sharedLayers.size === 0) {
ctx.warn(
"BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " +
name +
": layer " +
layerName +
" not found for now, but ignoring as this is a bootstrapping run. ",
name +
": layer " +
layerName +
" not found for now, but ignoring as this is a bootstrapping run. "
)
} else {
ctx.err(
": While reusing tagrendering: " +
name +
": layer " +
layerName +
" not found. Maybe you meant one of " +
candidates.slice(0, 3).join(", "),
name +
": layer " +
layerName +
" not found. Maybe you meant one of " +
candidates.slice(0, 3).join(", ")
)
}
continue
}
candidates = Utils.NoNull(layer.tagRenderings.map((tr) => tr["id"])).map(
(id) => layerName + "." + id,
(id) => layerName + "." + id
)
}
candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i)
ctx.err(
"The tagRendering with identifier " +
name +
" was not found.\n\tDid you mean one of " +
candidates.join(", ") +
"?\n(Hint: did you add a new label and are you trying to use this label at the same time? Run 'reset:layeroverview' first",
name +
" was not found.\n\tDid you mean one of " +
candidates.join(", ") +
"?\n(Hint: did you add a new label and are you trying to use this label at the same time? Run 'reset:layeroverview' first"
)
continue
}
@ -553,13 +569,13 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
super(
"If no 'inline' is set on the freeform key, it will be automatically added. If no special renderings are used, it'll be set to true",
["freeform.inline"],
"DetectInline",
"DetectInline"
)
}
convert(
json: QuestionableTagRenderingConfigJson,
context: ConversionContext,
context: ConversionContext
): QuestionableTagRenderingConfigJson {
if (json.freeform === undefined) {
return json
@ -582,7 +598,7 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
if (json.freeform.inline === true) {
context.err(
"'inline' is set, but the rendering contains a special visualisation...\n " +
spec[key],
spec[key]
)
}
json = JSON.parse(JSON.stringify(json))
@ -596,17 +612,18 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
return json
}
if(json.render === undefined){
if (json.render === undefined) {
context.err("No 'render' defined")
return json
}
if(!Object.values(json?.render)?.some(render => render !== "{"+json.freeform.key+"}")){
if (
!Object.values(json?.render)?.some((render) => render !== "{" + json.freeform.key + "}")
) {
// We only render the current value, without anything more. Not worth inlining
return json
}
json.freeform.inline ??= true
return json
}
@ -617,7 +634,7 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
super(
"Adds a 'questions'-object if no question element is added yet",
["tagRenderings"],
"AddQuestionBox",
"AddQuestionBox"
)
}
@ -641,18 +658,18 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
json.tagRenderings = [...json.tagRenderings]
const allSpecials: Exclude<RenderingSpecification, string>[] = <any>(
ValidationUtils.getAllSpecialVisualisations(
<QuestionableTagRenderingConfigJson[]>json.tagRenderings,
<QuestionableTagRenderingConfigJson[]>json.tagRenderings
).filter((spec) => typeof spec !== "string")
)
const questionSpecials = allSpecials.filter((sp) => sp.func.funcName === "questions")
const noLabels = questionSpecials.filter(
(sp) => sp.args.length === 0 || sp.args[0].trim() === "",
(sp) => sp.args.length === 0 || sp.args[0].trim() === ""
)
if (noLabels.length > 1) {
context.err(
"Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this",
"Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this"
)
}
@ -660,9 +677,9 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
const allLabels = new Set(
[].concat(
...json.tagRenderings.map(
(tr) => (<QuestionableTagRenderingConfigJson>tr).labels ?? [],
),
),
(tr) => (<QuestionableTagRenderingConfigJson>tr).labels ?? []
)
)
)
const seen: Set<string> = new Set()
for (const questionSpecial of questionSpecials) {
@ -680,20 +697,20 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
if (blacklisted?.length > 0 && used?.length > 0) {
context.err(
"The {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." +
"\n Whitelisted: " +
used.join(", ") +
"\n Blacklisted: " +
blacklisted.join(", "),
"\n Whitelisted: " +
used.join(", ") +
"\n Blacklisted: " +
blacklisted.join(", ")
)
}
for (const usedLabel of used) {
if (!allLabels.has(usedLabel)) {
context.err(
"This layers specifies a special question element for label `" +
usedLabel +
"`, but this label doesn't exist.\n" +
" Available labels are " +
Array.from(allLabels).join(", "),
usedLabel +
"`, but this label doesn't exist.\n" +
" Available labels are " +
Array.from(allLabels).join(", ")
)
}
seen.add(usedLabel)
@ -726,7 +743,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
super(
"Add some editing elements, such as the delete button or the move button if they are configured. These used to be handled by the feature info box, but this has been replaced by special visualisation elements",
[],
"AddEditingElements",
"AddEditingElements"
)
this._desugaring = desugaring
this.builtinQuestions = Array.from(this._desugaring.tagRenderings?.values() ?? [])
@ -756,13 +773,13 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
json.tagRenderings = [...(json.tagRenderings ?? [])]
const allIds = new Set<string>(json.tagRenderings.map((tr) => tr["id"]))
const specialVisualisations = ValidationUtils.getAllSpecialVisualisations(
<any>json.tagRenderings,
<any>json.tagRenderings
)
const usedSpecialFunctions = new Set(
specialVisualisations.map((sv) =>
typeof sv === "string" ? undefined : sv.func.funcName,
),
typeof sv === "string" ? undefined : sv.func.funcName
)
)
/***** ADD TO TOP ****/
@ -830,7 +847,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
super(
"Converts a 'special' translation into a regular translation which uses parameters",
["special"],
"RewriteSpecial",
"RewriteSpecial"
)
}
@ -921,12 +938,12 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
private static convertIfNeeded(
input:
| (object & {
special: {
type: string
}
})
special: {
type: string
}
})
| any,
context: ConversionContext,
context: ConversionContext
): any {
const special = input["special"]
if (special === undefined) {
@ -936,7 +953,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
const type = special["type"]
if (type === undefined) {
context.err(
"A 'special'-block should define 'type' to indicate which visualisation should be used",
"A 'special'-block should define 'type' to indicate which visualisation should be used"
)
return undefined
}
@ -946,10 +963,10 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
const options = Utils.sortedByLevenshteinDistance(
type,
SpecialVisualizations.specialVisualizations,
(sp) => sp.funcName,
(sp) => sp.funcName
)
context.err(
`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`,
`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`
)
return undefined
}
@ -970,7 +987,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
const byDistance = Utils.sortedByLevenshteinDistance(
wrongArg,
argNamesList,
(x) => x,
(x) => x
)
return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${
byDistance[0]
@ -989,8 +1006,8 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
`Obligated parameter '${arg.name}' in special rendering of type ${
vis.funcName
} not found.\n The full special rendering specification is: '${JSON.stringify(
input,
)}'\n ${arg.name}: ${arg.doc}`,
input
)}'\n ${arg.name}: ${arg.doc}`
)
}
}
@ -1092,7 +1109,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
continue
}
Utils.WalkPath(path.path, json, (leaf, travelled) =>
RewriteSpecial.convertIfNeeded(leaf, context.enter(travelled)),
RewriteSpecial.convertIfNeeded(leaf, context.enter(travelled))
)
}
@ -1126,7 +1143,7 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
} = badgesJson[i]
const expanded = this._expand.convert(
<QuestionableTagRenderingConfigJson>iconBadge.then,
context.enters("iconBadges", i),
context.enters("iconBadges", i)
)
if (expanded === undefined) {
iconBadges.push(iconBadge)
@ -1137,7 +1154,7 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
...expanded.map((resolved) => ({
if: iconBadge.if,
then: <MinimalTagRenderingConfigJson>resolved,
})),
}))
)
}
@ -1154,11 +1171,11 @@ class PreparePointRendering extends Fuse<PointRenderingConfigJson> {
new Each(
new On(
"icon",
new FirstOf(new ExpandTagRendering(state, layer, { applyCondition: false })),
),
),
new FirstOf(new ExpandTagRendering(state, layer, { applyCondition: false }))
)
)
),
new ExpandIconBadges(state, layer),
new ExpandIconBadges(state, layer)
)
}
}
@ -1168,7 +1185,7 @@ class SetFullNodeDatabase extends DesugaringStep<LayerConfigJson> {
super(
"sets the fullNodeDatabase-bit if needed",
["fullNodeDatabase"],
"SetFullNodeDatabase",
"SetFullNodeDatabase"
)
}
@ -1197,7 +1214,7 @@ class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> {
super(
"Expands tagRenderings in the icons, if needed",
["icon", "color"],
"ExpandMarkerRenderings",
"ExpandMarkerRenderings"
)
this._layer = layer
this._state = state
@ -1229,7 +1246,7 @@ class AddFavouriteBadges extends DesugaringStep<LayerConfigJson> {
super(
"Adds the favourite heart to the title and the rendering badges",
[],
"AddFavouriteBadges",
"AddFavouriteBadges"
)
}
@ -1254,7 +1271,7 @@ export class AddRatingBadge extends DesugaringStep<LayerConfigJson> {
super(
"Adds the 'rating'-element if a reviews-element is used in the tagRenderings",
["titleIcons"],
"AddRatingBadge",
"AddRatingBadge"
)
}
@ -1273,8 +1290,8 @@ export class AddRatingBadge extends DesugaringStep<LayerConfigJson> {
const specialVis: Exclude<RenderingSpecification, string>[] = <
Exclude<RenderingSpecification, string>[]
>ValidationUtils.getAllSpecialVisualisations(<any>json.tagRenderings).filter(
(rs) => typeof rs !== "string",
>ValidationUtils.getAllSpecialVisualisations(<any>json.tagRenderings).filter(
(rs) => typeof rs !== "string"
)
const funcs = new Set<string>(specialVis.map((rs) => rs.func.funcName))
@ -1290,12 +1307,12 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
super(
"The auto-icon creates a (non-clickable) title icon based on a tagRendering which has icons",
["titleIcons"],
"AutoTitleIcon",
"AutoTitleIcon"
)
}
private createTitleIconsBasedOn(
tr: QuestionableTagRenderingConfigJson,
tr: QuestionableTagRenderingConfigJson
): TagRenderingConfigJson | undefined {
const mappings: { if: TagConfigJson; then: string }[] = tr.mappings
?.filter((m) => m.icon !== undefined)
@ -1325,7 +1342,7 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
return undefined
}
return this.createTitleIconsBasedOn(<any>tr)
}),
})
)
json.titleIcons.splice(allAutoIndex, 1, ...generated)
return json
@ -1354,8 +1371,8 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
.enters("titleIcons", i)
.warn(
"TagRendering with id " +
trId +
" does not have any icons, not generating an icon for this",
trId +
" does not have any icons, not generating an icon for this"
)
continue
}
@ -1370,7 +1387,7 @@ class DeriveSource extends DesugaringStep<LayerConfigJson> {
super(
"If no source is given, automatically derives the osmTags by 'or'-ing all the preset tags",
["source"],
"DeriveSource",
"DeriveSource"
)
}
@ -1380,7 +1397,7 @@ class DeriveSource extends DesugaringStep<LayerConfigJson> {
}
if (!json.presets) {
context.err(
"No source tags given. Trying to derive the source-tags based on the presets, but no presets are given",
"No source tags given. Trying to derive the source-tags based on the presets, but no presets are given"
)
return json
}
@ -1406,7 +1423,7 @@ class DeriveSource extends DesugaringStep<LayerConfigJson> {
export class PrepareLayer extends Fuse<LayerConfigJson> {
constructor(
state: DesugaringContext,
options?: { addTagRenderingsToContext?: false | boolean },
options?: { addTagRenderingsToContext?: false | boolean }
) {
super(
"Fully prepares and expands a layer for the LayerConfig.",
@ -1419,8 +1436,8 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
new Concat(
new ExpandTagRendering(state, layer, {
addToContext: options?.addTagRenderingsToContext ?? false,
}),
),
})
)
),
new On("tagRenderings", new Each(new DetectInline())),
new AddQuestionBox(),
@ -1433,11 +1450,11 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
new On<PointRenderingConfigJson[], LayerConfigJson>(
"pointRendering",
(layer) =>
new Each(new On("marker", new Each(new ExpandMarkerRenderings(state, layer)))),
new Each(new On("marker", new Each(new ExpandMarkerRenderings(state, layer))))
),
new On<PointRenderingConfigJson[], LayerConfigJson>(
"pointRendering",
(layer) => new Each(new PreparePointRendering(state, layer)),
(layer) => new Each(new PreparePointRendering(state, layer))
),
new SetDefault("titleIcons", ["icons.defaults"]),
new AddRatingBadge(),
@ -1446,10 +1463,10 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
new On(
"titleIcons",
(layer) =>
new Concat(new ExpandTagRendering(state, layer, { noHardcodedStrings: true })),
new Concat(new ExpandTagRendering(state, layer, { noHardcodedStrings: true }))
),
new AddFiltersFromTagRenderings(),
new ExpandFilter(state),
new ExpandFilter(state)
)
}
}

View file

@ -1,4 +1,14 @@
import { Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, Pass, SetDefault } from "./Conversion"
import {
Concat,
Conversion,
DesugaringContext,
DesugaringStep,
Each,
Fuse,
On,
Pass,
SetDefault,
} from "./Conversion"
import { ThemeConfigJson } from "../Json/ThemeConfigJson"
import { PrepareLayer } from "./PrepareLayer"
import { LayerConfigJson } from "../Json/LayerConfigJson"
@ -18,7 +28,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
super(
"Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form. Note that 'tagRenderings+' will be inserted before 'leftover-questions'",
[],
"SubstituteLayer",
"SubstituteLayer"
)
this._state = state
}
@ -28,7 +38,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
function reportNotFound(name: string) {
const knownLayers = Array.from(state.sharedLayers.keys())
const withDistance:[string,number][] = knownLayers.map((lname) => [
const withDistance: [string, number][] = knownLayers.map((lname) => [
lname,
Utils.levenshteinDistance(name, lname),
])
@ -74,14 +84,14 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
(found["tagRenderings"] ?? []).length > 0
) {
context.err(
`When overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`,
`When overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`
)
}
try {
const trPlus = json["override"]["tagRenderings+"]
if (trPlus) {
let index = found.tagRenderings.findIndex(
(tr) => tr["id"] === "leftover-questions",
(tr) => tr["id"] === "leftover-questions"
)
if (index < 0) {
index = found.tagRenderings.length
@ -95,8 +105,8 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
} catch (e) {
context.err(
`Could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(
json["override"],
)}`,
json["override"]
)}`
)
}
@ -120,9 +130,9 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
usedLabels.add(labels[forbiddenLabel])
context.info(
"Dropping tagRendering " +
tr["id"] +
" as it has a forbidden label: " +
labels[forbiddenLabel],
tr["id"] +
" as it has a forbidden label: " +
labels[forbiddenLabel]
)
continue
}
@ -131,7 +141,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
if (hideLabels.has(tr["id"])) {
usedLabels.add(tr["id"])
context.info(
"Dropping tagRendering " + tr["id"] + " as its id is a forbidden label",
"Dropping tagRendering " + tr["id"] + " as its id is a forbidden label"
)
continue
}
@ -140,10 +150,10 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
usedLabels.add(tr["group"])
context.info(
"Dropping tagRendering " +
tr["id"] +
" as its group `" +
tr["group"] +
"` is a forbidden label",
tr["id"] +
" as its group `" +
tr["group"] +
"` is a forbidden label"
)
continue
}
@ -154,8 +164,8 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
if (unused.length > 0) {
context.err(
"This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " +
unused.join(", ") +
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore",
unused.join(", ") +
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore"
)
}
found.tagRenderings = filtered
@ -172,7 +182,7 @@ class AddDefaultLayers extends DesugaringStep<ThemeConfigJson> {
super(
"Adds the default layers, namely: " + Constants.added_by_default.join(", "),
["layers"],
"AddDefaultLayers",
"AddDefaultLayers"
)
this._state = state
}
@ -195,10 +205,10 @@ class AddDefaultLayers extends DesugaringStep<ThemeConfigJson> {
if (alreadyLoaded.has(v.id)) {
context.warn(
"Layout " +
context +
" already has a layer with name " +
v.id +
"; skipping inclusion of this builtin layer",
context +
" already has a layer with name " +
v.id +
"; skipping inclusion of this builtin layer"
)
continue
}
@ -214,7 +224,7 @@ class AddContextToTranslationsInLayout extends DesugaringStep<ThemeConfigJson> {
super(
"Adds context to translations, including the prefix 'themes:json.id'; this is to make sure terms in an 'overrides' or inline layer are linkable too",
["_context"],
"AddContextToTranlationsInLayout",
"AddContextToTranlationsInLayout"
)
}
@ -223,7 +233,7 @@ class AddContextToTranslationsInLayout extends DesugaringStep<ThemeConfigJson> {
// 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"]),
ConversionContext.construct([json.id], ["AddContextToTranslation"])
)
}
}
@ -233,7 +243,7 @@ class ApplyOverrideAll extends DesugaringStep<ThemeConfigJson> {
super(
"Applies 'overrideAll' onto every 'layer'. The 'overrideAll'-field is removed afterwards",
["overrideAll", "layers"],
"ApplyOverrideAll",
"ApplyOverrideAll"
)
}
@ -262,7 +272,7 @@ class ApplyOverrideAll extends DesugaringStep<ThemeConfigJson> {
layer.tagRenderings = tagRenderingsPlus
} else {
let index = layer.tagRenderings.findIndex(
(tr) => tr["id"] === "leftover-questions",
(tr) => tr["id"] === "leftover-questions"
)
if (index < 0) {
index = layer.tagRenderings.length - 1
@ -285,7 +295,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
super(
`If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)`,
["layers"],
"AddDependencyLayersToTheme",
"AddDependencyLayersToTheme"
)
this._state = state
}
@ -294,7 +304,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
alreadyLoaded: LayerConfigJson[],
allKnownLayers: Map<string, LayerConfigJson>,
themeId: string,
context: ConversionContext,
context: ConversionContext
): { config: LayerConfigJson; reason: string }[] {
const dependenciesToAdd: { config: LayerConfigJson; reason: string }[] = []
const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map((l) => l?.id))
@ -318,7 +328,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
for (const layerConfig of alreadyLoaded) {
try {
const layerDeps = DependencyCalculator.getLayerDependencies(
new LayerConfig(layerConfig, themeId + "(dependencies)"),
new LayerConfig(layerConfig, themeId + "(dependencies)")
)
dependencies.push(...layerDeps)
} catch (e) {
@ -337,8 +347,16 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
// We mark the needed layer as 'mustLoad'
const loadedLayer = alreadyLoaded.find((l) => l.id === dependency.neededLayer)
loadedLayer.forceLoad = true
if(dependency.checkHasSnapName && !loadedLayer.snapName){
context.enters("layer dependency").err("Layer "+dependency.neededLayer+" is loaded because "+dependency.reason+"; so it must specify a `snapName`. This is used in the sentence `move this point to snap it to {snapName}`")
if (dependency.checkHasSnapName && !loadedLayer.snapName) {
context
.enters("layer dependency")
.err(
"Layer " +
dependency.neededLayer +
" is loaded because " +
dependency.reason +
"; so it must specify a `snapName`. This is used in the sentence `move this point to snap it to {snapName}`"
)
}
}
}
@ -362,10 +380,10 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
if (dep === undefined) {
const message = [
"Loading a dependency failed: layer " +
unmetDependency.neededLayer +
" is not found, neither as layer of " +
themeId +
" nor as builtin layer.",
unmetDependency.neededLayer +
" is not found, neither as layer of " +
themeId +
" nor as builtin layer.",
reason,
"Loaded layers are: " + alreadyLoaded.map((l) => l.id).join(","),
]
@ -381,12 +399,11 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
})
loadedLayerIds.add(dep.id)
unmetDependencies = unmetDependencies.filter(
(d) => d.neededLayer !== unmetDependency.neededLayer,
(d) => d.neededLayer !== unmetDependency.neededLayer
)
}
} while (unmetDependencies.length > 0)
return dependenciesToAdd
}
@ -404,12 +421,12 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
layers,
allKnownLayers,
theme.id,
context,
context
)
if (dependencies.length > 0) {
for (const dependency of dependencies) {
context.info(
"Added " + dependency.config.id + " to the theme. " + dependency.reason,
"Added " + dependency.config.id + " to the theme. " + dependency.reason
)
}
}
@ -457,7 +474,7 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<ThemeConfigJson>
super(
"Generates a warning if a theme uses an unsubstituted layer",
["layers"],
"WarnForUnsubstitutedLayersInTheme",
"WarnForUnsubstitutedLayersInTheme"
)
}
@ -469,7 +486,7 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<ThemeConfigJson>
context
.enter("layers")
.err(
"No layers are defined. You must define at least one layer to have a valid theme",
"No layers are defined. You must define at least one layer to have a valid theme"
)
return json
}
@ -493,10 +510,10 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<ThemeConfigJson>
context.warn(
"The theme " +
json.id +
" has an inline layer: " +
layer["id"] +
". This is discouraged.",
json.id +
" has an inline layer: " +
layer["id"] +
". This is discouraged."
)
}
return json
@ -523,13 +540,13 @@ class PostvalidateTheme extends DesugaringStep<ThemeConfigJson> {
}
const sameBasedOn = <LayerConfigJson[]>(
json.layers.filter(
(l) => l["_basedOn"] === layer["_basedOn"] && l["id"] !== layer.id,
(l) => l["_basedOn"] === layer["_basedOn"] && l["id"] !== layer.id
)
)
const minZoomAll = Math.min(...sameBasedOn.map((sbo) => sbo.minzoom))
const sameNameDetected = sameBasedOn.some(
(same) => JSON.stringify(layer["name"]) === JSON.stringify(same["name"]),
(same) => JSON.stringify(layer["name"]) === JSON.stringify(same["name"])
)
if (!sameNameDetected) {
// The name is unique, so it'll won't be confusing
@ -538,12 +555,12 @@ class PostvalidateTheme extends DesugaringStep<ThemeConfigJson> {
if (minZoomAll < layer.minzoom) {
context.err(
"There are multiple layers based on " +
basedOn +
". The layer with id " +
layer.id +
" has a minzoom of " +
layer.minzoom +
", and has a name set. Another similar layer has a lower minzoom. As such, the layer selection might show 'zoom in to see features' even though some of the features are already visible. Set `\"name\": null` for this layer and eventually remove the 'name':null for the other layer.",
basedOn +
". The layer with id " +
layer.id +
" has a minzoom of " +
layer.minzoom +
", and has a name set. Another similar layer has a lower minzoom. As such, the layer selection might show 'zoom in to see features' even though some of the features are already visible. Set `\"name\": null` for this layer and eventually remove the 'name':null for the other layer."
)
}
}
@ -563,17 +580,17 @@ class PostvalidateTheme extends DesugaringStep<ThemeConfigJson> {
const closeLayers = Utils.sortedByLevenshteinDistance(
sameAs,
json.layers,
(l) => l["id"],
(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(", "),
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(", ")
)
}
}
@ -589,7 +606,7 @@ export class PrepareTheme extends Fuse<ThemeConfigJson> {
state: DesugaringContext,
options?: {
skipDefaultLayers: false | boolean
},
}
) {
super(
"Fully prepares and expands a theme",
@ -610,8 +627,8 @@ export class PrepareTheme extends Fuse<ThemeConfigJson> {
? new Pass("AddDefaultLayers is disabled due to the set flag")
: new AddDefaultLayers(state),
new AddDependencyLayersToTheme(state),
// new AddImportLayers(),
new PostvalidateTheme(state),
// new AddImportLayers(),
new PostvalidateTheme(state)
)
this.state = state
}
@ -626,13 +643,13 @@ export class PrepareTheme extends Fuse<ThemeConfigJson> {
const needsNodeDatabase = result.layers?.some((l: LayerConfigJson) =>
l.tagRenderings?.some((tr) =>
ValidationUtils.getSpecialVisualisations(<any>tr)?.some(
(special) => special.needsNodeDatabase,
),
),
(special) => special.needsNodeDatabase
)
)
)
if (needsNodeDatabase) {
context.info(
"Setting 'enableNodeDatabase' as this theme uses a special visualisation which needs to keep track of _all_ nodes",
"Setting 'enableNodeDatabase' as this theme uses a special visualisation which needs to keep track of _all_ nodes"
)
result.enableNodeDatabase = true
}

View file

@ -88,7 +88,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
}
}
if(json["doCount"] !== undefined){
if (json["doCount"] !== undefined) {
context.err("Detected 'doCount'. did you mean: isCounted ?")
}
@ -145,7 +145,11 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
}
}
if(this._isBuiltin && json.allowMove === undefined && json.source["geoJson"] === undefined) {
if (
this._isBuiltin &&
json.allowMove === undefined &&
json.source["geoJson"] === undefined
) {
if (!Constants.priviliged_layers.find((x) => x == json.id)) {
context.err("Layer " + json.id + " does not have an explicit 'allowMove'")
}

View file

@ -30,7 +30,7 @@ export class ValidateLanguageCompleteness extends DesugaringStep<ThemeConfig> {
super(
"Checks that the given object is fully translated in the specified languages",
[],
"ValidateLanguageCompleteness",
"ValidateLanguageCompleteness"
)
this._languages = languages ?? ["en"]
}
@ -44,18 +44,18 @@ export class ValidateLanguageCompleteness extends DesugaringStep<ThemeConfig> {
.filter(
(t) =>
t.tr.translations[neededLanguage] === undefined &&
t.tr.translations["*"] === undefined,
t.tr.translations["*"] === undefined
)
.forEach((missing) => {
context
.enter(missing.context.split("."))
.err(
`The theme ${obj.id} should be translation-complete for ` +
neededLanguage +
", but it lacks a translation for " +
missing.context +
".\n\tThe known translation is " +
missing.tr.textFor("en"),
neededLanguage +
", but it lacks a translation for " +
missing.context +
".\n\tThe known translation is " +
missing.tr.textFor("en")
)
})
}
@ -72,7 +72,7 @@ export class DoesImageExist extends DesugaringStep<string> {
constructor(
knownImagePaths: Set<string>,
checkExistsSync: (path: string) => boolean = undefined,
ignore?: Set<string>,
ignore?: Set<string>
) {
super("Checks if an image exists", [], "DoesImageExist")
this._ignore = ignore
@ -112,15 +112,15 @@ export class DoesImageExist extends DesugaringStep<string> {
if (!this._knownImagePaths.has(image)) {
if (this.doesPathExist === undefined) {
context.err(
`Image with path ${image} not found or not attributed; it is used in ${context}`,
`Image with path ${image} not found or not attributed; it is used in ${context}`
)
} else if (!this.doesPathExist(image)) {
context.err(
`Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.`,
`Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.`
)
} else {
context.err(
`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`,
`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`
)
}
}
@ -133,7 +133,7 @@ class OverrideShadowingCheck extends DesugaringStep<ThemeConfigJson> {
super(
"Checks that an 'overrideAll' does not override a single override",
[],
"OverrideShadowingCheck",
"OverrideShadowingCheck"
)
}
@ -183,7 +183,7 @@ class MiscThemeChecks extends DesugaringStep<ThemeConfigJson> {
context
.enter("layers")
.err(
"The 'layers'-field should be an array, but it is not. Did you pase a layer identifier and forget to add the '[' and ']'?",
"The 'layers'-field should be an array, but it is not. Did you pase a layer identifier and forget to add the '[' and ']'?"
)
}
if (json.socialImage === "") {
@ -217,23 +217,37 @@ class MiscThemeChecks extends DesugaringStep<ThemeConfigJson> {
context
.enter("overideAll")
.err(
"'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them.",
"'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them."
)
}
if (json.defaultBackgroundId
&& ![AvailableRasterLayers.osmCartoProperties.id, ...eliCategory ]
.find(l => l === json.defaultBackgroundId) ) {
if (
json.defaultBackgroundId &&
![AvailableRasterLayers.osmCartoProperties.id, ...eliCategory].find(
(l) => l === json.defaultBackgroundId
)
) {
const background = json.defaultBackgroundId
const match = AvailableRasterLayers.globalLayers.find(l => l.properties.id === background)
const match = AvailableRasterLayers.globalLayers.find(
(l) => l.properties.id === background
)
if (!match) {
const suggestions = Utils.sortedByLevenshteinDistance(background,
AvailableRasterLayers.globalLayers, l => l.properties.id)
context.enter("defaultBackgroundId")
.warn("The default background layer with id", background, "does not exist or is not a global layer. Perhaps you meant one of:",
suggestions.slice(0, 5).map(l => l.properties.id).join(", "),
"If you want to use a certain category of background image, use", AvailableRasterLayers.globalLayers.join(", ")
)
const suggestions = Utils.sortedByLevenshteinDistance(
background,
AvailableRasterLayers.globalLayers,
(l) => l.properties.id
)
context.enter("defaultBackgroundId").warn(
"The default background layer with id",
background,
"does not exist or is not a global layer. Perhaps you meant one of:",
suggestions
.slice(0, 5)
.map((l) => l.properties.id)
.join(", "),
"If you want to use a certain category of background image, use",
AvailableRasterLayers.globalLayers.join(", ")
)
}
}
return json
@ -245,7 +259,7 @@ export class PrevalidateTheme extends Fuse<ThemeConfigJson> {
super(
"Various consistency checks on the raw JSON",
new MiscThemeChecks(),
new OverrideShadowingCheck(),
new OverrideShadowingCheck()
)
}
}
@ -255,7 +269,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
super(
"The `if`-part in a mapping might set some keys. Those keys are not allowed to be set in the `addExtraTags`, as this might result in conflicting values",
[],
"DetectConflictingAddExtraTags",
"DetectConflictingAddExtraTags"
)
}
@ -282,7 +296,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
.enters("mappings", i)
.err(
"AddExtraTags overrides a key that is set in the `if`-clause of this mapping. Selecting this answer might thus first set one value (needed to match as answer) and then override it with a different value, resulting in an unsaveable question. The offending `addExtraTags` is " +
duplicateKeys.join(", "),
duplicateKeys.join(", ")
)
}
}
@ -300,13 +314,13 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
super(
"A tagRendering might set a freeform key (e.g. `name` and have an option that _should_ erase this name, e.g. `noname=yes`). Under normal circumstances, every mapping/freeform should affect all touched keys",
[],
"DetectNonErasedKeysInMappings",
"DetectNonErasedKeysInMappings"
)
}
convert(
json: QuestionableTagRenderingConfigJson,
context: ConversionContext,
context: ConversionContext
): QuestionableTagRenderingConfigJson {
if (json.multiAnswer) {
// No need to check this here, this has its own validation
@ -360,8 +374,8 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
.enters("freeform")
.warn(
"The freeform block does not modify the key `" +
neededKey +
"` which is set in a mapping. Use `addExtraTags` to overwrite it",
neededKey +
"` which is set in a mapping. Use `addExtraTags` to overwrite it"
)
}
}
@ -379,8 +393,8 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
.enters("mappings", i)
.warn(
"This mapping does not modify the key `" +
neededKey +
"` which is set in a mapping or by the freeform block. Use `addExtraTags` to overwrite it",
neededKey +
"` which is set in a mapping or by the freeform block. Use `addExtraTags` to overwrite it"
)
}
}
@ -397,7 +411,7 @@ export class DetectMappingsShadowedByCondition extends DesugaringStep<TagRenderi
super(
"Checks that, if the tagrendering has a condition, that a mapping is not contradictory to it, i.e. that there are no dead mappings",
[],
"DetectMappingsShadowedByCondition",
"DetectMappingsShadowedByCondition"
)
this._forceError = forceError
}
@ -469,7 +483,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
* DetectShadowedMappings.extractCalculatedTagNames({calculatedTags: ["_abc=js()"]}) // => ["_abc"]
*/
private static extractCalculatedTagNames(
layerConfig?: LayerConfigJson | { calculatedTags: string[] },
layerConfig?: LayerConfigJson | { calculatedTags: string[] }
) {
return (
layerConfig?.calculatedTags?.map((ct) => {
@ -555,16 +569,16 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
json.mappings[i]["hideInAnswer"] !== true
) {
context.warn(
`Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`,
`Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`
)
} else if (doesMatch) {
// The current mapping is shadowed!
context.err(`Mapping ${i} is shadowed by mapping ${j} and will thus never be shown:
The mapping ${parsedConditions[i].asHumanString(
false,
false,
{},
)} is fully matched by a previous mapping (namely ${j}), which matches:
false,
false,
{}
)} is fully matched by a previous mapping (namely ${j}), which matches:
${parsedConditions[j].asHumanString(false, false, {})}.
To fix this problem, you can try to:
@ -589,7 +603,7 @@ export class ValidatePossibleLinks extends DesugaringStep<string | Record<string
super(
"Given a possible set of translations, validates that <a href=... target='_blank'> does have `rel='noopener'` set",
[],
"ValidatePossibleLinks",
"ValidatePossibleLinks"
)
}
@ -619,21 +633,21 @@ export class ValidatePossibleLinks extends DesugaringStep<string | Record<string
convert(
json: string | Record<string, string>,
context: ConversionContext,
context: ConversionContext
): string | Record<string, string> {
if (typeof json === "string") {
if (this.isTabnabbingProne(json)) {
context.err(
"The string " +
json +
" has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping",
json +
" has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping"
)
}
} else {
for (const k in json) {
if (this.isTabnabbingProne(json[k])) {
context.err(
`The translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping`,
`The translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping`
)
}
}
@ -651,7 +665,7 @@ export class CheckTranslation extends DesugaringStep<Translatable> {
super(
"Checks that a translation is valid and internally consistent",
["*"],
"CheckTranslation",
"CheckTranslation"
)
this._allowUndefined = allowUndefined
}
@ -698,7 +712,7 @@ export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
isBuiltin: boolean,
doesImageExist: DoesImageExist,
studioValidations: boolean = false,
skipDefaultLayers: boolean = false,
skipDefaultLayers: boolean = false
) {
super("Thin wrapper around 'ValidateLayer", [], "ValidateLayerConfig")
this.validator = new ValidateLayer(
@ -706,7 +720,7 @@ export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
isBuiltin,
doesImageExist,
studioValidations,
skipDefaultLayers,
skipDefaultLayers
)
}
@ -734,7 +748,7 @@ export class ValidatePointRendering extends DesugaringStep<PointRenderingConfigJ
context
.enter("markers")
.err(
`Detected a field 'markerS' in pointRendering. It is written as a singular case`,
`Detected a field 'markerS' in pointRendering. It is written as a singular case`
)
}
if (json.marker && !Array.isArray(json.marker)) {
@ -744,7 +758,7 @@ export class ValidatePointRendering extends DesugaringStep<PointRenderingConfigJ
context
.enter("location")
.err(
"A pointRendering should have at least one 'location' to defined where it should be rendered. ",
"A pointRendering should have at least one 'location' to defined where it should be rendered. "
)
}
return json
@ -763,26 +777,26 @@ export class ValidateLayer extends Conversion<
isBuiltin: boolean,
doesImageExist: DoesImageExist,
studioValidations: boolean = false,
skipDefaultLayers: boolean = false,
skipDefaultLayers: boolean = false
) {
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
this._prevalidation = new PrevalidateLayer(
path,
isBuiltin,
doesImageExist,
studioValidations,
studioValidations
)
this._skipDefaultLayers = skipDefaultLayers
}
convert(
json: LayerConfigJson,
context: ConversionContext,
context: ConversionContext
): { parsed: LayerConfig; raw: LayerConfigJson } {
context = context.inOperation(this.name)
if (typeof json === "string") {
context.err(
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`,
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`
)
return undefined
}
@ -814,7 +828,7 @@ export class ValidateLayer extends Conversion<
context
.enters("calculatedTags", i)
.err(
`Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`,
`Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`
)
}
}
@ -862,7 +876,7 @@ export class ValidateLayer extends Conversion<
context
.enters("allowMove", "enableAccuracy")
.err(
"`enableAccuracy` is written with two C in the first occurrence and only one in the last",
"`enableAccuracy` is written with two C in the first occurrence and only one in the last"
)
}
@ -893,8 +907,8 @@ export class ValidateFilter extends DesugaringStep<FilterConfigJson> {
.enters("fields", i)
.err(
`Invalid filter: ${type} is not a valid textfield type.\n\tTry one of ${Array.from(
Validators.availableTypes,
).join(",")}`,
Validators.availableTypes
).join(",")}`
)
}
}
@ -911,13 +925,13 @@ export class DetectDuplicateFilters extends DesugaringStep<{
super(
"Tries to detect layers where a shared filter can be used (or where similar filters occur)",
[],
"DetectDuplicateFilters",
"DetectDuplicateFilters"
)
}
convert(
json: { layers: LayerConfigJson[]; themes: ThemeConfigJson[] },
context: ConversionContext,
context: ConversionContext
): { layers: LayerConfigJson[]; themes: ThemeConfigJson[] } {
const { layers, themes } = json
const perOsmTag = new Map<
@ -981,7 +995,7 @@ export class DetectDuplicateFilters extends DesugaringStep<{
filter: FilterConfigJson
}[]
>,
theme?: ThemeConfigJson | undefined,
theme?: ThemeConfigJson | undefined
): void {
if (layer.filter === undefined || layer.filter === null) {
return
@ -1021,7 +1035,7 @@ export class DetectDuplicatePresets extends DesugaringStep<ThemeConfig> {
super(
"Detects mappings which have identical (english) names or identical mappings.",
["presets"],
"DetectDuplicatePresets",
"DetectDuplicatePresets"
)
}
@ -1032,13 +1046,13 @@ export class DetectDuplicatePresets extends DesugaringStep<ThemeConfig> {
if (new Set(enNames).size != enNames.length) {
const dups = Utils.Duplicates(enNames)
const layersWithDup = json.layers.filter((l) =>
l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0),
l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0)
)
const layerIds = layersWithDup.map((l) => l.id)
context.err(
`This theme has multiple presets which are named:${dups}, namely layers ${layerIds.join(
", ",
)} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`,
", "
)} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`
)
}
@ -1053,17 +1067,17 @@ export class DetectDuplicatePresets extends DesugaringStep<ThemeConfig> {
Utils.SameObject(presetATags, presetBTags) &&
Utils.sameList(
presetA.preciseInput.snapToLayers,
presetB.preciseInput.snapToLayers,
presetB.preciseInput.snapToLayers
)
) {
context.err(
`This theme has multiple presets with the same tags: ${presetATags.asHumanString(
false,
false,
{},
{}
)}, namely the preset '${presets[i].title.textFor("en")}' and '${presets[
j
].title.textFor("en")}'`,
].title.textFor("en")}'`
)
}
}
@ -1088,13 +1102,13 @@ export class ValidateThemeEnsemble extends Conversion<
super(
"Validates that all themes together are logical, i.e. no duplicate ids exists within (overriden) themes",
[],
"ValidateThemeEnsemble",
"ValidateThemeEnsemble"
)
}
convert(
json: ThemeConfig[],
context: ConversionContext,
context: ConversionContext
): Map<
string,
{
@ -1145,11 +1159,11 @@ export class ValidateThemeEnsemble extends Conversion<
context.err(
[
"The layer with id '" +
id +
"' is found in multiple themes with different tag definitions:",
id +
"' is found in multiple themes with different tag definitions:",
"\t In theme " + oldTheme + ":\t" + oldTags.asHumanString(false, false, {}),
"\tIn theme " + theme.id + ":\t" + tags.asHumanString(false, false, {}),
].join("\n"),
].join("\n")
)
}
}

View file

@ -33,9 +33,20 @@ export default class DependencyCalculator {
*/
public static getLayerDependencies(
layer: LayerConfig
): { neededLayer: string; reason: string; context?: string; neededBy: string, checkHasSnapName: boolean }[] {
const deps: { neededLayer: string; reason: string; context?: string; neededBy: string, checkHasSnapName: boolean }[] =
[]
): {
neededLayer: string
reason: string
context?: string
neededBy: string
checkHasSnapName: boolean
}[] {
const deps: {
neededLayer: string
reason: string
context?: string
neededBy: string
checkHasSnapName: boolean
}[] = []
for (let i = 0; layer.presets !== undefined && i < layer.presets.length; i++) {
const preset = layer.presets[i]
@ -51,7 +62,7 @@ export default class DependencyCalculator {
reason: `preset \`${preset.title.textFor("en")}\` snaps to this layer`,
context: `${layer.id}.presets[${i}]`,
neededBy: layer.id,
checkHasSnapName: true
checkHasSnapName: true,
})
})
}
@ -63,7 +74,7 @@ export default class DependencyCalculator {
reason: "a tagrendering needs this layer",
context: tr.id,
neededBy: layer.id,
checkHasSnapName: false
checkHasSnapName: false,
})
}
}
@ -99,7 +110,7 @@ export default class DependencyCalculator {
"] which calculates the value for " +
currentKey,
neededBy: layer.id,
checkHasSnapName: false
checkHasSnapName: false,
})
return []

View file

@ -59,20 +59,30 @@ export default class FilterConfig {
throw `Invalid filter: no question given at ${ctx}`
}
const fields: { name: string; type: ValidatorType }[] = (option.fields ?? []).map((f, i) => {
const type = <ValidatorType> f.type ?? "regex"
if(Validators.availableTypes.indexOf(type) < 0){
throw `Invalid filter: type is not a valid validator. Did you mean one of ${Utils.sortedByLevenshteinDistance(type, <ReadonlyArray<string>>Validators.availableTypes, x => x).slice(0, 3)}`
const fields: { name: string; type: ValidatorType }[] = (option.fields ?? []).map(
(f, i) => {
const type = <ValidatorType>f.type ?? "regex"
if (Validators.availableTypes.indexOf(type) < 0) {
throw `Invalid filter: type is not a valid validator. Did you mean one of ${Utils.sortedByLevenshteinDistance(
type,
<ReadonlyArray<string>>Validators.availableTypes,
(x) => x
).slice(0, 3)}`
}
// Type is validated against 'ValidatedTextField' in Validation.ts, in ValidateFilterConfig
if (
f.name === undefined ||
f.name === "" ||
f.name.match(/[a-z0-9_-]+/) == null
) {
throw `Invalid filter: a variable name should match [a-z0-9_-]+ at ${ctx}.fields[${i}]`
}
return {
name: f.name,
type,
}
}
// Type is validated against 'ValidatedTextField' in Validation.ts, in ValidateFilterConfig
if (f.name === undefined || f.name === "" || f.name.match(/[a-z0-9_-]+/) == null) {
throw `Invalid filter: a variable name should match [a-z0-9_-]+ at ${ctx}.fields[${i}]`
}
return {
name: f.name,
type
}
})
)
for (const field of fields) {
for (const ln in question.translations) {
@ -226,7 +236,7 @@ export default class FilterConfig {
opt.osmTags?.asHumanString() ?? "",
opt.fields?.length > 0
? opt.fields.map((f) => f.name + " (" + f.type + ")").join(" ")
: undefined
: undefined,
])
)
})

View file

@ -85,12 +85,12 @@ export default class LayerConfig extends WithContextLoader {
}
this.syncSelection = json.syncSelection ?? "no"
if(!json.source) {
if(json.presets === undefined){
throw "Error while parsing "+json.id+" in "+context+"; no source given"
if (!json.source) {
if (json.presets === undefined) {
throw "Error while parsing " + json.id + " in " + context + "; no source given"
}
this.source = new SourceConfig({
osmTags: TagUtils.Tag({or: json.presets.map(pr => ({and:pr.tags}))}),
osmTags: TagUtils.Tag({ or: json.presets.map((pr) => ({ and: pr.tags })) }),
})
} else if (typeof json.source !== "string") {
this.maxAgeOfCache = json.source["maxCacheAge"] ?? 24 * 60 * 60 * 30
@ -104,7 +104,7 @@ export default class LayerConfig extends WithContextLoader {
mercatorCrs: json.source["mercatorCrs"],
idKey: json.source["idKey"],
},
json.id,
json.id
)
}
@ -124,7 +124,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 = []
@ -194,7 +194,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,
@ -208,7 +208,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 = []
@ -216,7 +216,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 = []
@ -228,7 +228,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 (
@ -250,7 +250,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 (
@ -269,7 +269,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)
@ -280,8 +280,8 @@ export default class LayerConfig extends WithContextLoader {
(tr, i) =>
new TagRenderingConfig(
<QuestionableTagRenderingConfigJson>tr,
this.id + ".tagRenderings[" + i + "]",
),
this.id + ".tagRenderings[" + i + "]"
)
)
if (json.units !== undefined && !Array.isArray(json.units)) {
throw (
@ -291,7 +291,7 @@ export default class LayerConfig extends WithContextLoader {
)
}
this.units = (json.units ?? []).flatMap((unitJson, i) =>
Unit.fromJson(unitJson, this.tagRenderings, `${context}.unit[${i}]`),
Unit.fromJson(unitJson, this.tagRenderings, `${context}.unit[${i}]`)
)
if (
@ -362,14 +362,18 @@ export default class LayerConfig extends WithContextLoader {
if (mapRenderings.length === 0) {
return undefined
}
return new Combine(mapRenderings.map(
mr => mr.GetBaseIcon(properties ?? this.GetBaseTags()).SetClass("absolute left-0 top-0 w-full h-full"))
return new Combine(
mapRenderings.map((mr) =>
mr
.GetBaseIcon(properties ?? this.GetBaseTags())
.SetClass("absolute left-0 top-0 w-full h-full")
)
).SetClass("relative block w-full h-full")
}
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" }]
)
}
@ -382,7 +386,7 @@ export default class LayerConfig extends WithContextLoader {
neededLayer: string
}[] = [],
addedByDefault = false,
canBeIncluded = true,
canBeIncluded = true
): string {
const extraProps: string[] = []
extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher")
@ -390,32 +394,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`"
)
}
@ -425,12 +429,12 @@ export default class LayerConfig extends WithContextLoader {
"<img src='../warning.svg' height='1rem'/>",
"This layer is loaded from an external source, namely ",
"`" + this.source.geojsonSource + "`",
].join("\n\n"),
].join("\n\n")
)
}
} 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."
)
}
@ -440,7 +444,7 @@ export default class LayerConfig extends WithContextLoader {
usingLayer = [
"## Themes using this layer",
MarkdownUtils.list(
(usedInThemes ?? []).map((id) => `[${id}](https://mapcomplete.org/${id})`),
(usedInThemes ?? []).map((id) => `[${id}](https://mapcomplete.org/${id})`)
),
]
} else if (this.source !== null) {
@ -456,31 +460,43 @@ export default class LayerConfig extends WithContextLoader {
" into the layout as it depends on it: ",
dep.reason,
"(" + dep.context + ")",
].join(" "),
].join(" ")
)
}
let presets: string[] = []
if (this.presets.length > 0) {
presets = [
"## Presets",
"The following options to create new points are included:",
MarkdownUtils.list(this.presets.map(preset => {
let snaps = ""
if (preset.preciseInput?.snapToLayers) {
snaps = " (snaps to layers " + preset.preciseInput.snapToLayers.map(id => `\`${id}\``).join(", ") + ")"
}
return "**" + preset.title.txt + "** which has the following tags:" + new And(preset.tags).asHumanString(true) + snaps
})),
MarkdownUtils.list(
this.presets.map((preset) => {
let snaps = ""
if (preset.preciseInput?.snapToLayers) {
snaps =
" (snaps to layers " +
preset.preciseInput.snapToLayers
.map((id) => `\`${id}\``)
.join(", ") +
")"
}
return (
"**" +
preset.title.txt +
"** which has the following tags:" +
new And(preset.tags).asHumanString(true) +
snaps
)
})
),
]
}
for (const revDep of Utils.Dedup(layerIsNeededBy?.get(this.id) ?? [])) {
extraProps.push(
["This layer is needed as dependency for layer", `[${revDep}](#${revDep})`].join(
" ",
),
" "
)
)
}
@ -491,10 +507,10 @@ export default class LayerConfig extends WithContextLoader {
.filter((values) => values.key !== "id")
.map((values) => {
const embedded: string[] = values.values?.map((v) =>
Link.OsmWiki(values.key, v, true).SetClass("mr-2").AsMarkdown(),
Link.OsmWiki(values.key, v, true).SetClass("mr-2").AsMarkdown()
) ?? ["_no preset options defined, or no values in them_"]
const statistics = `https://taghistory.raifer.tech/?#***/${encodeURIComponent(
values.key,
values.key
)}/`
const tagInfo = `https://taginfo.openstreetmap.org/keys/${values.key}#values`
return [
@ -509,7 +525,7 @@ export default class LayerConfig extends WithContextLoader {
: `[${values.type}](../SpecialInputElements.md#${values.type})`,
embedded.join(" "),
]
}),
})
)
let quickOverview: string[] = []
@ -519,7 +535,7 @@ export default class LayerConfig extends WithContextLoader {
"this quick overview is incomplete",
MarkdownUtils.table(
["attribute", "type", "values which are supported by this layer"],
tableRows,
tableRows
),
]
}
@ -553,19 +569,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, {}) +
"**"
)
}
@ -616,8 +632,7 @@ export default class LayerConfig extends WithContextLoader {
if (!presets) {
return undefined
}
const matchingPresets = presets
.filter((pr) => new And(pr.tags).matchesProperties(tags))
const matchingPresets = presets.filter((pr) => new And(pr.tags).matchesProperties(tags))
let mostShadowed = matchingPresets[0]
let mostShadowedTags = new And(mostShadowed.tags)
for (let i = 1; i < matchingPresets.length; i++) {
@ -646,18 +661,18 @@ export default class LayerConfig extends WithContextLoader {
* Indicates if this is a normal layer, meaning that it can be toggled by the user in normal circumstances
* Thus: name is set, not a note import layer, not synced with another filter, ...
*/
public isNormal(){
if(this.id.startsWith("note_import")){
public isNormal() {
if (this.id.startsWith("note_import")) {
return false
}
if(Constants.added_by_default.indexOf(<any> this.id) >=0){
if (Constants.added_by_default.indexOf(<any>this.id) >= 0) {
return false
}
if(this.filterIsSameAs !== undefined){
if (this.filterIsSameAs !== undefined) {
return false
}
if(!this.name ){
if (!this.name) {
return false
}
return true

View file

@ -85,7 +85,7 @@ export default class TagRenderingConfig {
| string
| TagRenderingConfigJson
| (QuestionableTagRenderingConfigJson & { questionHintIsMd?: boolean }),
context?: string,
context?: string
) {
let json = <string | QuestionableTagRenderingConfigJson>config
if (json === undefined) {
@ -144,7 +144,7 @@ export default class TagRenderingConfig {
this.description = Translations.T(json.description, translationKey + ".description")
this.editButtonAriaLabel = Translations.T(
json.editButtonAriaLabel,
translationKey + ".editButtonAriaLabel",
translationKey + ".editButtonAriaLabel"
)
this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`)
@ -160,7 +160,7 @@ export default class TagRenderingConfig {
}
this.metacondition = TagUtils.Tag(
json.metacondition ?? { and: [] },
`${context}.metacondition`,
`${context}.metacondition`
)
if (json.freeform) {
if (
@ -178,7 +178,7 @@ export default class TagRenderingConfig {
}, perhaps you meant ${Utils.sortedByLevenshteinDistance(
json.freeform.key,
<any>Validators.availableTypes,
(s) => <any>s,
(s) => <any>s
)}`
}
const type: ValidatorType = <any>json.freeform.type ?? "string"
@ -200,7 +200,7 @@ export default class TagRenderingConfig {
placeholder,
addExtraTags:
json.freeform.addExtraTags?.map((tg, i) =>
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`),
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`)
) ?? [],
inline: json.freeform.inline ?? false,
default: json.freeform.default,
@ -266,8 +266,8 @@ export default class TagRenderingConfig {
context,
this.multiAnswer,
this.question !== undefined,
commonIconSize,
),
commonIconSize
)
)
} else {
this.mappings = []
@ -293,7 +293,7 @@ export default class TagRenderingConfig {
for (const expectedKey of keys) {
if (usedKeys.indexOf(expectedKey) < 0) {
const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join(
", ",
", "
)}, but it should also give a value for ${expectedKey}`
this.configuration_warnings.push(msg)
}
@ -340,7 +340,7 @@ export default class TagRenderingConfig {
context: string,
multiAnswer?: boolean,
isQuestionable?: boolean,
commonSize: string = "small",
commonSize: string = "small"
): Mapping {
const ctx = `${translationKey}.mappings.${i}`
if (mapping.if === undefined) {
@ -349,7 +349,7 @@ export default class TagRenderingConfig {
if (mapping.then === undefined) {
if (mapping["render"] !== undefined) {
throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(
mapping,
mapping
)}`
}
throw `${ctx}: Invalid mapping: no 'then'-clause found in ${JSON.stringify(mapping)}`
@ -360,7 +360,7 @@ export default class TagRenderingConfig {
if (mapping["render"] !== undefined) {
throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(
mapping,
mapping
)}`
}
if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) {
@ -383,11 +383,11 @@ export default class TagRenderingConfig {
} else if (mapping.hideInAnswer !== undefined) {
hideInAnswer = TagUtils.Tag(
mapping.hideInAnswer,
`${context}.mapping[${i}].hideInAnswer`,
`${context}.mapping[${i}].hideInAnswer`
)
}
const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) =>
TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`),
TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`)
)
if (hideInAnswer === true && addExtraTags.length > 0) {
throw `${ctx}: Invalid mapping: 'hideInAnswer' is set to 'true', but 'addExtraTags' is enabled as well. This means that extra tags will be applied if this mapping is chosen as answer, but it cannot be chosen as answer. This either indicates a thought error or obsolete code that must be removed.`
@ -483,7 +483,7 @@ export default class TagRenderingConfig {
* @constructor
*/
public GetRenderValues(
tags: Record<string, string>,
tags: Record<string, string>
): { then: Translation; icon?: string; iconClass?: string }[] {
if (!this.multiAnswer) {
return [this.GetRenderValueWithImage(tags)]
@ -506,7 +506,7 @@ export default class TagRenderingConfig {
return mapping
}
return undefined
}),
})
)
if (freeformKeyDefined && tags[this.freeform.key] !== undefined) {
@ -514,7 +514,7 @@ export default class TagRenderingConfig {
applicableMappings
?.flatMap((m) => m.if?.usedTags() ?? [])
?.filter((kv) => kv.key === this.freeform.key)
?.map((kv) => kv.value),
?.map((kv) => kv.value)
)
const freeformValues = tags[this.freeform.key].split(";")
@ -523,7 +523,7 @@ export default class TagRenderingConfig {
applicableMappings.push({
then: new TypedTranslation<object>(
this.render.replace("{" + this.freeform.key + "}", leftover).translations,
this.render.context,
this.render.context
),
})
}
@ -541,7 +541,7 @@ export default class TagRenderingConfig {
* @constructor
*/
public GetRenderValueWithImage(
tags: Record<string, string>,
tags: Record<string, string>
): { then: TypedTranslation<any>; icon?: string; iconClass?: string } | undefined {
if (this.condition !== undefined) {
if (!this.condition.matchesProperties(tags)) {
@ -610,7 +610,7 @@ export default class TagRenderingConfig {
const answerMappings = this.mappings?.filter((m) => m.hideInAnswer !== true)
if (key === undefined) {
const values: { k: string; v: string }[][] = Utils.NoNull(
answerMappings?.map((m) => m.if.asChange({})) ?? [],
answerMappings?.map((m) => m.if.asChange({})) ?? []
)
if (values.length === 0) {
return
@ -628,15 +628,15 @@ export default class TagRenderingConfig {
return {
key: commonKey,
values: Utils.NoNull(
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v),
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v)
),
}
}
let values = Utils.NoNull(
answerMappings?.map(
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v,
) ?? [],
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v
) ?? []
)
if (values.length === undefined) {
values = undefined
@ -700,7 +700,7 @@ export default class TagRenderingConfig {
freeformValue: string | undefined,
singleSelectedMapping: number,
multiSelectedMapping: boolean[] | undefined,
currentProperties: Record<string, string>,
currentProperties: Record<string, string>
): UploadableTag {
if (typeof freeformValue === "string") {
freeformValue = freeformValue?.trim()
@ -775,7 +775,7 @@ export default class TagRenderingConfig {
new And([
new Tag(this.freeform.key, freeformValue),
...(this.freeform.addExtraTags ?? []),
]),
])
)
}
const and = TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings])
@ -845,11 +845,11 @@ export default class TagRenderingConfig {
}
const msgs: string[] = [
icon +
" " +
"*" +
m.then.textFor(lang) +
"* is shown if with " +
m.if.asHumanString(true, false, {}),
" " +
"*" +
m.then.textFor(lang) +
"* is shown if with " +
m.if.asHumanString(true, false, {}),
]
if (m.hideInAnswer === true) {
@ -858,11 +858,11 @@ export default class TagRenderingConfig {
if (m.ifnot !== undefined) {
msgs.push(
"Unselecting this answer will add " +
m.ifnot.asHumanString(true, false, {}),
m.ifnot.asHumanString(true, false, {})
)
}
return msgs.join(". ")
}),
})
)
}
@ -871,7 +871,7 @@ export default class TagRenderingConfig {
const conditionAsLink = (<TagsFilter>this.condition.optimize()).asHumanString(
true,
false,
{},
{}
)
condition =
"This tagrendering is only visible in the popup if the following condition is met: " +
@ -905,7 +905,7 @@ export default class TagRenderingConfig {
this.metacondition,
this.condition,
this.freeform?.key ? new RegexTag(this.freeform?.key, /.*/) : undefined,
this.invalidValues,
this.invalidValues
)
for (const m of this.mappings ?? []) {
tags.push(m.if)
@ -925,21 +925,26 @@ export default class TagRenderingConfig {
* The keys that should be erased if one has to revert to 'unknown'.
* Might give undefined if setting to unknown is not possible
*/
public removeToSetUnknown(partOfLayer: LayerConfig, currentTags: Record<string, string>): string[] | undefined {
public removeToSetUnknown(
partOfLayer: LayerConfig,
currentTags: Record<string, string>
): string[] | undefined {
if (!partOfLayer?.source || !currentTags) {
return
}
const toDelete = new Set<string>()
if (this.freeform) {
toDelete.add(this.freeform.key)
const extraTags = new And(this.freeform.addExtraTags ?? []).usedKeys().filter(k => k !== "fixme")
const extraTags = new And(this.freeform.addExtraTags ?? [])
.usedKeys()
.filter((k) => k !== "fixme")
if (extraTags.length > 0) {
return undefined
}
}
if (this.mappings?.length > 0) {
const mainkey = this.mappings[0].if.usedKeys()
mainkey.forEach(k => toDelete.add(k))
mainkey.forEach((k) => toDelete.add(k))
for (const mapping of this.mappings) {
if (mapping.addExtraTags?.length > 0) {
return undefined
@ -953,7 +958,6 @@ export default class TagRenderingConfig {
}
}
currentTags = { ...currentTags }
for (const key of toDelete) {
delete currentTags[key]
@ -971,7 +975,7 @@ export class TagRenderingConfigUtils {
public static withNameSuggestionIndex(
config: TagRenderingConfig,
tags: UIEventSource<Record<string, string>>,
feature?: Feature,
feature?: Feature
): Store<TagRenderingConfig> {
const isNSI = NameSuggestionIndex.supportedTypes().indexOf(config.freeform?.key) >= 0
if (!isNSI) {
@ -989,8 +993,8 @@ export class TagRenderingConfigUtils {
tags,
country.split(";"),
center,
{ sortByFrequency: true },
),
{ sortByFrequency: true }
)
)
})
return extraMappings.map((extraMappings) => {
@ -1000,19 +1004,17 @@ export class TagRenderingConfigUtils {
const clone: TagRenderingConfig = Object.create(config)
// The original mappings get "priorityIf" set
const oldMappingsCloned =
clone.mappings?.map(
(m) => {
const mapping = {
...m,
priorityIf: m.priorityIf ?? TagUtils.Tag("id~*"),
}
if (m.if.usedKeys().indexOf("nobrand") < 0) {
// Erase 'nobrand=yes', unless this option explicitly sets it
mapping["addExtraTags"] = [new Tag("nobrand", "")]
}
return <Mapping>mapping
},
) ?? []
clone.mappings?.map((m) => {
const mapping = {
...m,
priorityIf: m.priorityIf ?? TagUtils.Tag("id~*"),
}
if (m.if.usedKeys().indexOf("nobrand") < 0) {
// Erase 'nobrand=yes', unless this option explicitly sets it
mapping["addExtraTags"] = [new Tag("nobrand", "")]
}
return <Mapping>mapping
}) ?? []
clone.mappings = [...oldMappingsCloned, ...extraMappings]
return clone
})

View file

@ -38,7 +38,6 @@ export class ThemeInformation {
keywords?: (Translatable | Translation)[]
}
export default class ThemeConfig implements ThemeInformation {
public static readonly defaultSocialImage = "assets/SocialImage.png"
public readonly id: string
@ -193,7 +192,7 @@ export default class ThemeConfig implements ThemeInformation {
icon: "./assets/svg/pop-out.svg",
href: "https://{basepath}/{theme}.html?lat={lat}&lon={lon}&z={zoom}&language={language}",
newTab: true,
requirements: ["iframe", "no-welcome-message"]
requirements: ["iframe", "no-welcome-message"],
},
context + ".extraLink"
)
@ -292,9 +291,7 @@ export default class ThemeConfig implements ThemeInformation {
if (!untranslated.has(ln)) {
untranslated.set(ln, [])
}
untranslated
.get(ln)
.push(translation.context)
untranslated.get(ln).push(translation.context)
}
})
},
@ -330,7 +327,12 @@ export default class ThemeConfig implements ThemeInformation {
}
}
}
console.trace("Fallthrough: could not find the appropriate layer for an object with tags", tags, "within layout", this)
console.trace(
"Fallthrough: could not find the appropriate layer for an object with tags",
tags,
"within layout",
this
)
return undefined
}
@ -342,7 +344,7 @@ export default class ThemeConfig implements ThemeInformation {
// The 'favourite'-layer contains pretty much all images as it bundles all layers, so we exclude it
const jsonNoFavourites = {
...json,
layers: json.layers.filter((l) => l["id"] !== "favourite")
layers: json.layers.filter((l) => l["id"] !== "favourite"),
}
const usedImages = jsonNoFavourites._usedImages
usedImages.sort()

View file

@ -2,7 +2,11 @@ import ThemeConfig from "./ThemeConfig/ThemeConfig"
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
import { Changes } from "../Logic/Osm/Changes"
import { Store, UIEventSource } from "../Logic/UIEventSource"
import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
import {
FeatureSource,
IndexedFeatureSource,
WritableFeatureSource,
} from "../Logic/FeatureSource/FeatureSource"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { ExportableMap, MapProperties } from "./MapProperties"
import LayerState from "../Logic/State/LayerState"
@ -46,7 +50,9 @@ import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"
import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector"
import NoElementsInViewDetector, {
FeatureViewState,
} from "../Logic/Actors/NoElementsInViewDetector"
import FilteredLayer from "./FilteredLayer"
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
@ -166,7 +172,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.featureSwitches = new FeatureSwitchState(layout)
this.guistate = new MenuState(
this.featureSwitches.featureSwitchWelcomeMessage.data,
layout.id,
layout.id
)
this.map = new UIEventSource<MlMap>(undefined)
const geolocationState = new GeoLocationState()
@ -182,14 +188,14 @@ export default class ThemeViewState implements SpecialVisualizationState {
oauth_token: QueryParameters.GetQueryParameter(
"oauth_token",
undefined,
"Used to complete the login",
"Used to complete the login"
),
})
this.userRelatedState = new UserRelatedState(
this.osmConnection,
layout,
this.featureSwitches,
this.mapProperties,
this.mapProperties
)
this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => {
this.mapProperties.allowRotating.setData(fixated !== "yes")
@ -200,20 +206,20 @@ export default class ThemeViewState implements SpecialVisualizationState {
geolocationState,
this.selectedElement,
this.mapProperties,
this.userRelatedState.gpsLocationHistoryRetentionTime,
this.userRelatedState.gpsLocationHistoryRetentionTime
)
this.geolocationControl = new GeolocationControlState(this.geolocation, this.mapProperties)
this.availableLayers = AvailableRasterLayers.layersAvailableAt(
this.mapProperties.location,
this.osmConnection.isLoggedIn,
this.osmConnection.isLoggedIn
)
this.layerState = new LayerState(
this.osmConnection,
layout.layers,
layout.id,
this.featureSwitches.featureSwitchLayerDefault,
this.featureSwitches.featureSwitchLayerDefault
)
{
@ -222,7 +228,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
const isDisplayed = QueryParameters.GetBooleanQueryParameter(
"overlay-" + rasterInfo.id,
rasterInfo.defaultState ?? true,
"Whether or not overlay layer " + rasterInfo.id + " is shown",
"Whether or not overlay layer " + rasterInfo.id + " is shown"
)
const state = { isDisplayed }
overlayLayerStates.set(rasterInfo.id, state)
@ -247,7 +253,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.osmConnection.Backend(),
(id) => this.layerState.filteredLayers.get(id).isDisplayed,
mvtAvailableLayers,
this.fullNodeDatabase,
this.fullNodeDatabase
)
let currentViewIndex = 0
@ -265,7 +271,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
id: "current_view_" + currentViewIndex,
}),
]
}),
})
)
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
@ -276,19 +282,19 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.changes = new Changes(
this,
layout?.isLeftRightSensitive() ?? false,
(e, extraMsg) => this.reportError(e, extraMsg),
(e, extraMsg) => this.reportError(e, extraMsg)
)
this.historicalUserLocations = this.geolocation.historicalUserLocations
this.newFeatures = new NewGeometryFromChangesFeatureSource(
this.changes,
layoutSource,
this.featureProperties,
this.featureProperties
)
layoutSource.addSource(this.newFeatures)
const perLayer = new PerLayerFeatureSourceSplitter(
Array.from(this.layerState.filteredLayers.values()).filter(
(l) => l.layerDef?.source !== null,
(l) => l.layerDef?.source !== null
),
new ChangeGeometryApplicator(this.indexedFeatures, this.changes),
{
@ -299,10 +305,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
"Got ",
features.length,
"leftover features, such as",
features[0].properties,
features[0].properties
)
},
},
}
)
this.perLayer = perLayer.perLayer
}
@ -342,12 +348,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.lastClickObject = new LastClickFeatureSource(
this.theme,
this.mapProperties.lastClickLocation,
this.userRelatedState.addNewFeatureMode,
this.userRelatedState.addNewFeatureMode
)
this.osmObjectDownloader = new OsmObjectDownloader(
this.osmConnection.Backend(),
this.changes,
this.changes
)
this.perLayerFiltered = this.showNormalDataOn(this.map)
@ -357,8 +363,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
{
currentZoom: this.mapProperties.zoom,
layerState: this.layerState,
bounds: this.visualFeedbackViewportBounds.map(bounds => bounds ?? this.mapProperties.bounds?.data, [this.mapProperties.bounds]),
},
bounds: this.visualFeedbackViewportBounds.map(
(bounds) => bounds ?? this.mapProperties.bounds?.data,
[this.mapProperties.bounds]
),
}
)
this.featureSummary = this.setupSummaryLayer()
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
@ -370,7 +379,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.changes,
this.geolocation.geolocationState.currentGPSLocation,
this.indexedFeatures,
this.reportError,
this.reportError
)
this.favourites = new FavouritesFeatureSource(this)
const longAgo = new Date()
@ -417,7 +426,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
ThemeSource.fromCacheZoomLevel,
fs,
this.featureProperties,
fs.layer.layerDef.maxAgeOfCache,
fs.layer.layerDef.maxAgeOfCache
)
toLocalStorage.set(layerId, storage)
})
@ -430,7 +439,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
const doShowLayer = this.mapProperties.zoom.map(
(z) =>
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
[fs.layer.isDisplayed],
[fs.layer.isDisplayed]
)
if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) {
@ -447,7 +456,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
fs.layer,
fs,
(id) => this.featureProperties.getStore(id),
this.layerState.globalFilters,
this.layerState.globalFilters
)
filteringFeatureSource.set(layerName, filtered)
@ -579,7 +588,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.guistate.pageStates.favourites.set(true)
})
Hotkeys.RegisterHotkey(
{
nomod: " ",
@ -593,11 +601,14 @@ export default class ThemeViewState implements SpecialVisualizationState {
if (this.guistate.isSomethingOpen() || this.previewedImage.data !== undefined) {
return
}
if (document.activeElement.tagName === "button" || document.activeElement.tagName === "input") {
if (
document.activeElement.tagName === "button" ||
document.activeElement.tagName === "input"
) {
return
}
this.selectClosestAtCenter(0)
},
}
)
for (let i = 1; i < 9; i++) {
@ -615,15 +626,18 @@ export default class ThemeViewState implements SpecialVisualizationState {
onUp: true,
},
doc,
() => this.selectClosestAtCenter(i - 1),
() => this.selectClosestAtCenter(i - 1)
)
}
Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => {
this.searchState.feedback.set(undefined)
this.searchState.searchIsFocused.set(true)
})
Hotkeys.RegisterHotkey(
{ ctrl: "F" },
Translations.t.hotkeyDocumentation.selectSearch,
() => {
this.searchState.feedback.set(undefined)
this.searchState.searchIsFocused.set(true)
}
)
this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => {
if (!enable) {
@ -638,7 +652,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
if (this.featureSwitches.featureSwitchBackgroundSelection.data) {
this.guistate.pageStates.background.setData(true)
}
},
}
)
Hotkeys.RegisterHotkey(
{
@ -649,7 +663,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
if (this.featureSwitches.featureSwitchFilter.data) {
this.guistate.openFilterView()
}
},
}
)
const setLayerCategory = (category: EliCategory, skipLayers: number = 0) => {
const timeOfCall = new Date()
@ -664,7 +678,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
available,
category,
current.data,
skipLayers,
skipLayers
)
if (!best) {
return
@ -677,43 +691,43 @@ export default class ThemeViewState implements SpecialVisualizationState {
Hotkeys.RegisterHotkey(
{ nomod: "O" },
Translations.t.hotkeyDocumentation.selectOsmbasedmap,
() => setLayerCategory("osmbasedmap"),
() => setLayerCategory("osmbasedmap")
)
Hotkeys.RegisterHotkey(
{ nomod: "M" },
Translations.t.hotkeyDocumentation.selectMap,
() => setLayerCategory("map"),
() => setLayerCategory("map")
)
Hotkeys.RegisterHotkey(
{ nomod: "P" },
Translations.t.hotkeyDocumentation.selectAerial,
() => setLayerCategory("photo"),
() => setLayerCategory("photo")
)
Hotkeys.RegisterHotkey(
{ shift: "O" },
Translations.t.hotkeyDocumentation.selectOsmbasedmap,
() => setLayerCategory("osmbasedmap", 2),
() => setLayerCategory("osmbasedmap", 2)
)
Hotkeys.RegisterHotkey(
{ shift: "M" },
Translations.t.hotkeyDocumentation.selectMap,
() => setLayerCategory("map", 2),
() => setLayerCategory("map", 2)
)
Hotkeys.RegisterHotkey(
{ shift: "P" },
Translations.t.hotkeyDocumentation.selectAerial,
() => setLayerCategory("photo", 2),
() => setLayerCategory("photo", 2)
)
Hotkeys.RegisterHotkey(
{ nomod: "L" },
Translations.t.hotkeyDocumentation.geolocate,
() => {
this.geolocationControl.handleClick()
},
}
)
return true
})
@ -730,7 +744,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
} else {
tm.setData("false")
}
},
}
)
}
@ -738,7 +752,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
/**
* MaxZoom for the summary layer
*/
const normalLayers = this.theme.layers.filter(l => l.isNormal())
const normalLayers = this.theme.layers.filter((l) => l.isNormal())
const maxzoom = Math.min(...normalLayers.map((l) => l.minzoom))
@ -746,7 +760,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
(l) =>
Constants.priviliged_layers.indexOf(<any>l.id) < 0 &&
l.source.geojsonSource === undefined &&
l.doCount,
l.doCount
)
if (!Constants.SummaryServer || layers.length === 0) {
return undefined
@ -758,7 +772,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.mapProperties,
{
isActive: this.mapProperties.zoom.map((z) => z < maxzoom),
},
}
)
return new SummaryTileSourceRewriter(summaryTileSource, this.layerState.filteredLayers)
@ -780,12 +794,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
gps_track: this.geolocation.historicalUserLocationsTrack,
geocoded_image: new StaticFeatureSource(this.geocodedImages),
selected_element: new StaticFeatureSource(
this.selectedElement.map((f) => (f === undefined ? empty : [f])),
this.selectedElement.map((f) => (f === undefined ? empty : [f]))
),
range: new StaticFeatureSource(
this.mapProperties.maxbounds.map((bbox) =>
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })],
),
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })]
)
),
current_view: this.currentView,
favourite: this.favourites,
@ -794,7 +808,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
search: this.searchState.locationResults,
}
this.closestFeatures.registerSource(specialLayers.favourite, "favourite")
if (this.theme?.lockLocation) {
const bbox = new BBox(<any>this.theme.lockLocation)
@ -802,7 +815,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
ShowDataLayer.showRange(
this.map,
new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]),
this.featureSwitches.featureSwitchIsTesting,
this.featureSwitches.featureSwitchIsTesting
)
}
const currentViewLayer = this.theme.layers.find((l) => l.id === "current_view")
@ -816,7 +829,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
currentViewLayer,
this.theme,
this.osmObjectDownloader,
this.featureProperties,
this.featureProperties
)
})
}
@ -848,7 +861,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
layer: flayer.layerDef,
metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement,
}
if (flayer.layerDef.id === "search") {
options.onClick = (feature) => {
@ -863,20 +875,20 @@ export default class ThemeViewState implements SpecialVisualizationState {
{
const lastClickLayerConfig = new LayerConfig(
<LayerConfigJson>last_click_layerconfig,
"last_click",
"last_click"
)
const lastClickFiltered =
lastClickLayerConfig.isShown === undefined
? specialLayers.last_click
: specialLayers.last_click.features.mapD((fs) =>
fs.filter((f) => {
const matches = lastClickLayerConfig.isShown.matchesProperties(
f.properties,
)
console.debug("LastClick ", f, "matches", matches)
return matches
}),
)
fs.filter((f) => {
const matches = lastClickLayerConfig.isShown.matchesProperties(
f.properties
)
console.debug("LastClick ", f, "matches", matches)
return matches
})
)
// show last click = new point/note marker
new ShowDataLayer(this.map, {
features: new StaticFeatureSource(lastClickFiltered),
@ -908,7 +920,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
* Setup various services for which no reference are needed
*/
private initActors() {
if (!this.theme.official) {
// Add custom themes to the "visited custom themes"
const th = this.theme
@ -916,13 +927,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
id: th.id,
icon: th.icon,
title: th.title.translations,
shortDescription: th.shortDescription.translations ,
layers: th.layers.filter(l => l.isNormal()).map(l => l.id)
shortDescription: th.shortDescription.translations,
layers: th.layers.filter((l) => l.isNormal()).map((l) => l.id),
})
}
this.selectedElement.addCallback((selected) => {
if (selected === undefined) {
this.focusOnMap()
@ -942,28 +951,31 @@ export default class ThemeViewState implements SpecialVisualizationState {
})
// Add the selected element to the recently visited history
this.selectedElement.addCallbackD(selected => {
this.selectedElement.addCallbackD((selected) => {
const [osm_type, osm_id] = selected.properties.id.split("/")
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
const layer = this.theme.getMatchingLayer(selected.properties)
const nameOptions = [
selected?.properties?.name,
selected?.properties?.alt_name, selected?.properties?.local_name,
selected?.properties?.alt_name,
selected?.properties?.local_name,
layer?.title.GetRenderValue(selected?.properties ?? {}).txt,
selected.properties.display_name,
selected.properties.id,
]
const r = <GeocodeResult>{
feature: selected,
display_name: nameOptions.find(opt => opt !== undefined),
osm_id, osm_type,
lon, lat,
display_name: nameOptions.find((opt) => opt !== undefined),
osm_id,
osm_type,
lon,
lat,
}
this.userRelatedState.recentlyVisitedSearch.add(r)
})
this.userRelatedState.showScale.addCallbackAndRun(showScale => {
this.userRelatedState.showScale.addCallbackAndRun((showScale) => {
this.mapProperties.showScale.set(showScale)
})
new ThemeViewStateHashActor(this)
@ -977,7 +989,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.mapProperties.rasterLayer,
this.availableLayers,
this.featureSwitches.backgroundLayerId,
this.userRelatedState.preferredBackgroundLayer,
this.userRelatedState.preferredBackgroundLayer
)
}
@ -990,7 +1002,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
* Searches the appropriate layer - will first try if a special layer matches; if not, a normal layer will be used by delegating to the theme
*/
public getMatchingLayer(properties: Record<string, string>) {
const id = properties.id
if (id.startsWith("summary_")) {
@ -1024,7 +1035,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
? ">>> _Not_ reporting error to report server as testmode is on"
: ">>> Reporting error to",
Constants.ErrorReportServer,
message,
message
)
if (isTesting) {
return

View file

@ -33,7 +33,7 @@
"oauth_token",
undefined,
"Used to complete the login"
)
),
})
const state = new UserRelatedState(osmConnection)
const t = Translations.t.index
@ -46,41 +46,56 @@
let searchIsFocused = new UIEventSource(true)
const officialThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(th => th.hideFromOverview === false)
const hiddenThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(th => th.hideFromOverview === true)
let visitedHiddenThemes: Store<MinimalThemeInformation[]> = UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection)
.map((knownIds) => hiddenThemes.filter((theme) =>
knownIds.indexOf(theme.id) >= 0 || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet"
))
const officialThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(
(th) => th.hideFromOverview === false
)
const hiddenThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(
(th) => th.hideFromOverview === true
)
let visitedHiddenThemes: Store<MinimalThemeInformation[]> =
UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection).map((knownIds) =>
hiddenThemes.filter(
(theme) =>
knownIds.indexOf(theme.id) >= 0 ||
state.osmConnection.userDetails.data.name === "Pieter Vander Vennet"
)
)
const customThemes: Store<MinimalThemeInformation[]> = Stores.ListStabilized<string>(state.installedUserThemes)
.mapD(stableIds => Utils.NoNullInplace(stableIds.map(id => state.getUnofficialTheme(id))))
const customThemes: Store<MinimalThemeInformation[]> = Stores.ListStabilized<string>(
state.installedUserThemes
).mapD((stableIds) => Utils.NoNullInplace(stableIds.map((id) => state.getUnofficialTheme(id))))
function filtered(themes: Store<MinimalThemeInformation[]>): Store<MinimalThemeInformation[]> {
return searchStable.map(search => {
if (!search) {
return themes.data
}
return searchStable.map(
(search) => {
if (!search) {
return themes.data
}
const start = new Date().getTime()
const scores = ThemeSearch.sortedByLowestScores(search, themes.data)
const end = new Date().getTime()
console.trace("Scores for", search , "are", scores, "searching took", end - start,"ms")
const strict = scores.filter(sc => sc.lowest < 2)
if (strict.length > 0) {
return strict.map(sc => sc.theme)
}
return scores.filter(sc => sc.lowest < 4).slice(0, 6).map(sc => sc.theme)
}, [themes])
const start = new Date().getTime()
const scores = ThemeSearch.sortedByLowestScores(search, themes.data)
const end = new Date().getTime()
console.trace("Scores for", search, "are", scores, "searching took", end - start, "ms")
const strict = scores.filter((sc) => sc.lowest < 2)
if (strict.length > 0) {
return strict.map((sc) => sc.theme)
}
return scores
.filter((sc) => sc.lowest < 4)
.slice(0, 6)
.map((sc) => sc.theme)
},
[themes]
)
}
let officialSearched : Store<MinimalThemeInformation[]>= filtered(new ImmutableStore(officialThemes))
let hiddenSearched: Store<MinimalThemeInformation[]> = filtered(visitedHiddenThemes)
let officialSearched: Store<MinimalThemeInformation[]> = filtered(
new ImmutableStore(officialThemes)
)
let hiddenSearched: Store<MinimalThemeInformation[]> = filtered(visitedHiddenThemes)
let customSearched: Store<MinimalThemeInformation[]> = filtered(customThemes)
let searchIsFocussed = new UIEventSource(false)
document.addEventListener("keydown", function(event) {
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.code === "KeyF") {
searchIsFocussed.set(true)
event.preventDefault()
@ -101,10 +116,7 @@
}
window.location.href = ThemeSearch.createUrlFor(candidate, undefined)
}
</script>
<main>
@ -136,7 +148,13 @@
</div>
</div>
<Searchbar value={search} placeholder={tr.searchForATheme} on:search={() => applySearch()} autofocus isFocused={searchIsFocussed} />
<Searchbar
value={search}
placeholder={tr.searchForATheme}
on:search={() => applySearch()}
autofocus
isFocused={searchIsFocussed}
/>
<ThemesList {search} {state} themes={$officialSearched} />
@ -166,8 +184,11 @@
</ThemesList>
{#if $customThemes.length > 0}
<ThemesList {search} {state} themes={$customSearched}
hasSelection={$officialSearched.length === 0 && $hiddenSearched.length === 0}
<ThemesList
{search}
{state}
themes={$customSearched}
hasSelection={$officialSearched.length === 0 && $hiddenSearched.length === 0}
>
<svelte:fragment slot="title">
<h3>
@ -177,7 +198,6 @@
</svelte:fragment>
</ThemesList>
{/if}
</LoginToggle>
<a

View file

@ -9,83 +9,82 @@
export let open = new UIEventSource(false)
export let dotsSize = `w-6 h-6`
export let dotsPosition = `top-0 right-0`
export let hideBackground= false
export let hideBackground = false
let menuPosition = ``
if(dotsPosition.indexOf("left-0") >= 0){
if (dotsPosition.indexOf("left-0") >= 0) {
menuPosition = "left-0"
}else{
} else {
menuPosition = `right-0`
}
if(dotsPosition.indexOf("top-0") > 0){
if (dotsPosition.indexOf("top-0") > 0) {
menuPosition += " bottom-0"
}else{
} else {
menuPosition += ` top-0`
}
function toggle() {
open.set(!open.data)
}
</script>
<div class="relative" style="z-index: 50">
<div
class="sidebar-unit absolute {menuPosition} collapsable normal-background button-unstyled "
class="sidebar-unit absolute {menuPosition} collapsable normal-background button-unstyled"
class:transition-background={hideBackground}
class:collapsed={!$open}>
class:collapsed={!$open}
>
<slot />
</div>
<DotsCircleHorizontal
class={ `absolute ${dotsPosition} ${dotsSize} dots-menu transition-colors ${$open?"dots-menu-opened":""}`}
on:click={toggle} />
class={`absolute ${dotsPosition} ${dotsSize} dots-menu transition-colors ${
$open ? "dots-menu-opened" : ""
}`}
on:click={toggle}
/>
</div>
<style>
.dots-menu {
z-index: 50;
}
.dots-menu {
z-index: 50;
}
:global(.dots-menu > path) {
fill: var(--interactive-background);
transition: fill 350ms linear;
cursor: pointer;
:global(.dots-menu > path) {
fill: var(--interactive-background);
transition: fill 350ms linear;
cursor: pointer;
}
}
:global(.dots-menu:hover > path, .dots-menu-opened > path) {
fill: var(--interactive-foreground);
}
:global(.dots-menu:hover > path, .dots-menu-opened > path) {
fill: var(--interactive-foreground)
}
.collapsable {
max-width: 50rem;
max-height: 10rem;
transition: max-width 500ms linear, max-height 500ms linear, border 500ms linear;
overflow: hidden;
flex-wrap: nowrap;
text-wrap: none;
width: max-content;
box-shadow: #ccc;
white-space: nowrap;
border: 1px solid var(--button-background);
background-color: white;
}
.collapsable {
max-width: 50rem;
max-height: 10rem;
transition: max-width 500ms linear, max-height 500ms linear, border 500ms linear;
overflow: hidden;
flex-wrap: nowrap;
text-wrap: none;
width: max-content;
box-shadow: #ccc;
white-space: nowrap;
border: 1px solid var(--button-background);
background-color: white;
}
.transition-background {
transition: background-color 150ms linear;
}
.transition-background {
transition: background-color 150ms linear;
}
.transition-background.collapsed {
background-color: #00000000;
}
.collapsed {
max-width: 0;
max-height: 0;
border: 2px solid #00000000;
pointer-events: none;
}
.transition-background.collapsed {
background-color: #00000000;
}
.collapsed {
max-width: 0;
max-height: 0;
border: 2px solid #00000000;
pointer-events: none;
}
</style>

View file

@ -8,11 +8,11 @@
let transitionParams = {
x: 640,
duration: 200,
easing: sineIn
easing: sineIn,
}
let hidden = !shown.data
shown.addCallback(sh => {
shown.addCallback((sh) => {
hidden = !sh
})
@ -23,19 +23,21 @@
})
</script>
<Drawer placement="right"
transitionType="fly" {transitionParams}
activateClickOutside={false}
divClass="overflow-y-auto z-3"
backdrop={false}
id="drawer-right"
width="w-full sm:w-80 md:w-96"
rightOffset="inset-y-0 right-0"
bind:hidden={hidden}>
<Drawer
placement="right"
transitionType="fly"
{transitionParams}
activateClickOutside={false}
divClass="overflow-y-auto z-3"
backdrop={false}
id="drawer-right"
width="w-full sm:w-80 md:w-96"
rightOffset="inset-y-0 right-0"
bind:hidden
>
<div class="low-interaction h-screen">
<div class="h-full" style={`padding-top: ${height}px`}>
<div class="flex flex-col h-full overflow-y-auto">
<div class="flex h-full flex-col overflow-y-auto">
<slot />
</div>
</div>

View file

@ -3,12 +3,10 @@
import { UIEventSource } from "../../Logic/UIEventSource"
import Popup from "./Popup.svelte"
export let onlyLink: boolean = false
export let bodyPadding = "p-4 md:p-5 "
export let fullscreen: boolean = false
export let shown: UIEventSource<boolean>
</script>
{#if !onlyLink}

View file

@ -8,7 +8,8 @@
export let fullscreen: boolean = false
const shared = "in-page normal-background dark:bg-gray-800 rounded-lg border-gray-200 dark:border-gray-700 border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md"
const shared =
"in-page normal-background dark:bg-gray-800 rounded-lg border-gray-200 dark:border-gray-700 border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md"
let defaultClass = "relative flex flex-col mx-auto w-full divide-y " + shared
if (fullscreen) {
defaultClass = shared
@ -27,20 +28,23 @@
export let shown: UIEventSource<boolean>
export let dismissable = true
let _shown = false
shown.addCallbackAndRun(sh => {
shown.addCallbackAndRun((sh) => {
_shown = sh
})
</script>
<Modal open={_shown} on:close={() => shown.set(false)} outsideclose
size="xl"
{dismissable}
{defaultClass} {bodyClass} {dialogClass} {headerClass}
color="none">
<Modal
open={_shown}
on:close={() => shown.set(false)}
outsideclose
size="xl"
{dismissable}
{defaultClass}
{bodyClass}
{dialogClass}
{headerClass}
color="none"
>
<svelte:fragment slot="header">
{#if $$slots.header}
<h1 class="page-header w-full">

View file

@ -10,7 +10,7 @@
export let value: UIEventSource<string>
let _value = value.data ?? ""
value.addCallbackD(v => {
value.addCallbackD((v) => {
_value = v
})
$: value.set(_value)
@ -22,7 +22,7 @@
export let autofocus = false
isFocused?.addCallback(focussed => {
isFocused?.addCallback((focussed) => {
if (focussed) {
requestAnimationFrame(() => {
if (document.activeElement !== inputElement) {
@ -33,41 +33,44 @@
}
})
if(autofocus){
if (autofocus) {
isFocused.set(true)
}
</script>
<form
class="w-full"
on:submit|preventDefault={() => dispatch("search")}
>
<form class="w-full" on:submit|preventDefault={() => dispatch("search")}>
<label
class="neutral-label normal-background flex w-full items-center rounded-full border border-black box-shadow"
class="neutral-label normal-background box-shadow flex w-full items-center rounded-full border border-black"
>
<SearchIcon aria-hidden="true" class="h-8 w-8 ml-2" />
<SearchIcon aria-hidden="true" class="ml-2 h-8 w-8" />
<input
bind:this={inputElement}
on:focus={() => {isFocused?.setData(true)}}
on:blur={() => {isFocused?.setData(false)}}
on:focus={() => {
isFocused?.setData(true)
}}
on:blur={() => {
isFocused?.setData(false)
}}
type="search"
style=" --tw-ring-color: rgb(0 0 0 / 0) !important;"
class="px-0 ml-1 w-full outline-none border-none"
class="ml-1 w-full border-none px-0 outline-none"
on:keypress={(keypr) => {
return keypr.key === "Enter" ? dispatch("search") : undefined
}}
return keypr.key === "Enter" ? dispatch("search") : undefined
}}
bind:value={_value}
use:set_placeholder={placeholder}
use:ariaLabel={placeholder}
/>
{#if $value.length > 0}
<Backspace on:click={() => value.set("")} color="var(--button-background)" class="w-6 h-6 mr-3 cursor-pointer" />
<Backspace
on:click={() => value.set("")}
color="var(--button-background)"
class="mr-3 h-6 w-6 cursor-pointer"
/>
{:else}
<div class="w-6 mr-3" />
<div class="mr-3 w-6" />
{/if}
</label>
</form>

View file

@ -1,55 +1,64 @@
<div class="sidebar-unit">
<slot/>
<slot />
</div>
<style>
:global(.sidebar-unit) {
display: flex;
flex-direction: column;
row-gap: 0.25rem;
background: var(--background-color);
padding: 0.5rem;
border-radius: 0.5rem;
}
:global(.sidebar-unit) {
display: flex;
flex-direction: column;
row-gap: 0.25rem;
background: var(--background-color);
padding: 0.5rem;
border-radius: 0.5rem;
}
:global(.sidebar-unit > h3) {
margin-top: 0;
margin-bottom: 0.5rem;
padding: 0.25rem;
}
:global(.sidebar-unit > h3) {
margin-top: 0;
margin-bottom: 0.5rem;
padding: 0.25rem;
}
:global(.sidebar-button svg, .sidebar-button img, .sidebar-unit > button img, .sidebar-unit > button svg) {
width: 1.5rem;
height: 1.5rem;
margin-right: 0.5rem;
flex-shrink: 0;
}
:global(
.sidebar-button svg,
.sidebar-button img,
.sidebar-unit > button img,
.sidebar-unit > button svg
) {
width: 1.5rem;
height: 1.5rem;
margin-right: 0.5rem;
flex-shrink: 0;
}
:global(.sidebar-button .weblate-link > svg) {
width: 0.75rem;
height: 0.75rem;
flex-shrink: 0;
}
:global(.sidebar-button .weblate-link > svg) {
width: 0.75rem;
height: 0.75rem;
flex-shrink: 0;
}
:global(.sidebar-button, .sidebar-unit > a, .sidebar-unit > button) {
display: flex;
align-items: center;
border-radius: 0.25rem !important;
padding: 0.4rem 0.75rem !important;
text-decoration: none !important;
width: 100%;
text-align: start;
}
:global(.sidebar-button, .sidebar-unit > a, .sidebar-unit > button) {
display: flex;
align-items: center;
border-radius: 0.25rem !important;
padding: 0.4rem 0.75rem !important;
text-decoration: none !important;
width: 100%;
text-align: start;
}
:global(.sidebar-button > svg , .sidebar-button > img, .sidebar-unit > a img, .sidebar-unit > a svg, .sidebar-unit > button svg, .sidebar-unit > button img) {
margin-right: 0.5rem;
flex-shrink: 0;
}
:global(.sidebar-button:hover, .sidebar-unit > a:hover, .sidebar-unit > button:hover) {
background: var(--low-interaction-background) !important;
}
:global(
.sidebar-button > svg,
.sidebar-button > img,
.sidebar-unit > a img,
.sidebar-unit > a svg,
.sidebar-unit > button svg,
.sidebar-unit > button img
) {
margin-right: 0.5rem;
flex-shrink: 0;
}
:global(.sidebar-button:hover, .sidebar-unit > a:hover, .sidebar-unit > button:hover) {
background: var(--low-interaction-background) !important;
}
</style>

View file

@ -31,7 +31,7 @@
return state.sync(
(f) => f === 0,
[],
(b) => (b ? 0 : undefined),
(b) => (b ? 0 : undefined)
)
}
@ -92,7 +92,7 @@
{/if}
</div>
{:else if $isDebugging}
<div class="code">
{layer.id} (no name)
</div>
<div class="code">
{layer.id} (no name)
</div>
{/if}

View file

@ -55,14 +55,18 @@
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
</script>
<div class="low-interaction p-1 rounded-2xl px-3" class:interactive={$firstValue?.length > 0}>
<div class="low-interaction rounded-2xl p-1 px-3" class:interactive={$firstValue?.length > 0}>
{#each parts as part, i}
{#if part["subs"]}
<!-- This is a field! -->
<span class="mx-1">
<InputHelper value={fieldValues[part["subs"]]} type={fieldTypes[part["subs"]]}>
<ValidatedInput slot="fallback" value={fieldValues[part["subs"]]} type={fieldTypes[part["subs"]]}
{feedback} />
<ValidatedInput
slot="fallback"
value={fieldValues[part["subs"]]}
type={fieldTypes[part["subs"]]}
{feedback}
/>
</InputHelper>
</span>
{:else}
@ -70,6 +74,6 @@
{/if}
{/each}
{#if $feedback}
<Tr cls="alert" t={$feedback}/>
<Tr cls="alert" t={$feedback} />
{/if}
</div>

View file

@ -150,7 +150,6 @@
</LoginToggle>
<LanguagePicker />
</SidebarUnit>
<!-- Theme related: documentation links, download, ... -->
@ -218,7 +217,6 @@
<!-- Other links and tools for the given location: open iD/JOSM; community index, ... -->
<SidebarUnit>
<h3>
<Tr t={t.moreUtilsTitle} />
</h3>
@ -238,13 +236,13 @@
<MapillaryLink large={false} mapProperties={state.mapProperties} />
</If>
<a class="flex sidebar-button" href="geo:{$location.lat},{$location.lon}"><ShareIcon /><Tr t={t.openHereDifferentApp}/></a>
<a class="sidebar-button flex" href="geo:{$location.lat},{$location.lon}">
<ShareIcon /><Tr t={t.openHereDifferentApp} />
</a>
</SidebarUnit>
<!-- About MC: various outward links, legal info, ... -->
<SidebarUnit>
<h3>
<Tr t={Translations.t.general.menu.aboutMapComplete} />
</h3>
@ -275,11 +273,11 @@
</a>
<a class="flex" href="mailto:info@mapcomplete.org">
<EnvelopeOpen class="h-6 w-6"/>
<Tr t={Translations.t.general.attribution.emailCreators}/>
<EnvelopeOpen class="h-6 w-6" />
<Tr t={Translations.t.general.attribution.emailCreators} />
</a>
<a class="flex" href="https://hosted.weblate.org/projects/mapcomplete/" target="_blank">
<TranslateIcon class="h-6 w-6"/>
<TranslateIcon class="h-6 w-6" />
<Tr t={Translations.t.translations.activateButton} />
</a>
@ -322,6 +320,3 @@
</div>
</SidebarUnit>
</div>

View file

@ -83,7 +83,11 @@
let featuresForLayer: FeatureSource = state.perLayer.get(targetLayer.id)
if (featuresForLayer) {
if (dontShow) {
featuresForLayer = new StaticFeatureSource(featuresForLayer.features.map(feats => feats.filter(f => dontShow.indexOf(f.properties.id) < 0)))
featuresForLayer = new StaticFeatureSource(
featuresForLayer.features.map((feats) =>
feats.filter((f) => dontShow.indexOf(f.properties.id) < 0)
)
)
}
new ShowDataLayer(map, {
layer: targetLayer,
@ -116,7 +120,7 @@
allowUnsnapped: true,
snappedTo,
snapLocation: value,
},
}
)
const withCorrectedAttributes = new StaticFeatureSource(
snappedLocation.features.mapD((feats) =>
@ -130,8 +134,8 @@
...f,
properties,
}
}),
),
})
)
)
// The actual point to be created, snapped at the new location
new ShowDataLayer(map, {
@ -140,14 +144,13 @@
})
withCorrectedAttributes.features.addCallbackAndRunD((f) => console.log("Snapped point is", f))
}
</script>
<LocationInput
{map}
on:click
{mapProperties}
value={ snapToLayers?.length > 0 ? new UIEventSource(undefined) : value}
value={snapToLayers?.length > 0 ? new UIEventSource(undefined) : value}
initialCoordinate={coordinate}
{maxDistanceInMeters}
>

View file

@ -6,7 +6,7 @@
import { twMerge } from "tailwind-merge"
import { PanoramaxXYZ, Panoramax } from "panoramax-js/dist"
import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte"
import {default as Panoramax_svg} from "../../assets/svg/Panoramax.svelte"
import { default as Panoramax_svg } from "../../assets/svg/Panoramax.svelte"
/*
A subtleButton which opens panoramax in a new tab at the current location
@ -19,11 +19,14 @@
}
let location = mapProperties.location
let zoom = mapProperties.zoom
let href = location.mapD(location =>
host.createViewLink({
location,
zoom: zoom.data,
}), [zoom])
let href = location.mapD(
(location) =>
host.createViewLink({
location,
zoom: zoom.data,
}),
[zoom]
)
export let large: boolean = true
</script>

View file

@ -116,7 +116,6 @@
</script>
<div class="link-underline flex flex-col">
<div class="flex flex-col">
<Tr t={tr.intro} />
<Copyable {state} text={linkToShare} />

View file

@ -6,7 +6,7 @@
import Translations from "../i18n/Translations"
import Marker from "../Map/Marker.svelte"
export let theme: MinimalThemeInformation & {isOfficial?: boolean}
export let theme: MinimalThemeInformation & { isOfficial?: boolean }
let isCustom: boolean = theme.id.startsWith("https://") || theme.id.startsWith("http://")
export let state: { layoutToUse?: { id: string }; osmConnection: OsmConnection }
@ -66,12 +66,12 @@
let href = createUrl(theme, isCustom, state)
</script>
<a class="low-interaction my-1 flex w-full items-center text-ellipsis rounded p-1" href={$href}>
<Marker icons={theme.icon} size="block h-8 w-8 sm:h-11 sm:w-11 m-1 sm:mx-2 md:mx-4 shrink-0" />
<a class="low-interaction my-1 flex w-full items-center text-ellipsis rounded p-1" href={$href}>
<Marker icons={theme.icon} size="block h-8 w-8 sm:h-11 sm:w-11 m-1 sm:mx-2 md:mx-4 shrink-0" />
<span class="flex flex-col overflow-hidden text-ellipsis text-xl font-bold">
<Tr cls="" t={title} />
<Tr cls="subtle text-base" t={description} />
<slot/>
</span>
</a>
<span class="flex flex-col overflow-hidden text-ellipsis text-xl font-bold">
<Tr cls="" t={title} />
<Tr cls="subtle text-base" t={description} />
<slot />
</span>
</a>

View file

@ -12,22 +12,18 @@
export let themes: MinimalThemeInformation[]
export let state: { osmConnection: OsmConnection }
export let hasSelection : boolean = true
export let hasSelection: boolean = true
</script>
<section class="w-full">
<slot name="title" />
<div class="theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3">
{#each themes as theme (theme.id)}
<ThemeButton
{theme}
{state}
>
<ThemeButton {theme} {state}>
{#if $search && hasSelection && themes?.[0] === theme}
<span class="thanks hidden-on-mobile" aria-hidden="true">
<Tr t={Translations.t.general.morescreen.enterToOpen} />
</span>
<span class="thanks hidden-on-mobile" aria-hidden="true">
<Tr t={Translations.t.general.morescreen.enterToOpen} />
</span>
{/if}
</ThemeButton>
{/each}

View file

@ -12,8 +12,5 @@
osmConnection: OsmConnection
}
let customThemes
</script>

View file

@ -22,7 +22,7 @@
JSON.stringify(contents),
"mapcomplete-favourites-" + new Date().toISOString() + ".geojson",
{
mimetype: "application/vnd.geo+json"
mimetype: "application/vnd.geo+json",
}
)
}
@ -33,7 +33,7 @@
gpx,
"mapcomplete-favourites-" + new Date().toISOString() + ".gpx",
{
mimetype: "{gpx=application/gpx+xml}"
mimetype: "{gpx=application/gpx+xml}",
}
)
}

View file

@ -3,8 +3,8 @@
export let expanded = false
export let noBorder = false
let defaultClass: string = undefined
if(noBorder){
let defaultClass: string = undefined
if (noBorder) {
defaultClass = "unstyled w-full flex-grow"
}
</script>

View file

@ -31,14 +31,18 @@
export let canZoom = previewedImage !== undefined
let loaded = false
let showBigPreview = new UIEventSource(false)
onDestroy(showBigPreview.addCallbackAndRun(shown => {
if (!shown) {
previewedImage.set(undefined)
}
}))
onDestroy(previewedImage.addCallbackAndRun(previewedImage => {
showBigPreview.set(previewedImage?.id === image.id)
}))
onDestroy(
showBigPreview.addCallbackAndRun((shown) => {
if (!shown) {
previewedImage.set(undefined)
}
})
)
onDestroy(
previewedImage.addCallbackAndRun((previewedImage) => {
showBigPreview.set(previewedImage?.id === image.id)
})
)
function highlight(entered: boolean = true) {
if (!entered) {
@ -72,43 +76,49 @@
</ImageOperations>
</div>
<div class="absolute top-4 right-4">
<CloseButton class="normal-background"
on:click={() => {console.log("Closing");previewedImage.set(undefined)}}></CloseButton>
<CloseButton
class="normal-background"
on:click={() => {
console.log("Closing")
previewedImage.set(undefined)
}}
/>
</div>
</Popup>
{#if image.status !== undefined && image.status !== "ready"}
<div class="h-full flex flex-col justify-center">
<div class="flex h-full flex-col justify-center">
<Loading>
<Tr t={Translations.t.image.processing}/>
<Tr t={Translations.t.image.processing} />
</Loading>
</div>
{:else}
<div class="relative shrink-0">
<div class="relative w-fit"
on:mouseenter={() => highlight()}
on:mouseleave={() => highlight(false)}
<div
class="relative w-fit"
on:mouseenter={() => highlight()}
on:mouseleave={() => highlight(false)}
>
<img
bind:this={imgEl}
on:load={() => (loaded = true)}
class={imgClass ?? ""}
class:cursor-zoom-in={canZoom}
on:click={() => {
previewedImage?.set(image)
}}
previewedImage?.set(image)
}}
on:error={() => {
if (fallbackImage) {
imgEl.src = fallbackImage
}
}}
if (fallbackImage) {
imgEl.src = fallbackImage
}
}}
src={image.url}
/>
{#if canZoom && loaded}
<div
class="bg-black-transparent absolute right-0 top-0 rounded-bl-full"
on:click={() => previewedImage.set(image)}>
on:click={() => previewedImage.set(image)}
>
<MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" />
</div>
{/if}

View file

@ -56,7 +56,5 @@
</div>
<slot />
</div>
</div>

View file

@ -35,7 +35,7 @@
key: undefined,
provider: AllImageProviders.byName(image.provider),
date: new Date(image.date),
id: Object.values(image.osmTags)[0]
id: Object.values(image.osmTags)[0],
}
async function applyLink(isLinked: boolean) {
@ -46,7 +46,7 @@
if (isLinked) {
const action = new LinkImageAction(currentTags.id, key, url, tags, {
theme: tags.data._orig_theme ?? state.theme.id,
changeType: "link-image"
changeType: "link-image",
})
await state.changes.applyAction(action)
} else {
@ -55,7 +55,7 @@
if (v === url) {
const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, {
theme: tags.data._orig_theme ?? state.theme.id,
changeType: "remove-image"
changeType: "remove-image",
})
state.changes.applyAction(action)
}
@ -67,9 +67,8 @@
let element: HTMLDivElement
if (highlighted) {
onDestroy(
highlighted.addCallbackD(highlightedUrl => {
highlighted.addCallbackD((highlightedUrl) => {
if (highlightedUrl === image.pictureUrl) {
Utils.scrollIntoView(element)
}

View file

@ -25,7 +25,6 @@
import { BBox } from "../../Logic/BBox"
import PanoramaxLink from "../BigComponents/PanoramaxLink.svelte"
export let tags: UIEventSource<OsmTags>
export let state: SpecialVisualizationState
export let lon: number
@ -38,7 +37,7 @@
let imagesProvider = state.nearbyImageSearcher
let loadedImages = AllImageProviders.LoadImagesFor(tags).mapD(
(loaded) => new Set(loaded.map((img) => img.url)),
(loaded) => new Set(loaded.map((img) => img.url))
)
let imageState = imagesProvider.getImagesAround(lon, lat)
let result: Store<P4CPicture[]> = imageState.images.mapD(
@ -47,53 +46,61 @@
.filter(
(p: P4CPicture) =>
!loadedImages.data.has(p.pictureUrl) && // We don't show any image which is already linked
!p.details.isSpherical,
!p.details.isSpherical
)
.slice(0, 25),
[loadedImages],
[loadedImages]
)
let asFeatures = result.map(p4cs => p4cs.map(p4c => (<Feature<Point>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [p4c.coordinates.lng, p4c.coordinates.lat],
},
properties: {
id: p4c.pictureUrl,
rotation: p4c.direction,
},
})))
let asFeatures = result.map((p4cs) =>
p4cs.map(
(p4c) =>
<Feature<Point>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [p4c.coordinates.lng, p4c.coordinates.lat],
},
properties: {
id: p4c.pictureUrl,
rotation: p4c.direction,
},
}
)
)
let selected = new UIEventSource<P4CPicture>(undefined)
let selectedAsFeature = selected.mapD(s => {
return [<Feature<Point>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [s.coordinates.lng, s.coordinates.lat],
let selectedAsFeature = selected.mapD((s) => {
return [
<Feature<Point>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [s.coordinates.lng, s.coordinates.lat],
},
properties: {
id: s.pictureUrl,
selected: "yes",
rotation: s.direction,
},
},
properties: {
id: s.pictureUrl,
selected: "yes",
rotation: s.direction,
},
}]
]
})
let someLoading = imageState.state.mapD((stateRecord) =>
Object.values(stateRecord).some((v) => v === "loading"),
Object.values(stateRecord).some((v) => v === "loading")
)
let errors = imageState.state.mapD((stateRecord) =>
Object.keys(stateRecord).filter((k) => stateRecord[k] === "error"),
Object.keys(stateRecord).filter((k) => stateRecord[k] === "error")
)
let highlighted = new UIEventSource<string>(undefined)
onDestroy(highlighted.addCallbackD(hl => {
const p4c = result.data?.find(i => i.pictureUrl === hl)
onDestroy(
highlighted.addCallbackD((hl) => {
const p4c = result.data?.find((i) => i.pictureUrl === hl)
selected.set(p4c)
},
))
})
)
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let mapProperties = new MapLibreAdaptor(map, {
@ -104,7 +111,6 @@
location: new UIEventSource({ lon, lat }),
})
const geocodedImageLayer = new LayerConfig(<LayerConfigJson>geocoded_image)
new ShowDataLayer(map, {
features: new StaticFeatureSource(asFeatures),
@ -115,15 +121,10 @@
},
})
ShowDataLayer.showMultipleLayers(
map,
new StaticFeatureSource([feature]),
state.theme.layers,
)
ShowDataLayer.showMultipleLayers(map, new StaticFeatureSource([feature]), state.theme.layers)
onDestroy(
asFeatures.addCallbackAndRunD(features => {
asFeatures.addCallbackAndRunD((features) => {
if (features.length == 0) {
return
}
@ -132,7 +133,7 @@
bbox = bbox.unionWith(BBox.get(f))
}
mapProperties.maxbounds.set(bbox.pad(4))
}),
})
)
new ShowDataLayer(map, {
@ -142,8 +143,6 @@
highlighted.set(feature.properties.id)
},
})
</script>
<div class="flex flex-col">
@ -158,16 +157,23 @@
{:else}
<div class="flex w-full space-x-4 overflow-x-auto" style="scroll-snap-type: x proximity">
{#each $result as image (image.pictureUrl)}
<span class="w-fit shrink-0" style="scroll-snap-align: start"
on:mouseenter={() => {highlighted.set(image.pictureUrl)}}
on:mouseleave={() =>{ highlighted.set(undefined); selected.set(undefined)}}
<span
class="w-fit shrink-0"
style="scroll-snap-align: start"
on:mouseenter={() => {
highlighted.set(image.pictureUrl)
}}
on:mouseleave={() => {
highlighted.set(undefined)
selected.set(undefined)
}}
>
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} {highlighted} />
</span>
{/each}
</div>
{/if}
<div class="w-full flex flex-wrap justify-end gap-x-8 pt-2">
<div class="flex w-full flex-wrap justify-end gap-x-8 pt-2">
<PanoramaxLink
large={false}
mapProperties={{ zoom: new ImmutableStore(16), location: new ImmutableStore({ lon, lat }) }}
@ -178,7 +184,6 @@
/>
</div>
<div class="my-2 flex justify-between">
<div>
{#if $someLoading && $result.length > 0}
@ -193,7 +198,6 @@
</div>
</div>
<div class="h-48">
<MaplibreMap interactive={false} {map} {mapProperties} />
</div>

View file

@ -30,7 +30,13 @@
</script>
{#if enableLogin.data}
<button on:click={() => {shown.set(!shown.data)}}><Tr t={t.seeNearby}/> </button>
<button
on:click={() => {
shown.set(!shown.data)
}}
>
<Tr t={t.seeNearby} />
</button>
<Popup {shown} bodyPadding="p-4">
<span slot="header">
<Tr t={t.seeNearby} />

View file

@ -49,16 +49,20 @@
}
if (layer?.id === "note") {
const uploadResult = await state?.imageUploadManager.uploadImageWithLicense(tags.data.id,
const uploadResult = await state?.imageUploadManager.uploadImageWithLicense(
tags.data.id,
state.osmConnection.userDetails.data?.name ?? "Anonymous",
file, "image", noBlur)
file,
"image",
noBlur
)
if (!uploadResult) {
return
}
const url = uploadResult.absoluteUrl
await state.osmConnection.addCommentToNote(tags.data.id, url)
NoteCommentElement.addCommentTo(url, <UIEventSource<any>>tags, {
osmConnection: state.osmConnection
osmConnection: state.osmConnection,
})
return
}
@ -88,7 +92,7 @@
multiple={true}
on:submit={(e) => handleFiles(e.detail)}
>
<div class="flex items-center text-2xl w-full justify-center">
<div class="flex w-full items-center justify-center text-2xl">
{#if image !== undefined}
<img src={image} aria-hidden="true" />
{:else}
@ -98,19 +102,17 @@
{labelText}
{:else}
<div class="flex flex-col">
<Tr t={t.addPicture} />
{#if noBlur}
<span class="subtle text-sm">
<Tr t={t.upload.noBlur}/>
</span>
<span class="subtle text-sm">
<Tr t={t.upload.noBlur} />
</span>
{/if}
</div>
{/if}
</div>
</FileSelector>
<div class="text-xs subtle italic">
<div class="subtle text-xs italic">
<Tr t={Translations.t.general.attribution.panoramaxLicenseCCBYSA} />
<span class="mx-1"></span>
<Tr t={t.respectPrivacy} />

View file

@ -20,7 +20,7 @@
let mla = new MapLibreAdaptor(map, mapProperties)
mla.allowMoving.setData(false)
mla.allowZooming.setData(false)
state?.mapProperties?.rasterLayer?.addCallbackAndRunD(l => mla.rasterLayer.set(l))
state?.mapProperties?.rasterLayer?.addCallbackAndRunD((l) => mla.rasterLayer.set(l))
let directionElem: HTMLElement | undefined
$: value.addCallbackAndRunD((degrees) => {

View file

@ -7,7 +7,7 @@
export let wd: number
export let h: number
export let type: "full" | "half"
let dispatch = createEventDispatcher<{ "start", "end", "move","clear" }>()
let dispatch = createEventDispatcher<{ start; end; move; clear }>()
let element: HTMLElement
function send(signal: "start" | "end" | "move", ev: Event) {
@ -39,32 +39,30 @@
element.onmouseenter = (ev) => send("move", ev)
element.onmouseup = (ev) => send("end", ev)
element.addEventListener("touchstart", ev => dispatch("start", ev))
element.addEventListener("touchend", ev => {
element.addEventListener("touchstart", (ev) => dispatch("start", ev))
element.addEventListener("touchend", (ev) => {
const el = elementUnderTouch(ev)
if (el?.onmouseup) {
el?.onmouseup(<any>ev)
}else{
} else {
// We dragged outside of the table
dispatch("clear")
}
})
element.addEventListener("touchmove", ev => {
element.addEventListener("touchmove", (ev) => {
const underTouch = elementUnderTouch(ev)
if(typeof underTouch?.onmouseenter !== "function"){
return
}
if (typeof underTouch?.onmouseenter !== "function") {
return
}
underTouch.onmouseenter(<any>ev)
underTouch.onmouseenter(<any>ev)
})
})
</script>
<td bind:this={element} id={"oh-"+type+"-"+h+"-"+wd}
class:border-black={(h + 1) % 6 === 0}
class={`oh-timecell oh-timecell-${type} oh-timecell-${wd} `}
<td
bind:this={element}
id={"oh-" + type + "-" + h + "-" + wd}
class:border-black={(h + 1) % 6 === 0}
class={`oh-timecell oh-timecell-${type} oh-timecell-${wd} `}
/>

View file

@ -1,5 +1,4 @@
<script lang="ts">
import { UIEventSource } from "../../../../Logic/UIEventSource"
import type { OpeningHour } from "../../../OpeningHours/OpeningHours"
import { OH as OpeningHours } from "../../../OpeningHours/OpeningHours"
@ -27,10 +26,9 @@
let element: HTMLTableElement
function range(n: number) {
return Utils.TimesT(n, n => n)
return Utils.TimesT(n, (n) => n)
}
function clearSelection() {
const allCells = Array.from(document.getElementsByClassName("oh-timecell"))
for (const timecell of allCells) {
@ -38,27 +36,33 @@
}
}
function setSelectionNormalized(weekdayStart: number, weekdayEnd: number, hourStart: number, hourEnd: number) {
function setSelectionNormalized(
weekdayStart: number,
weekdayEnd: number,
hourStart: number,
hourEnd: number
) {
for (let wd = weekdayStart; wd <= weekdayEnd; wd++) {
for (let h = (hourStart); h < (hourEnd); h++) {
for (let h = hourStart; h < hourEnd; h++) {
h = Math.floor(h)
if (h >= hourStart && h < hourEnd) {
const elFull = document.getElementById("oh-full-" + h + "-" + wd)
elFull?.classList?.add("oh-timecell-selected")
}
if (h + 0.5 < hourEnd) {
const elHalf = document.getElementById("oh-half-" + h + "-" + wd)
elHalf?.classList?.add("oh-timecell-selected")
}
}
}
}
function setSelection(weekdayStart: number, weekdayEnd: number, hourStart: number, hourEnd: number) {
function setSelection(
weekdayStart: number,
weekdayEnd: number,
hourStart: number,
hourEnd: number
) {
let hourA = hourStart
let hourB = hourEnd
if (hourA > hourB) {
@ -69,8 +73,12 @@
hourA -= 0.5
hourB += 0.5
}
setSelectionNormalized(Math.min(weekdayStart, weekdayEnd), Math.max(weekdayStart, weekdayEnd),
hourA, hourB)
setSelectionNormalized(
Math.min(weekdayStart, weekdayEnd),
Math.max(weekdayStart, weekdayEnd),
hourA,
hourB
)
}
let selectionStart: [number, number] = undefined
@ -100,8 +108,11 @@
let startMinutes = Math.round((start * 60) % 60)
let endMinutes = Math.round((end * 60) % 60)
let newOhs = [...value.data]
for (let wd = Math.min(selectionStart[0], weekday); wd <= Math.max(selectionStart[0], weekday); wd++) {
for (
let wd = Math.min(selectionStart[0], weekday);
wd <= Math.max(selectionStart[0], weekday);
wd++
) {
const oh: OpeningHour = {
startHour: Math.floor(start),
endHour: Math.floor(end),
@ -116,7 +127,6 @@
clearSelection()
}
let lasttouched: [number, number] = undefined
function moved(weekday: number, hour: number) {
@ -125,11 +135,10 @@
clearSelection()
setSelection(selectionStart[0], weekday, selectionStart[1], hour + 0.5)
}
const allRows = Array.from(element.getElementsByTagName("tr"))
const allRows = Array.from(element.getElementsByTagName("tr"))
for (const r of allRows) {
r.classList.remove("hover")
r.classList.remove("hovernext")
}
const selectedRow = allRows[hour * 2 + 2]
selectedRow?.classList?.add("hover")
@ -158,26 +167,33 @@
* @param oh
*/
function rangeStyle(oh: OpeningHour, totalHeight: number): string {
const top = (oh.startHour + oh.startMinutes / 60) * totalHeight / 24
const height = (oh.endHour - oh.startHour + (oh.endMinutes - oh.startMinutes) / 60) * totalHeight / 24
const top = ((oh.startHour + oh.startMinutes / 60) * totalHeight) / 24
const height =
((oh.endHour - oh.startHour + (oh.endMinutes - oh.startMinutes) / 60) * totalHeight) / 24
return `top: ${top}px; height: ${height}px; z-index: 20`
}
</script>
<table
bind:this={element}
class="oh-table no-weblate w-full" cellspacing="0" cellpadding="0"
class:hasselection={selectionStart !== undefined} class:hasnoselection={selectionStart === undefined}
on:mouseleave={mouseLeft}>
class="oh-table no-weblate w-full"
cellspacing="0"
cellpadding="0"
class:hasselection={selectionStart !== undefined}
class:hasnoselection={selectionStart === undefined}
on:mouseleave={mouseLeft}
>
<tr>
<!-- Header row -->
<th style="width: 9%">
<!-- Top-left cell -->
<slot name="top-left">
<button class="absolute top-0 left-0 p-1 rounded-full" on:click={() => value.set([])} style="z-index: 10">
<TrashIcon class="w-5 h-5" />
<button
class="absolute top-0 left-0 rounded-full p-1"
on:click={() => value.set([])}
style="z-index: 10"
>
<TrashIcon class="h-5 w-5" />
</button>
</slot>
</th>
@ -188,101 +204,116 @@
{/each}
</tr>
<tr class="h-0 nobold">
<tr class="nobold h-0">
<!-- Virtual row to add the ranges to-->
<td style="width: 9%" />
{#each range(7) as wd}
<td style="width: 13%; position: relative;">
<div class="h-0 pointer-events-none" style="z-index: 10">
{#each $value.filter(oh => oh.weekday === wd).map(oh => OpeningHours.rangeAs24Hr(oh)) as range }
<div class="absolute pointer-events-none px-1 md:px-2 w-full "
style={rangeStyle(range, totalHeight)}
<div class="pointer-events-none h-0" style="z-index: 10">
{#each $value
.filter((oh) => oh.weekday === wd)
.map((oh) => OpeningHours.rangeAs24Hr(oh)) as range}
<div
class="pointer-events-none absolute w-full px-1 md:px-2"
style={rangeStyle(range, totalHeight)}
>
<div class="rounded-xl border-interactive h-full low-interaction flex flex-col justify-between">
<div
class="border-interactive low-interaction flex h-full flex-col justify-between rounded-xl"
>
<div class:hidden={range.endHour - range.startHour < 3}>
{OpeningHours.hhmm(range.startHour, range.startMinutes)}
</div>
<button class="w-fit rounded-full p-1 self-center pointer-events-auto"
on:click={() => {
const cleaned = value.data.filter(v => !OpeningHours.isSame(v, range))
console.log("Cleaned", cleaned, OpeningHours.ToString(value.data))
value.set(cleaned)
}}>
<TrashIcon class="w-6 h-6" />
<button
class="pointer-events-auto w-fit self-center rounded-full p-1"
on:click={() => {
const cleaned = value.data.filter((v) => !OpeningHours.isSame(v, range))
console.log("Cleaned", cleaned, OpeningHours.ToString(value.data))
value.set(cleaned)
}}
>
<TrashIcon class="h-6 w-6" />
</button>
<div class:hidden={range.endHour - range.startHour < 3}>
{OpeningHours.hhmm(range.endHour, range.endMinutes)}
</div>
</div>
</div>
{/each}
</div>
</td>
{/each}
</tr>
{#each range(24) as h}
<tr style="height: 0.75rem; width: 9%"> <!-- even row, for the hour -->
<td rowspan={ h > 0 ? 2: 1 }
class="relative text-sm sm:text-base oh-left-col oh-timecell-full border-box interactive "
style={ h > 0 ? "top: -0.75rem" : "height:0; top: -0.75rem"}>
<tr style="height: 0.75rem; width: 9%">
<!-- even row, for the hour -->
<td
rowspan={h > 0 ? 2 : 1}
class="oh-left-col oh-timecell-full border-box interactive relative text-sm sm:text-base"
style={h > 0 ? "top: -0.75rem" : "height:0; top: -0.75rem"}
>
{#if h > 0}
<span class="hour-header w-full">
{h}:00
{h}:00
</span>
{/if}
</td>
{#each range(7) as wd}
<OHCell type="full" {h} {wd} on:start={() => startSelection(wd, h)} on:end={() => endSelection(wd, h)}
on:move={() => moved(wd, h)} on:clear={() => clearSelection()} />
<OHCell
type="full"
{h}
{wd}
on:start={() => startSelection(wd, h)}
on:end={() => endSelection(wd, h)}
on:move={() => moved(wd, h)}
on:clear={() => clearSelection()}
/>
{/each}
</tr>
<tr style="height: calc( 0.75rem - 1px) "> <!-- odd row, for the half hour -->
<tr style="height: calc( 0.75rem - 1px) ">
<!-- odd row, for the half hour -->
{#if h === 0}
<td/> <!-- extra cell to compensate for irregular header-->
<td />
<!-- extra cell to compensate for irregular header-->
{/if}
{#each range(7) as wd}
<OHCell type="half" {h} {wd} on:start={() => startSelection(wd, h + 0.5)}
on:end={() => endSelection(wd, h + 0.5)}
on:move={() => moved(wd, h + 0.5)} on:clear={() => clearSelection()} />
<OHCell
type="half"
{h}
{wd}
on:start={() => startSelection(wd, h + 0.5)}
on:end={() => endSelection(wd, h + 0.5)}
on:move={() => moved(wd, h + 0.5)}
on:clear={() => clearSelection()}
/>
{/each}
</tr>
{/each}
</table>
<style>
th {
top: 0;
position: sticky;
z-index: 10;
}
th {
top: 0;
position: sticky;
z-index: 10;
}
.hasselection tr:hover .hour-header, .hasselection tr.hover .hour-header {
border-bottom: 2px solid black;
}
.hasselection tr:hover + tr {
font-weight: bold;
}
.hasselection tr.hovernext {
font-weight: bold;
}
.hasnoselection tr:hover, .hasnoselection tr.hover {
font-weight: bold;
}
.hasselection tr:hover .hour-header,
.hasselection tr.hover .hour-header {
border-bottom: 2px solid black;
}
.hasselection tr:hover + tr {
font-weight: bold;
}
.hasselection tr.hovernext {
font-weight: bold;
}
.hasnoselection tr:hover,
.hasnoselection tr.hover {
font-weight: bold;
}
</style>

View file

@ -15,7 +15,6 @@
let postfix = ""
if (args) {
try {
const data = JSON.stringify(args)
if (data["prefix"]) {
prefix = data["prefix"]
@ -31,11 +30,15 @@
const state = new OpeningHoursState(value, prefix, postfix)
let expanded = new UIEventSource(false)
</script>
<Popup bodyPadding="p-0" shown={expanded}>
<OHTable value={state.normalOhs} />
<button on:click={() => expanded.set(false)} class="absolute left-0 bottom-0 primary pointer-events-auto h-8 w-10 rounded-full">
<Check class="shrink-0 w-6 h-6 m-0 p-0" color="white"/>
</button>
<button
on:click={() => expanded.set(false)}
class="primary pointer-events-auto absolute left-0 bottom-0 h-8 w-10 rounded-full"
>
<Check class="m-0 h-6 w-6 shrink-0 p-0" color="white" />
</button>
</Popup>
<button on:click={() => expanded.set(true)}>Pick opening hours</button>
<PublicHolidaySelector value={state.phSelectorValue} />

View file

@ -62,7 +62,7 @@ export default class Validators {
"velopark",
"nsi",
"currency",
"regex"
"regex",
] as const
public static readonly AllValidators: ReadonlyArray<Validator> = [
@ -94,7 +94,7 @@ export default class Validators {
new VeloparkValidator(),
new NameSuggestionIndexValidator(),
new CurrencyValidator(),
new RegexValidator()
new RegexValidator(),
]
private static _byType = Validators._byTypeConstructor()

View file

@ -7,23 +7,24 @@ export default class OpeningHoursValidator extends Validator {
"opening_hours",
[
"Has extra elements to easily input when a POI is opened.",
("### Helper arguments"),
"### Helper arguments",
"Only one helper argument named `options` can be provided. It is a JSON-object of type `{ prefix: string, postfix: string }`:",
MarkdownUtils.table(
["subarg", "doc"],
[
[
"prefix",
"Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse."
"Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse.",
],
[
"postfix",
"Piece of text that will always be added to the end of the generated opening hours"
]
]),
("### Example usage"),
"Piece of text that will always be added to the end of the generated opening hours",
],
]
),
"### Example usage",
"To add a conditional (based on time) access restriction:\n\n```\n" +
`
`
"freeform": {
"key": "access:conditional",
"type": "opening_hours",
@ -34,7 +35,7 @@ export default class OpeningHoursValidator extends Validator {
}
]
}` +
"\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`"
"\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`",
].join("\n")
)
}

View file

@ -48,15 +48,12 @@ export default class PhoneValidator extends Validator {
}
let countryCode: CountryCode = undefined
if (country) {
countryCode = <CountryCode> country()?.toUpperCase()
countryCode = <CountryCode>country()?.toUpperCase()
}
if (this.isShortCode(str, countryCode)) {
return str
}
return parsePhoneNumberFromString(
str,
countryCode
)?.formatInternational()
return parsePhoneNumberFromString(str, countryCode)?.formatInternational()
}
/**

View file

@ -3,16 +3,16 @@ import { s } from "vitest/dist/env-afee91f0"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
export default class RegexValidator extends StringValidator{
export default class RegexValidator extends StringValidator {
constructor() {
super("regex", "Validates a regex")
}
getFeedback(s: string): Translation | undefined {
try{
try {
new RegExp(s)
}catch (e) {
return Translations.T("Not a valid Regex: "+e)
} catch (e) {
return Translations.T("Not a valid Regex: " + e)
}
}

View file

@ -17,12 +17,12 @@ export default class WikidataValidator extends Validator {
[
[
"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"
"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",
"A JSON-object of type `{ removePrefixes: Record<string, string[]>, removePostfixes: Record<string, string[]>, ... }`. See the more detailed explanation below"
]
"A JSON-object of type `{ removePrefixes: Record<string, string[]>, removePostfixes: Record<string, string[]>, ... }`. See the more detailed explanation below",
],
]
),
"#### Suboptions",
@ -31,28 +31,26 @@ export default class WikidataValidator extends Validator {
[
[
"removePrefixes",
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes"
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes",
],
[
"removePostfixes",
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes."
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes.",
],
[
"instanceOf",
"A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans"
"A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans",
],
[
"notInstanceof",
"A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results"
"A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results",
],
[
"multiple",
"If 'yes' or 'true', will allow to select multiple values at once"
]
["multiple", "If 'yes' or 'true', will allow to select multiple values at once"],
]
)
),
].join("\n\n")
private static readonly docsExampleUsage: string = "### Example usage\n\n" +
private static readonly docsExampleUsage: string =
"### Example usage\n\n" +
`The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
\`\`\`json
@ -96,9 +94,13 @@ Another example is to search for species and trees:
\`\`\`
`
constructor() {
super("wikidata", "A wikidata identifier, e.g. Q42.\n\n" + WikidataValidator.docs + WikidataValidator.docsExampleUsage)
super(
"wikidata",
"A wikidata identifier, e.g. Q42.\n\n" +
WikidataValidator.docs +
WikidataValidator.docsExampleUsage
)
}
public isValid(str): boolean {

View file

@ -157,18 +157,18 @@
<LockClosed class={clss} {color} />
{:else if icon === "key"}
<Key class={clss} {color} />
{:else if icon==="globe_alt"}
{:else if icon === "globe_alt"}
<GlobeAltIcon class={clss} {color} />
{:else if icon === "building_office_2"}
<BuildingOffice2 class={clss} {color} />
{:else if icon === "house"}
<HomeIcon class={clss} {color} />
{:else if icon === "train"}
<Train {color} class={clss}/>
{:else if icon === "train"}
<Train {color} class={clss} />
{:else if icon === "airport"}
<Airport {color} class={clss}/>
<Airport {color} class={clss} />
{:else if icon === "building_storefront"}
<BuildingStorefront {color} class={clss}/>
<BuildingStorefront {color} class={clss} />
{:else if icon === "snap"}
<Snap class={clss} />
{:else if Utils.isEmoji(icon)}

View file

@ -1,5 +1,10 @@
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import maplibregl, { Map as MLMap, Map as MlMap, ScaleControl, SourceSpecification } from "maplibre-gl"
import maplibregl, {
Map as MLMap,
Map as MlMap,
ScaleControl,
SourceSpecification,
} from "maplibre-gl"
import { RasterLayerPolygon } from "../../Models/RasterLayers"
import { Utils } from "../../Utils"
import { BBox } from "../../Logic/BBox"
@ -23,13 +28,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 }>
private readonly isFlying = new UIEventSource(false)
@ -225,7 +230,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
this.allowZooming.addCallbackAndRun((allowZooming) => self.setAllowZooming(allowZooming))
this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds))
this.useTerrain?.addCallbackAndRun((useTerrain) => self.setTerrain(useTerrain))
this.showScale?.addCallbackAndRun(showScale => self.setScale(showScale))
this.showScale?.addCallbackAndRun((showScale) => self.setScale(showScale))
}
/**
@ -240,9 +245,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),
}
}
@ -310,7 +315,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++) {
@ -499,7 +504,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)
@ -693,14 +698,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)
@ -716,17 +721,16 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
return
}
if (!showScale) {
if(this.scaleControl){
if (this.scaleControl) {
map.removeControl(this.scaleControl)
this.scaleControl = undefined
}
return
}
if (this.scaleControl === undefined) {
this.scaleControl = new ScaleControl({
maxWidth: 100,
unit: "metric"
unit: "metric",
})
}
if (!map.hasControl(this.scaleControl)) {
@ -739,7 +743,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
window.requestAnimationFrame(() => {
this._maplibreMap.data?.flyTo({
zoom,
center: [lon, lat]
center: [lon, lat],
})
})
}

View file

@ -48,8 +48,11 @@ class PointRenderingLayer {
this._onClick = onClick
this._selectedElement = selectedElement
const self = this
if(!features?.features){
throw "Could not setup a PointRenderingLayer; features?.features is undefined/null. The layer is "+layer.id
if (!features?.features) {
throw (
"Could not setup a PointRenderingLayer; features?.features is undefined/null. The layer is " +
layer.id
)
}
features.features?.addCallbackAndRunD((features) => self.updateFeatures(features))
visibility?.addCallbackAndRunD((visible) => {
@ -163,7 +166,7 @@ class PointRenderingLayer {
})
if (this._onClick) {
el.addEventListener("click", (ev)=> {
el.addEventListener("click", (ev) => {
ev.preventDefault()
this._onClick(feature)
// Workaround to signal the MapLibreAdaptor to ignore this click

View file

@ -859,8 +859,14 @@ This list will be sorted
return ranges
}
public static isSame(a: OpeningHour, b: OpeningHour){
return a.weekday === b.weekday && a.startHour === b.startHour && a.startMinutes === b.startMinutes && a.endHour === b.endHour && a.endMinutes === b.endMinutes
public static isSame(a: OpeningHour, b: OpeningHour) {
return (
a.weekday === b.weekday &&
a.startHour === b.startHour &&
a.startMinutes === b.startMinutes &&
a.endHour === b.endHour &&
a.endMinutes === b.endMinutes
)
}
private static multiply(
weekdays: number[],
@ -930,11 +936,12 @@ This list will be sorted
* OH.rangeAs24Hr(oh).endHour // => 24
*/
static rangeAs24Hr(oh: OpeningHour) {
if(oh.endHour === 0){
return {
...oh, endHour : 24
}
}
if (oh.endHour === 0) {
return {
...oh,
endHour: 24,
}
}
return oh
}
}

Some files were not shown because too many files have changed in this diff Show more