chore: automated housekeeping...

This commit is contained in:
Pieter Vander Vennet 2025-08-13 23:06:38 +02:00
parent 9cd7ad597d
commit 69ab755f29
520 changed files with 16616 additions and 13483 deletions

View file

@ -1,6 +1,10 @@
import { Store, UIEventSource } from "../UIEventSource"
import { Utils } from "../../Utils"
import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "../../Models/RasterLayers"
import {
AvailableRasterLayers,
RasterLayerPolygon,
RasterLayerUtils,
} from "../../Models/RasterLayers"
/**
* When a user pans around on the map, they might pan out of the range of the current background raster layer.
@ -22,10 +26,7 @@ export default class BackgroundLayerResetter {
(global) => global.properties.id !== l.properties.id
)
) {
BackgroundLayerResetter.installHandler(
currentBackgroundLayer,
availableLayers
)
BackgroundLayerResetter.installHandler(currentBackgroundLayer, availableLayers)
return true // unregister
}
})

View file

@ -144,25 +144,27 @@ export default class DetermineTheme {
if (json.layers === undefined && json.tagRenderings !== undefined) {
// We got fed a layer instead of a theme
const layerConfig = <LayerConfigJson>json
let icon = Lists.noNull(layerConfig.pointRendering
.flatMap((pr) => pr.marker)
.map((iconSpec) => {
if (!iconSpec) {
return undefined
}
const icon = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.icon)
.render.txt
if (
iconSpec.color === undefined ||
icon.startsWith("http:") ||
icon.startsWith("https:")
) {
return icon
}
const color = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.color)
.render.txt
return icon + ":" + color
})).join(";")
let icon = Lists.noNull(
layerConfig.pointRendering
.flatMap((pr) => pr.marker)
.map((iconSpec) => {
if (!iconSpec) {
return undefined
}
const icon = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.icon)
.render.txt
if (
iconSpec.color === undefined ||
icon.startsWith("http:") ||
icon.startsWith("https:")
) {
return icon
}
const color = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.color)
.render.txt
return icon + ":" + color
})
).join(";")
if (!icon) {
icon = "./assets/svg/bug.svg"

View file

@ -7,21 +7,24 @@ export interface FeatureSource<T extends Feature = Feature<Geometry, OsmTags>> {
features: Store<T[]>
}
export interface UpdatableFeatureSource<T extends Feature = Feature<Geometry, OsmTags>> extends FeatureSource<T> {
export interface UpdatableFeatureSource<T extends Feature = Feature<Geometry, OsmTags>>
extends FeatureSource<T> {
/**
* Forces an update and downloads the data, even if the feature source is supposed to be active
*/
updateAsync(): void
}
export interface WritableFeatureSource<T extends Feature = Feature<Geometry, OsmTags>> extends FeatureSource<T> {
export interface WritableFeatureSource<T extends Feature = Feature<Geometry, OsmTags>>
extends FeatureSource<T> {
features: UIEventSource<T[]>
}
/**
* A feature source which only contains features for the defined layer
*/
export interface FeatureSourceForLayer<T extends Feature = Feature<Geometry, OsmTags>> extends FeatureSource<T> {
export interface FeatureSourceForLayer<T extends Feature = Feature<Geometry, OsmTags>>
extends FeatureSource<T> {
readonly layer: FilteredLayer
}

View file

@ -11,7 +11,10 @@ import { UIEventSource } from "../UIEventSource"
* If this is the case, multiple objects with a different _matching_layer_id are generated.
* In any case, this featureSource marks the objects with _matching_layer_id
*/
export default class PerLayerFeatureSourceSplitter<T extends Feature, SRC extends FeatureSource<T>> {
export default class PerLayerFeatureSourceSplitter<
T extends Feature,
SRC extends FeatureSource<T>
> {
public readonly perLayer: ReadonlyMap<string, SRC>
constructor(
layers: FilteredLayer[],
@ -115,7 +118,7 @@ export default class PerLayerFeatureSourceSplitter<T extends Feature, SRC extend
})
}
public forEach(f: ((src: SRC) => void)) {
public forEach(f: (src: SRC) => void) {
for (const fs of this.perLayer.values()) {
f(fs)
}

View file

@ -5,7 +5,9 @@ import { Feature, Geometry } from "geojson"
import { GlobalFilter } from "../../../Models/GlobalFilter"
import { OsmTags } from "../../../Models/OsmFeature"
export default class FilteringFeatureSource<T extends Feature = Feature<Geometry, OsmTags>> implements FeatureSource<T> {
export default class FilteringFeatureSource<T extends Feature = Feature<Geometry, OsmTags>>
implements FeatureSource<T>
{
public readonly features: UIEventSource<T[]> = new UIEventSource([])
private readonly upstream: FeatureSource<T>
private readonly _fetchStore?: (id: string) => Store<Record<string, string>>

View file

@ -3,14 +3,12 @@ import { Feature } from "geojson"
import { Store, UIEventSource } from "../../UIEventSource"
export class IfVisibleFeatureSource<T extends Feature> implements FeatureSource<T> {
private readonly _features: UIEventSource<T[]> = new UIEventSource<T[]>([])
public readonly features: Store<T[]> = this._features
constructor(upstream: FeatureSource<T>, visible: Store<boolean>) {
let dirty = false
upstream.features.addCallbackAndRun(features => {
upstream.features.addCallbackAndRun((features) => {
if (!visible.data) {
dirty = true
this._features.set([])
@ -20,7 +18,7 @@ export class IfVisibleFeatureSource<T extends Feature> implements FeatureSource<
dirty = false
})
visible.addCallbackAndRun(isVisible => {
visible.addCallbackAndRun((isVisible) => {
if (isVisible && dirty) {
this._features.set(upstream.features.data)
dirty = false
@ -28,11 +26,6 @@ export class IfVisibleFeatureSource<T extends Feature> implements FeatureSource<
if (!visible) {
this._features.set([])
}
})
}
}

View file

@ -49,9 +49,11 @@ export class LastClickFeatureSource implements FeatureSource {
allPresets.push(html)
}
this.renderings = Lists.dedup(allPresets.map((uiElem) =>
Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML
))
this.renderings = Lists.dedup(
allPresets.map((uiElem) =>
Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML
)
)
this._features = new UIEventSource<Feature[]>([])
this.features = this._features

View file

@ -11,18 +11,11 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
public readonly y: number
public readonly z: number
private readonly _url: string
private readonly _features: UIEventSource<
GeojsonFeature<Geometry, OsmTags>[]
> = new UIEventSource<GeojsonFeature<Geometry, OsmTags>[]>([])
private readonly _features: UIEventSource<GeojsonFeature<Geometry, OsmTags>[]> =
new UIEventSource<GeojsonFeature<Geometry, OsmTags>[]>([])
private currentlyRunning: Promise<any>
constructor(
url: string,
x: number,
y: number,
z: number,
isActive?: Store<boolean>
) {
constructor(url: string, x: number, y: number, z: number, isActive?: Store<boolean>) {
this._url = url
this.x = x
this.y = y
@ -54,7 +47,9 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
return
}
const buffer = await result.arrayBuffer()
const features = <Feature<Geometry, OsmTags>[]>MvtToGeojson.fromBuffer(buffer, this.x, this.y, this.z)
const features = <Feature<Geometry, OsmTags>[]>(
MvtToGeojson.fromBuffer(buffer, this.x, this.y, this.z)
)
for (const feature of features) {
const properties = feature.properties
if (!properties["osm_type"]) {

View file

@ -10,7 +10,7 @@ import { BBox } from "../../BBox"
import { OsmTags } from "../../../Models/OsmFeature"
import { Lists } from "../../../Utils/Lists"
("use strict")
;("use strict")
/**
* A wrapper around the 'Overpass'-object.

View file

@ -4,7 +4,9 @@ import { FeatureSourceForLayer } from "../FeatureSource"
import { Feature, Geometry } from "geojson"
import { OsmTags } from "../../../Models/OsmFeature"
export default class SimpleFeatureSource<T extends Feature = Feature<Geometry, OsmTags>> implements FeatureSourceForLayer<T> {
export default class SimpleFeatureSource<T extends Feature = Feature<Geometry, OsmTags>>
implements FeatureSourceForLayer<T>
{
public readonly features: UIEventSource<T[]>
public readonly layer: FilteredLayer

View file

@ -19,13 +19,14 @@ export interface ClusteringOptions {
}
interface SummaryProperties {
id: string,
total: number,
id: string
total: number
tile_id: number
}
export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>> implements FeatureSource<T> {
export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>>
implements FeatureSource<T>
{
private readonly id: string
private readonly showSummaryAt: "tilecenter" | "average"
features: Store<T[]>
@ -37,52 +38,59 @@ export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>>
*
* We ignore the polygons, as polygons get smaller when zoomed out and thus don't clutter the map too much
*/
constructor(upstream: FeatureSource<T>,
currentZoomlevel: Store<number>,
id: string,
options?: ClusteringOptions) {
constructor(
upstream: FeatureSource<T>,
currentZoomlevel: Store<number>,
id: string,
options?: ClusteringOptions
) {
this.id = id
this.showSummaryAt = options?.showSummaryAt ?? "average"
const clusterCutoff = options?.dontClusterAboveZoom ?? 17
const doCluster = options?.dontClusterAboveZoom === undefined ? new ImmutableStore(true) : currentZoomlevel.map(zoom => zoom <= clusterCutoff)
const doCluster =
options?.dontClusterAboveZoom === undefined
? new ImmutableStore(true)
: currentZoomlevel.map((zoom) => zoom <= clusterCutoff)
const cutoff = options?.cutoff ?? 20
const summaryPoints = new UIEventSource<Feature<Point, SummaryProperties>[]>([])
currentZoomlevel = currentZoomlevel.stabilized(500).map(z => Math.floor(z))
this.features = (upstream.features.map(features => {
if (!doCluster.data) {
summaryPoints.set([])
return features
}
const z = currentZoomlevel.data
const perTile = GeoOperations.spreadIntoBboxes(features, z)
const resultingFeatures = []
const summary: Feature<Point, SummaryProperties>[] = []
for (const tileIndex of perTile.keys()) {
const tileFeatures: Feature<Point>[] = perTile.get(tileIndex)
if (tileFeatures.length > cutoff) {
summary.push(this.createSummaryFeature(tileFeatures, tileIndex))
} else {
resultingFeatures.push(...tileFeatures)
currentZoomlevel = currentZoomlevel.stabilized(500).map((z) => Math.floor(z))
this.features = upstream.features.map(
(features) => {
if (!doCluster.data) {
summaryPoints.set([])
return features
}
}
summaryPoints.set(summary)
return resultingFeatures
}, [doCluster, currentZoomlevel]))
const z = currentZoomlevel.data
const perTile = GeoOperations.spreadIntoBboxes(features, z)
const resultingFeatures = []
const summary: Feature<Point, SummaryProperties>[] = []
for (const tileIndex of perTile.keys()) {
const tileFeatures: Feature<Point>[] = perTile.get(tileIndex)
if (tileFeatures.length > cutoff) {
summary.push(this.createSummaryFeature(tileFeatures, tileIndex))
} else {
resultingFeatures.push(...tileFeatures)
}
}
summaryPoints.set(summary)
return resultingFeatures
},
[doCluster, currentZoomlevel]
)
ClusterGrouping.singleton.registerSource(summaryPoints)
}
private createSummaryFeature(features: Feature<Point>[], tileId: number): Feature<Point, SummaryProperties> {
private createSummaryFeature(
features: Feature<Point>[],
tileId: number
): Feature<Point, SummaryProperties> {
let lon: number
let lat: number
const [z, x, y] = Tiles.tile_from_index(tileId)
if (this.showSummaryAt === "tilecenter") {
[lon, lat] = Tiles.centerPointOf(z, x, y)
;[lon, lat] = Tiles.centerPointOf(z, x, y)
} else {
let lonSum = 0
let latSum = 0
@ -98,13 +106,13 @@ export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>>
type: "Feature",
geometry: {
type: "Point",
coordinates: [lon, lat]
coordinates: [lon, lat],
},
properties: {
id: "summary_" + this.id + "_" + tileId,
tile_id: tileId,
total: features.length
}
total: features.length,
},
}
}
}
@ -113,7 +121,8 @@ export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>>
* Groups multiple summaries together
*/
export class ClusterGrouping implements FeatureSource<Feature<Point, { total_metric: string }>> {
private readonly _features: UIEventSource<Feature<Point, { total_metric: string }>[]> = new UIEventSource([])
private readonly _features: UIEventSource<Feature<Point, { total_metric: string }>[]> =
new UIEventSource([])
public readonly features: Store<Feature<Point, { total_metric: string }>[]> = this._features
public static readonly singleton = new ClusterGrouping()
@ -121,14 +130,14 @@ export class ClusterGrouping implements FeatureSource<Feature<Point, { total_met
public readonly isDirty = new UIEventSource(false)
private constructor() {
this.isDirty.stabilized(200).addCallback(dirty => {
this.isDirty.stabilized(200).addCallback((dirty) => {
if (dirty) {
this.update()
}
})
}
private allSource: Store<Feature<Point, { total: number, tile_id: number }>[]>[] = []
private allSource: Store<Feature<Point, { total: number; tile_id: number }>[]>[] = []
private update() {
const countPerTile = new Map<number, number>()
@ -139,7 +148,7 @@ export class ClusterGrouping implements FeatureSource<Feature<Point, { total_met
countPerTile.set(id, count)
}
}
const features: Feature<Point, { total_metric: string, id: string }>[] = []
const features: Feature<Point, { total_metric: string; id: string }>[] = []
const now = new Date().getTime() + ""
for (const tileId of countPerTile.keys()) {
const coordinates = Tiles.centerPointOf(tileId)
@ -147,12 +156,12 @@ export class ClusterGrouping implements FeatureSource<Feature<Point, { total_met
type: "Feature",
properties: {
total_metric: "" + countPerTile.get(tileId),
id: "clustered_all_" + tileId + "_" + now // We add the date to force a fresh ID every time, this makes sure values are updated
id: "clustered_all_" + tileId + "_" + now, // We add the date to force a fresh ID every time, this makes sure values are updated
},
geometry: {
type: "Point",
coordinates
}
coordinates,
},
})
}
this._features.set(features)
@ -166,5 +175,4 @@ export class ClusterGrouping implements FeatureSource<Feature<Point, { total_met
this.update()
})
}
}

View file

@ -20,7 +20,7 @@ export class SummaryTileSourceRewriter implements FeatureSource {
public readonly totalNumberOfFeatures: Store<number> = this._totalNumberOfFeatures
constructor(
summarySource: SummaryTileSource,
filteredLayers: ReadonlyMap<string, FilteredLayer>,
filteredLayers: ReadonlyMap<string, FilteredLayer>
) {
this.filteredLayers = Array.from(filteredLayers.values()).filter(
(l) => !Constants.isPriviliged(l.layerDef)
@ -73,18 +73,20 @@ export class SummaryTileSource extends DynamicTileSource {
zoom: Store<number>
},
options?: {
isActive?: Store<boolean>,
isActive?: Store<boolean>
availableLayers?: Store<Set<string>>
}
) {
const zDiff = 2
const layersSummed = (options?.availableLayers??new ImmutableStore(undefined)).map(available => {
console.log("Determining 'layersSummed' with currently available:", available)
if(available === undefined){
return layers.join("+")
const layersSummed = (options?.availableLayers ?? new ImmutableStore(undefined)).map(
(available) => {
console.log("Determining 'layersSummed' with currently available:", available)
if (available === undefined) {
return layers.join("+")
}
return layers.filter((l) => available.has(l)).join("+")
}
return layers.filter(l => available.has(l)).join("+")
})
)
super(
zoomRounded,
0, // minzoom
@ -117,7 +119,7 @@ export class SummaryTileSource extends DynamicTileSource {
cacheserver: string,
layersSummed: string
): Store<Feature<Point>[]> {
if(layersSummed === ""){
if (layersSummed === "") {
return new ImmutableStore([])
}
const [z, x, y] = Tiles.tile_from_index(tileIndex)

View file

@ -10,13 +10,13 @@ import {
MultiPolygon,
Point,
Polygon,
Position
Position,
} from "geojson"
import { Tiles } from "../Models/TileRange"
import { Utils } from "../Utils"
import { Lists } from "../Utils/Lists"
("use strict")
;("use strict")
export class GeoOperations {
private static readonly _earthRadius: number = 6378137
@ -538,7 +538,10 @@ export class GeoOperations {
* @param features
* @param zoomlevel
*/
public static spreadIntoBboxes<T extends Feature = Feature>(features: T[], zoomlevel: number): Map<number, T[]> {
public static spreadIntoBboxes<T extends Feature = Feature>(
features: T[],
zoomlevel: number
): Map<number, T[]> {
const perBbox = new Map<number, T[]>()
const z = zoomlevel
for (const feature of features) {

View file

@ -102,7 +102,9 @@ export default class AllImageProviders {
Mapillary.singleton,
AllImageProviders.genericImageProvider,
]
const allPrefixes = Lists.dedup(prefixes ?? [].concat(...sources.map((s) => s.defaultKeyPrefixes)))
const allPrefixes = Lists.dedup(
prefixes ?? [].concat(...sources.map((s) => s.defaultKeyPrefixes))
)
for (const prefix of allPrefixes) {
for (const k in tags) {
const v = tags[k]
@ -148,7 +150,7 @@ export default class AllImageProviders {
allSources.push(singleSource)
}
const source = Stores.concat(allSources).map((result) => {
const all = result.flatMap(x => x)
const all = result.flatMap((x) => x)
return Utils.DedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id])
})
this._cachedImageStores[cachekey] = source

View file

@ -75,9 +75,11 @@ export class Mapillary extends ImageProvider {
pKey,
}
const baselink = `https://www.mapillary.com/app/?`
const paramsStr = Lists.noNull(Object.keys(params).map((k) =>
params[k] === undefined ? undefined : k + "=" + params[k]
))
const paramsStr = Lists.noNull(
Object.keys(params).map((k) =>
params[k] === undefined ? undefined : k + "=" + params[k]
)
)
return baselink + paramsStr.join("&")
}

View file

@ -5,7 +5,6 @@ import Constants from "../Models/Constants"
import { Store, UIEventSource } from "./UIEventSource"
import { Utils } from "../Utils"
export interface AreaDescription {
/**
* Thie filename at the host and in the indexedDb
@ -15,16 +14,16 @@ export interface AreaDescription {
/**
* Minzoom that is covered, inclusive
*/
minzoom: number,
minzoom: number
/**
* Maxzoom that is covered, inclusive
*/
maxzoom: number,
maxzoom: number
/**
* The x, y of the tile that is covered (at minzoom)
*/
x: number,
y: number,
x: number
y: number
/**
* ISO-datestring of when the data was processed
@ -35,7 +34,6 @@ export interface AreaDescription {
* Blob.size
*/
size?: number
}
class TypedIdb<T> {
@ -52,7 +50,6 @@ class TypedIdb<T> {
return Promise.resolve(undefined)
}
return new Promise((resolve, reject) => {
const request: IDBOpenDBRequest = indexedDB.open(name)
request.onerror = (event) => {
console.error("Could not open the Database: ", event)
@ -68,7 +65,6 @@ class TypedIdb<T> {
})
}
async set(key: string, value: T): Promise<void> {
if (Utils.runningFromConsole) {
return Promise.resolve()
@ -134,8 +130,6 @@ class TypedIdb<T> {
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}
}
@ -153,7 +147,7 @@ class BlobSource implements Source {
async getBytes(offset: number, length: number): Promise<RangeResponse> {
const sliced = this._blob.slice(offset, offset + length)
return {
data: await sliced.arrayBuffer()
data: await sliced.arrayBuffer(),
}
}
@ -168,9 +162,7 @@ class BlobSource implements Source {
async getDataVersion(): Promise<string> {
const meta: Record<string, string> = <any>await this.pmtiles.getMetadata()
return meta["planetiler:osm:osmosisreplicationtime"]
}
}
export class OfflineBasemapManager {
@ -184,7 +176,7 @@ export class OfflineBasemapManager {
0: 4,
5: 7,
8: 9,
10: undefined
10: undefined,
} as const
private readonly blobs: TypedIdb<any>
@ -192,7 +184,9 @@ export class OfflineBasemapManager {
public _installedAreas: UIEventSource<AreaDescription[]> = new UIEventSource([])
public installedAreas: Store<ReadonlyArray<Readonly<AreaDescription>>> = this._installedAreas
private readonly _installing: UIEventSource<Map<string, Promise<boolean>>> = new UIEventSource(new Map())
private readonly _installing: UIEventSource<Map<string, Promise<boolean>>> = new UIEventSource(
new Map()
)
public readonly installing: Store<ReadonlyMap<string, object>> = this._installing
public static singleton = new OfflineBasemapManager(Constants.pmtiles_host)
@ -224,18 +218,25 @@ export class OfflineBasemapManager {
return this.installedAreas.data
}
public isInstalled(toCompare: { z?: number, minzoom?: number, x: number, y: number }): boolean {
return this.installedAreas.data.some(area => area.x === toCompare.x && area.y === toCompare.y && (toCompare.minzoom ?? toCompare.z) === area.minzoom)
public isInstalled(toCompare: { z?: number; minzoom?: number; x: number; y: number }): boolean {
return this.installedAreas.data.some(
(area) =>
area.x === toCompare.x &&
area.y === toCompare.y &&
(toCompare.minzoom ?? toCompare.z) === area.minzoom
)
}
/**
* Returns all AreaDescriptions needed for the specified tile. Most specific zoom level last.
* Already installed area descriptions are _not_ returned
* @param tile
*/
public* getInstallCandidates(tile: { z: number, x: number, y: number }): Generator<AreaDescription, void, unknown> {
public *getInstallCandidates(tile: {
z: number
x: number
y: number
}): Generator<AreaDescription, void, unknown> {
for (const k in OfflineBasemapManager.zoomelevels) {
const z = Number(k)
@ -250,12 +251,11 @@ export class OfflineBasemapManager {
name: `${z}-${x}-${y}.pmtiles`,
minzoom: z,
maxzoom: OfflineBasemapManager.zoomelevels[z] ?? 15,
x, y
x,
y,
}
}
}
}
/**
@ -276,7 +276,10 @@ export class OfflineBasemapManager {
}
const blob = await response.blob()
await this.blobs.set(areaDescription.name, blob)
areaDescription.dataVersion = await new BlobSource(areaDescription.name, blob).getDataVersion()
areaDescription.dataVersion = await new BlobSource(
areaDescription.name,
blob
).getDataVersion()
areaDescription.size = blob.size
await this.meta.set(areaDescription.name, areaDescription)
await this.updateCachedMeta()
@ -289,17 +292,20 @@ export class OfflineBasemapManager {
* @see GeneratePmTilesExtractionScript
*/
public static getAreaDescriptionForMapcomplete(name: string): AreaDescription {
if (!name.endsWith(".pmtiles")) {
throw "Invalid filename, should end with .pmtiles"
}
const [z, x, y] = name.substring(0, name.length - ".pmtiles".length).split("-").map(Number)
const [z, x, y] = name
.substring(0, name.length - ".pmtiles".length)
.split("-")
.map(Number)
const maxzooms: Record<number, number | undefined> = this.zoomelevels
return {
name,
minzoom: z,
maxzoom: maxzooms[z] ?? 15,
x, y
x,
y,
}
}
@ -342,14 +348,16 @@ export class OfflineBasemapManager {
if (alreadyInstalling) {
return alreadyInstalling
}
const promise = this.installArea(candidate).catch(e => {
console.error("Could not install basemap archive", candidate.name, "due to", e)
const promise = this.installArea(candidate)
.catch((e) => {
console.error("Could not install basemap archive", candidate.name, "due to", e)
return false
}).finally(() => {
this._installing.data.delete(candidate.name)
this._installing.ping()
})
return false
})
.finally(() => {
this._installing.data.delete(candidate.name)
this._installing.ping()
})
this._installing.data.set(candidate.name, promise)
this._installing.ping()
return promise
@ -359,7 +367,7 @@ export class OfflineBasemapManager {
* Attempts to install all required areas for the given location
* @param tile
*/
public async autoInstall(tile: { z: number, x: number, y: number }) {
public async autoInstall(tile: { z: number; x: number; y: number }) {
const candidates = this.getInstallCandidates(tile)
for (const candidate of candidates) {
await this.attemptInstall(candidate)
@ -391,12 +399,9 @@ export class OfflineBasemapManager {
return undefined
}
console.log("Served tile", { z, x, y }, "from installed archive")
return new Response(
tileData.data,
{
headers: { "Content-Type": "application/x.protobuf" }
}
)
return new Response(tileData.data, {
headers: { "Content-Type": "application/x.protobuf" },
})
}
deleteArea(description: AreaDescription): Promise<ReadonlyArray<Readonly<AreaDescription>>> {
@ -405,12 +410,9 @@ export class OfflineBasemapManager {
return this.updateCachedMeta()
}
private async fallback(params: RequestParameters,
abortController: AbortController) {
private async fallback(params: RequestParameters, abortController: AbortController) {
params.url = params.url.substr("pmtilesoffl://".length)
const response = await fetch(
new Request(params.url, params)
, abortController)
const response = await fetch(new Request(params.url, params), abortController)
if (!response.ok) {
throw new Error("Could not fetch " + params.url + "; status code is" + response.status)
}
@ -420,11 +422,17 @@ export class OfflineBasemapManager {
public async tilev4(
params: RequestParameters,
abortController: AbortController
): Promise<{ data: unknown } | { data: { tiles: string[], minzoom: number, maxzoom: number, bounds: number[] } } | {
data: Uint8Array,
cacheControl: string,
expires: string
} | { data: Uint8Array } | { data: null }> {
): Promise<
| { data: unknown }
| { data: { tiles: string[]; minzoom: number; maxzoom: number; bounds: number[] } }
| {
data: Uint8Array
cacheControl: string
expires: string
}
| { data: Uint8Array }
| { data: null }
> {
if (params.type === "arrayBuffer") {
const re = new RegExp(/(\d+)\/(\d+)\/(\d+).(mvt|pbf)/)
const result = params.url.match(re)
@ -438,9 +446,7 @@ export class OfflineBasemapManager {
if (r?.ok) {
return { data: await r.arrayBuffer() }
}
}
return await this.fallback(params, abortController)
}
}

View file

@ -51,11 +51,11 @@ export default class DeleteAction extends OsmChangeAction {
} else {
this._softDeletionTags = new And(
Lists.noNull([
softDeletionTags,
new Tag(
"fixme",
`A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})`
),
softDeletionTags,
new Tag(
"fixme",
`A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})`
),
])
)
}

View file

@ -318,7 +318,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr
candidate = undefined
moveDistance = Infinity
distances.forEach((distances, nodeId) => {
const minDist = Math.min(...(Lists.noNull(distances)))
const minDist = Math.min(...Lists.noNull(distances))
if (moveDistance > minDist) {
// We have found a candidate to move
candidate = nodeId

View file

@ -718,9 +718,11 @@ export class Changes {
* We _do not_ pass in the Changes object itself - we want the data from OSM directly in order to apply the changes
*/
const downloader = new OsmObjectDownloader(this.backend, undefined)
const osmObjects = Lists.noNull(await Promise.all<{ id: string; osmObj: OsmObject | "deleted" }>(
neededIds.map((id) => this.getOsmObject(id, downloader))
))
const osmObjects = Lists.noNull(
await Promise.all<{ id: string; osmObj: OsmObject | "deleted" }>(
neededIds.map((id) => this.getOsmObject(id, downloader))
)
)
// Drop changes to deleted items
for (const { osmObj, id } of osmObjects) {
@ -815,21 +817,23 @@ export class Changes {
}
}
const perBinMessage = Lists.noNull(perBinCount.map((count, i) => {
if (count === 0) {
return undefined
}
const maxD = maxDistances[i]
let key = `change_within_${maxD}m`
if (maxD === Number.MAX_VALUE) {
key = `change_over_${maxDistances[i - 1]}m`
}
return {
key,
value: count,
aggregate: true,
}
}))
const perBinMessage = Lists.noNull(
perBinCount.map((count, i) => {
if (count === 0) {
return undefined
}
const maxD = maxDistances[i]
let key = `change_within_${maxD}m`
if (maxD === Number.MAX_VALUE) {
key = `change_over_${maxDistances[i - 1]}m`
}
return {
key,
value: count,
aggregate: true,
}
})
)
// This method is only called with changedescriptions for this theme
const theme = pending[0].meta.theme

View file

@ -2,24 +2,23 @@
* Various tools for the OSM wiki
*/
export default class OsmWiki {
/**
* Create a link to the wiki for the given key and value (optional)
* @param key
* @param value
*/
public static constructLink(key: string, value?: string) : string{
public static constructLink(key: string, value?: string): string {
if (value !== undefined) {
return `https://wiki.openstreetmap.org/wiki/Tag:${key}%3D${value}`
}
return "https://wiki.openstreetmap.org/wiki/Key:" + key
}
public static constructLinkMd(key: string, value?: string) : string {
public static constructLinkMd(key: string, value?: string): string {
const link = this.constructLink(key, value)
let displayed = key
if(value){
displayed += "="+value
if (value) {
displayed += "=" + value
}
return `[${displayed}](${link})`
}

View file

@ -9,7 +9,6 @@ export default class CombinedSearcher implements GeocodingProvider {
private _providersWithSuggest: ReadonlyArray<GeocodingProvider>
public readonly needsInternet
/**
* Merges the various providers together; ignores errors.
* IF all providers fail, no errors will be given
@ -18,7 +17,7 @@ export default class CombinedSearcher implements GeocodingProvider {
constructor(...providers: ReadonlyArray<GeocodingProvider>) {
this._providers = Lists.noNull(providers)
this._providersWithSuggest = this._providers.filter((pr) => pr.suggest !== undefined)
this.needsInternet = this._providers.some(p => p.needsInternet)
this.needsInternet = this._providers.some((p) => p.needsInternet)
}
/**
@ -54,10 +53,15 @@ export default class CombinedSearcher implements GeocodingProvider {
return CombinedSearcher.merge(results)
}
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]}> {
suggest(query: string, options?: GeocodingOptions): Store<{ success: GeocodeResult[] }> {
const concatted = Stores.concat(
this._providersWithSuggest.map((pr) => <Store<GeocodeResult[]>> pr.suggest(query, options).map(result => result["success"] ?? []))
);
return concatted.map(gcrss => ({success: CombinedSearcher.merge(gcrss) }))
this._providersWithSuggest.map(
(pr) =>
<Store<GeocodeResult[]>>(
pr.suggest(query, options).map((result) => result["success"] ?? [])
)
)
)
return concatted.map((gcrss) => ({ success: CombinedSearcher.merge(gcrss) }))
}
}

View file

@ -77,7 +77,9 @@ export default class CoordinateSearch implements GeocodingProvider {
(m) => CoordinateSearch.asResult(m[2], m[1], "latlon")
)
const matchesLonLat = Lists.noNull(CoordinateSearch.lonLatRegexes.map((r) => query.match(r))).map((m) => CoordinateSearch.asResult(m[1], m[2], "lonlat"))
const matchesLonLat = Lists.noNull(
CoordinateSearch.lonLatRegexes.map((r) => query.match(r))
).map((m) => CoordinateSearch.asResult(m[1], m[2], "lonlat"))
const init = matches.concat(matchesLonLat)
if (init.length > 0) {
return init
@ -119,8 +121,8 @@ export default class CoordinateSearch implements GeocodingProvider {
}
}
suggest(query: string): Store<{success: GeocodeResult[]}> {
return new ImmutableStore({success: this.directSearch(query)})
suggest(query: string): Store<{ success: GeocodeResult[] }> {
return new ImmutableStore({ success: this.directSearch(query) })
}
async search(query: string): Promise<GeocodeResult[]> {

View file

@ -58,7 +58,10 @@ export default interface GeocodingProvider {
*/
search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]>
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error :any}>
suggest(
query: string,
options?: GeocodingOptions
): Store<{ success: GeocodeResult[] } | { error: any }>
}
export type ReverseGeocodingResult = Feature<
@ -90,7 +93,7 @@ export class GeocodingUtils {
// We are resetting the layeroverview; trying to parse is useless
return undefined
}
return new LayerConfig(<LayerConfigJson><any> search, "search")
return new LayerConfig(<LayerConfigJson>(<any>search), "search")
}
public static categoryToZoomLevel: Record<GeocodingCategory, number> = {

View file

@ -45,12 +45,12 @@ export default class LocalElementSearch implements GeocodingProvider {
for (const feature of features) {
const props = feature.properties
const searchTerms: string[] = Lists.noNull([
props.name,
props.alt_name,
props.local_name,
props["addr:street"] && props["addr:number"]
? props["addr:street"] + props["addr:number"]
: undefined,
props.name,
props.alt_name,
props.local_name,
props["addr:street"] && props["addr:number"]
? props["addr:street"] + props["addr:number"]
: undefined,
])
let levehnsteinD: number
@ -144,7 +144,7 @@ export default class LocalElementSearch implements GeocodingProvider {
})
}
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]}> {
return this.searchEntries(query, options, true).mapD(r => ({success:r}))
suggest(query: string, options?: GeocodingOptions): Store<{ success: GeocodeResult[] }> {
return this.searchEntries(query, options, true).mapD((r) => ({ success: r }))
}
}

View file

@ -40,7 +40,10 @@ export class NominatimGeocoding implements GeocodingProvider {
return Utils.downloadJson(url)
}
suggest(query: string, options?: GeocodingOptions): Store<{ success: GeocodeResult[] } | { error: any }> {
suggest(
query: string,
options?: GeocodingOptions
): Store<{ success: GeocodeResult[] } | { error: any }> {
return UIEventSource.fromPromiseWithErr(this.search(query, options))
}
}

View file

@ -93,7 +93,10 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
return [await this.getInfoAbout(id)]
}
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error: any}> {
suggest(
query: string,
options?: GeocodingOptions
): Store<{ success: GeocodeResult[] } | { error: any }> {
return UIEventSource.fromPromiseWithErr(this.search(query, options))
}
}

View file

@ -5,7 +5,7 @@ import GeocodingProvider, {
GeocodingOptions,
GeocodingUtils,
ReverseGeocodingProvider,
ReverseGeocodingResult
ReverseGeocodingResult,
} from "./GeocodingProvider"
import { Utils } from "../../Utils"
import { Feature, FeatureCollection } from "geojson"
@ -26,16 +26,12 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
private readonly ignoreBounds: boolean
private readonly searchLimit: number = 1
constructor(
ignoreBounds: boolean = false,
searchLimit: number = 1,
endpoint?: string
) {
constructor(ignoreBounds: boolean = false, searchLimit: number = 1, endpoint?: string) {
this.ignoreBounds = ignoreBounds
this.searchLimit = searchLimit
this._endpoint = endpoint ?? Constants.photonEndpoint ?? "https://photon.komoot.io/"
if(this.ignoreBounds){
if (this.ignoreBounds) {
this.name += " (global)"
}
}
@ -71,7 +67,10 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
return `&lang=${language}`
}
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error: any}> {
suggest(
query: string,
options?: GeocodingOptions
): Store<{ success: GeocodeResult[] } | { error: any }> {
return UIEventSource.fromPromiseWithErr(this.search(query, options))
}

View file

@ -107,7 +107,7 @@ export class GeoLocationState {
this.requestPermission()
}
const hasLocation: Store<boolean> = this.currentGPSLocation.map(l => l !== undefined)
const hasLocation: Store<boolean> = this.currentGPSLocation.map((l) => l !== undefined)
this.gpsStateExplanation = this.gpsAvailable.map(
(available) => {
if (hasLocation.data) {

View file

@ -64,53 +64,70 @@ export default class SearchState {
return undefined
}
return this.locationSearchers
.filter(ls => !ls.needsInternet || IsOnline.isOnline.data)
.filter((ls) => !ls.needsInternet || IsOnline.isOnline.data)
.map((ls) => ({
source: ls,
results: ls.suggest(search, { bbox: bounds.data }),
}))
source: ls,
results: ls.suggest(search, { bbox: bounds.data }),
}))
},
[bounds]
)
const suggestionsList = suggestionsListWithSource
.mapD(list => list.map(sugg => sugg.results))
const suggestionsList = suggestionsListWithSource.mapD((list) =>
list.map((sugg) => sugg.results)
)
const isRunningPerEngine: Store<Store<GeocodingProvider>[]> =
suggestionsListWithSource.mapD(
allProviders => allProviders.map(provider =>
provider.results.map(result => {
suggestionsListWithSource.mapD((allProviders) =>
allProviders.map((provider) =>
provider.results.map((result) => {
if (result === undefined) {
return provider.source
} else {
return undefined
}
})))
this.runningEngines = isRunningPerEngine.bindD(
listOfSources => Stores.concat(listOfSources).mapD(list => Lists.noNull(list)))
})
)
)
this.runningEngines = isRunningPerEngine.bindD((listOfSources) =>
Stores.concat(listOfSources).mapD((list) => Lists.noNull(list))
)
this.failedEngines = suggestionsListWithSource
.bindD((allProviders: {
source: GeocodingProvider;
results: Store<{ success: GeocodeResult[] } | { error: any }>
}[]) => Stores.concat(
allProviders.map(providerAndResult =>
<Store<{ source: GeocodingProvider, error: any }[]>>providerAndResult.results.map(result => {
let error = result?.["error"]
if (error) {
return [{
source: providerAndResult.source, error,
}]
} else {
return []
}
}),
))).map(list => Lists.noNull(list?.flatMap(x => x) ?? []))
.bindD(
(
allProviders: {
source: GeocodingProvider
results: Store<{ success: GeocodeResult[] } | { error: any }>
}[]
) =>
Stores.concat(
allProviders.map(
(providerAndResult) =>
<Store<{ source: GeocodingProvider; error: any }[]>>(
providerAndResult.results.map((result) => {
let error = result?.["error"]
if (error) {
return [
{
source: providerAndResult.source,
error,
},
]
} else {
return []
}
})
)
)
)
)
.map((list) => Lists.noNull(list?.flatMap((x) => x) ?? []))
this.suggestionsSearchRunning = this.runningEngines.map(running => running?.length > 0)
this.suggestionsSearchRunning = this.runningEngines.map((running) => running?.length > 0)
this.suggestions = suggestionsList.bindD((suggestions) =>
Stores.concat(suggestions.map(sugg => sugg.map(maybe => maybe?.["success"])))
.map((suggestions: GeocodeResult[][]) => CombinedSearcher.merge(suggestions))
Stores.concat(suggestions.map((sugg) => sugg.map((maybe) => maybe?.["success"]))).map(
(suggestions: GeocodeResult[][]) => CombinedSearcher.merge(suggestions)
)
)
const themeSearch = ThemeSearchIndex.fromState(state)

View file

@ -56,12 +56,12 @@ class RoundRobinStore<T> {
*/
public add(t: T) {
const i = this._index.data ?? 0
if(isNaN(Number(i))){
if (isNaN(Number(i))) {
this._index.set(0)
this.add(t)
return
}
this._index.set((Math.max(i,0) + 1) % this._maxCount)
this._index.set((Math.max(i, 0) + 1) % this._maxCount)
this._store.data[i] = t
this._store.ping()
}
@ -91,10 +91,12 @@ export class OptionallySyncedHistory<T extends object | string> {
defaultValue: "sync",
})
this.syncedBackingStore = UIEventSource.concat(Utils.timesT(maxHistory, (i) => {
const pref = osmconnection.getPreference(key + "-hist-" + i + "-")
return UIEventSource.asObject<T>(pref, undefined)
}))
this.syncedBackingStore = UIEventSource.concat(
Utils.timesT(maxHistory, (i) => {
const pref = osmconnection.getPreference(key + "-hist-" + i + "-")
return UIEventSource.asObject<T>(pref, undefined)
})
)
const ringIndex = UIEventSource.asInt(
osmconnection.getPreference(key + "-hist-round-robin", {
@ -374,7 +376,7 @@ export default class UserRelatedState {
private static initUserSettingsState(): LayerConfig {
try {
return new LayerConfig(<LayerConfigJson><any>usersettings, "userinformationpanel")
return new LayerConfig(<LayerConfigJson>(<any>usersettings), "userinformationpanel")
} catch (e) {
return undefined
}
@ -557,25 +559,27 @@ export default class UserRelatedState {
const untranslated = missing.untranslated.get(language) ?? []
const hasMissingTheme = untranslated.some((k) => k.startsWith("themes:"))
const missingLayers = Lists.dedup(untranslated
.filter((k) => k.startsWith("layers:"))
.map((k) => k.slice("layers:".length).split(".")[0]))
const missingLayers = Lists.dedup(
untranslated
.filter((k) => k.startsWith("layers:"))
.map((k) => k.slice("layers:".length).split(".")[0])
)
const zenLinks: { link: string; id: string }[] = Lists.noNull([
hasMissingTheme
? {
id: "theme:" + layout.id,
link: Translations.hrefToWeblateZen(
language,
"themes",
layout.id
),
}
: undefined,
...missingLayers.map((id) => ({
id: "layer:" + id,
link: Translations.hrefToWeblateZen(language, "layers", id),
})),
hasMissingTheme
? {
id: "theme:" + layout.id,
link: Translations.hrefToWeblateZen(
language,
"themes",
layout.id
),
}
: undefined,
...missingLayers.map((id) => ({
id: "layer:" + id,
link: Translations.hrefToWeblateZen(language, "layers", id),
})),
])
const untranslated_count = untranslated.length
amendedPrefs.data["_translation_total"] = "" + total
@ -674,18 +678,17 @@ export default class UserRelatedState {
amendedPrefs.ping()
})
amendedPrefs.data["___device_pixel_ratio"] = ""+window.devicePixelRatio
amendedPrefs.data["___device_pixel_ratio"] = "" + window.devicePixelRatio
AndroidPolyfill.getInsetSizes().top.addCallbackAndRun(topInset =>
amendedPrefs.data["___device_inset_top"] = ""+topInset
AndroidPolyfill.getInsetSizes().top.addCallbackAndRun(
(topInset) => (amendedPrefs.data["___device_inset_top"] = "" + topInset)
)
AndroidPolyfill.getInsetSizes().bottom.addCallbackAndRun(topInset =>
amendedPrefs.data["___device_inset_bottom"] = ""+topInset
AndroidPolyfill.getInsetSizes().bottom.addCallbackAndRun(
(topInset) => (amendedPrefs.data["___device_inset_bottom"] = "" + topInset)
)
AndroidPolyfill.inAndroid.addCallbackAndRun(isAndroid => {
amendedPrefs.data["___device_is_android"] = ""+isAndroid
AndroidPolyfill.inAndroid.addCallbackAndRun((isAndroid) => {
amendedPrefs.data["___device_is_android"] = "" + isAndroid
})
return amendedPrefs

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

@ -114,7 +114,7 @@ export class Tag extends TagsFilter {
return "<span class='line-through'>" + this.key + "</span>"
}
if (linkToWiki) {
const hrefK = OsmWiki.constructLink(this.key)
const hrefK = OsmWiki.constructLink(this.key)
const hrefKV = OsmWiki.constructLink(this.key, this.value)
return (
`<a href='${hrefK}' target='_blank'>${this.key}</a>` +

View file

@ -920,10 +920,10 @@ export class TagUtils {
public static GetPopularity(tag: TagsFilter): number | undefined {
if (tag instanceof And) {
return Math.min(...(Lists.noNull(tag.and.map((t) => TagUtils.GetPopularity(t))))) - 1
return Math.min(...Lists.noNull(tag.and.map((t) => TagUtils.GetPopularity(t)))) - 1
}
if (tag instanceof Or) {
return Math.max(...(Lists.noNull(tag.or.map((t) => TagUtils.GetPopularity(t))))) + 1
return Math.max(...Lists.noNull(tag.or.map((t) => TagUtils.GetPopularity(t)))) + 1
}
if (tag instanceof Tag) {
return TagUtils.GetCount(tag.key, tag.value)

View file

@ -23,12 +23,10 @@ export class Stores {
return source
}
public static concat<T>(stores: ReadonlyArray<Store<T | undefined>>): Store<(T | undefined)[]> ;
public static concat<T>(stores: ReadonlyArray<Store<T>>): Store<T[]> ;
public static concat<T>(stores: ReadonlyArray<Store<T | undefined>>): Store<(T | undefined)[]>
public static concat<T>(stores: ReadonlyArray<Store<T>>): Store<T[]>
public static concat<T>(stores: ReadonlyArray<Store<T | undefined>>): Store<(T | undefined)[]> {
const newStore = new UIEventSource<(T | undefined)[]>(
stores.map(store => store?.data),
)
const newStore = new UIEventSource<(T | undefined)[]>(stores.map((store) => store?.data))
function update() {
if (newStore._callbacks.isDestroyed) {
@ -118,22 +116,22 @@ export abstract class Store<T> implements Readable<T> {
abstract map<J>(
f: (t: T) => J,
extraStoresToWatch: Store<unknown>[],
callbackDestroyFunction?: (f: () => void) => void,
callbackDestroyFunction?: (f: () => void) => void
): Store<J>
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraStoresToWatch?: Store<unknown>[],
callbackDestroyFunction?: (f: () => void) => void,
callbackDestroyFunction?: (f: () => void) => void
): Store<J>
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
callbackDestroyFunction?: (f: () => void) => void,
callbackDestroyFunction?: (f: () => void) => void
): Store<J>
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraStoresToWatch?: Store<unknown>[] | ((f: () => void) => void),
callbackDestroyFunction?: (f: () => void) => void,
callbackDestroyFunction?: (f: () => void) => void
): Store<J> {
return this.map(
(t) => {
@ -146,7 +144,8 @@ export abstract class Store<T> implements Readable<T> {
return f(<Exclude<T, undefined | null>>t)
},
typeof extraStoresToWatch === "function" ? [] : extraStoresToWatch,
callbackDestroyFunction ?? (typeof extraStoresToWatch === "function" ? extraStoresToWatch : undefined),
callbackDestroyFunction ??
(typeof extraStoresToWatch === "function" ? extraStoresToWatch : undefined)
)
}
@ -219,9 +218,16 @@ export abstract class Store<T> implements Readable<T> {
* src.setData(0)
* lastValue // => "def"
*/
public bind<X>(f: (t: T) => Store<X>, extraSources?: Store<unknown>[] | ((f: () => void) => void), onDestroy?: (f : () => void) => void): Store<X> {
const mapped = this.map(f, typeof extraSources === "function" ? undefined : extraSources,
onDestroy ?? (typeof extraSources === "function" ? extraSources : undefined))
public bind<X>(
f: (t: T) => Store<X>,
extraSources?: Store<unknown>[] | ((f: () => void) => void),
onDestroy?: (f: () => void) => void
): Store<X> {
const mapped = this.map(
f,
typeof extraSources === "function" ? undefined : extraSources,
onDestroy ?? (typeof extraSources === "function" ? extraSources : undefined)
)
const sink = new UIEventSource<X>(undefined)
const seenEventSources = new Set<Store<X>>()
mapped.addCallbackAndRun((newEventSource) => {
@ -255,17 +261,21 @@ export abstract class Store<T> implements Readable<T> {
public bindD<X>(
f: (t: Exclude<T, undefined | null>) => Store<X>,
extraSources?: Store<unknown>[],
onDestroy?: ((f: () => void) => void)
onDestroy?: (f: () => void) => void
): Store<X> {
return this.bind((t) => {
if (t === null) {
return null
}
if (t === undefined) {
return undefined
}
return f(<Exclude<T, undefined | null>>t)
}, extraSources, onDestroy)
return this.bind(
(t) => {
if (t === null) {
return null
}
if (t === undefined) {
return undefined
}
return f(<Exclude<T, undefined | null>>t)
},
extraSources,
onDestroy
)
}
public stabilized(millisToStabilize): Store<T> {
@ -349,8 +359,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
@ -378,8 +387,8 @@ export class ImmutableStore<T> extends Store<T> {
map<J>(
f: (t: T) => J,
extraStores: Store<any>[] | ((f: () => void) => void)= undefined,
ondestroyCallback?: (f: () => void) => void,
extraStores: Store<any>[] | ((f: () => void) => void) = undefined,
ondestroyCallback?: (f: () => void) => void
): ImmutableStore<J> {
if (extraStores?.length > 0) {
return new MappedStore(this, f, extraStores, undefined, f(this.data), ondestroyCallback)
@ -489,7 +498,7 @@ class MappedStore<TIn, T> extends Store<T> {
extraStores: Store<unknown>[] | ((t: () => void) => void),
upstreamListenerHandler: ListenerTracker<TIn> | undefined,
initialState: T,
onDestroy?: (f: () => void) => void,
onDestroy?: (f: () => void) => void
) {
super()
this._upstream = upstream
@ -533,11 +542,10 @@ class MappedStore<TIn, T> extends Store<T> {
map<J>(
f: (t: T) => J,
extraStores: Store<unknown>[] | ((f: () => void) => void) = undefined,
ondestroyCallback?: (f: () => void) => void,
ondestroyCallback?: (f: () => void) => void
): Store<J> {
let stores: Store<unknown>[] = undefined
if (typeof extraStores !== "function") {
if (extraStores?.length > 0 || this._extraStores?.length > 0) {
stores = []
}
@ -559,7 +567,7 @@ class MappedStore<TIn, T> extends Store<T> {
stores,
this._callbacks,
f(this.data),
ondestroyCallback,
ondestroyCallback
)
}
@ -617,7 +625,7 @@ class MappedStore<TIn, T> extends Store<T> {
private registerCallbacksToUpstream() {
this._unregisterFromUpstream = this._upstream.addCallback(() => this.update())
this._unregisterFromExtraStores = this._extraStores?.map((store) =>
store?.addCallback(() => this.update()),
store?.addCallback(() => this.update())
)
this._callbacksAreRegistered = true
}
@ -638,8 +646,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>()
@ -654,7 +661,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)
@ -683,7 +690,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))
@ -704,7 +711,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
@ -737,7 +744,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return undefined
}
return "" + fl
},
}
)
}
@ -768,7 +775,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return undefined
}
return "" + fl
},
}
)
}
@ -776,13 +783,13 @@ 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 | string>(
stringUIEventSource: UIEventSource<string>,
defaultV: T,
defaultV: T
): UIEventSource<T> {
return stringUIEventSource.sync(
(str) => {
@ -798,13 +805,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
"due to",
e,
"; the underlying data store has tag",
stringUIEventSource.tag,
stringUIEventSource.tag
)
return defaultV
}
},
[],
(b) => JSON.stringify(b) ?? "",
(b) => JSON.stringify(b) ?? ""
)
}
@ -894,16 +901,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
public map<J>(
f: (t: T) => J,
extraSources?: Store<unknown>[],
onDestroy?: (f: () => void) => void,
)
public map<J>(
f: (t: T) => J,
onDestroy: (f: () => void) => void,
onDestroy?: (f: () => void) => void
)
public map<J>(f: (t: T) => J, onDestroy: (f: () => void) => void)
public map<J>(
f: (t: T) => J,
extraSources: Store<unknown>[] | ((f: () => void) => void),
onDestroy?: (f: () => void) => void,
onDestroy?: (f: () => void) => void
): Store<J> {
return new MappedStore(this, f, extraSources, this._callbacks, f(this.data), onDestroy)
}
@ -915,7 +919,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraSources?: Store<unknown>[] | ((f: () => void) => void),
callbackDestroyFunction?: (f: () => void) => void,
callbackDestroyFunction?: (f: () => void) => void
): Store<J | undefined> {
return new MappedStore(
this,
@ -933,7 +937,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
this.data === undefined || this.data === null
? <undefined | null>this.data
: f(<Exclude<T, undefined | null>>this.data),
callbackDestroyFunction,
callbackDestroyFunction
)
}
@ -953,7 +957,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
f: (t: T) => J,
extraSources: Store<unknown>[],
g: (j: J, t: T) => T,
allowUnregister = false,
allowUnregister = false
): UIEventSource<J> {
const stack = new Error().stack.split("\n")
const callee = stack[1]
@ -1002,11 +1006,15 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
this.setData(f(this.data))
}
public static concat<T>(stores: ReadonlyArray<UIEventSource<T | undefined>>): UIEventSource<(T | undefined)[]> ;
public static concat<T>(stores: ReadonlyArray<UIEventSource<T>>): UIEventSource<T[]> ;
public static concat<T>(stores: ReadonlyArray<UIEventSource<T | undefined>>): UIEventSource<(T | undefined)[]> {
public static concat<T>(
stores: ReadonlyArray<UIEventSource<T | undefined>>
): UIEventSource<(T | undefined)[]>
public static concat<T>(stores: ReadonlyArray<UIEventSource<T>>): UIEventSource<T[]>
public static concat<T>(
stores: ReadonlyArray<UIEventSource<T | undefined>>
): UIEventSource<(T | undefined)[]> {
const newStore = <UIEventSource<T[]>>Stores.concat(stores)
newStore.addCallbackD(list => {
newStore.addCallbackD((list) => {
for (let i = 0; i < list.length; i++) {
stores[i]?.setData(list[i])
}

View file

@ -97,9 +97,9 @@ export class AndroidPolyfill {
/**
* Gets how much padding we should add on top and at the bottom; in pixels
*/
private static insets: { top: Store<number>, bottom: Store<number> } = undefined
private static insets: { top: Store<number>; bottom: Store<number> } = undefined
public static getInsetSizes(): Readonly<{ top: Store<number>, bottom: Store<number> }> {
public static getInsetSizes(): Readonly<{ top: Store<number>; bottom: Store<number> }> {
if (AndroidPolyfill.insets) {
return AndroidPolyfill.insets
}
@ -110,14 +110,14 @@ export class AndroidPolyfill {
AndroidPolyfill.insets = insets
console.log("Web: requesting inset sizes")
DatabridgePluginSingleton.request<{ top: number, bottom: number }>({
DatabridgePluginSingleton.request<{ top: number; bottom: number }>({
key: "insets",
}).then((result) => {
if(!result){
if (!result) {
return
}
let v = result.value
if(typeof v === "string"){
if (typeof v === "string") {
v = JSON.parse(v)
}
console.log("Got inset sizes:", result)

View file

@ -3,11 +3,10 @@ import { Utils } from "../../Utils"
export class IsOnline {
private static readonly _isOnline: UIEventSource<boolean> = new UIEventSource(
Utils.runningFromConsole || navigator.onLine)
Utils.runningFromConsole || navigator.onLine
)
static {
if (!Utils.runningFromConsole) {
window.addEventListener("online", () => {
IsOnline._isOnline.set(true)
})
@ -19,5 +18,4 @@ export class IsOnline {
}
public static readonly isOnline: Store<boolean> = IsOnline._isOnline
}

View file

@ -145,7 +145,6 @@ export class MangroveIdentity {
}
}
/**
* Tracks all reviews of a given feature, allows to create a new review (and inserts this into the list)
*
@ -163,8 +162,9 @@ export default class FeatureReviews implements ReviewCollection {
private readonly _reviews: UIEventSource<
(Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> })[]
> = new UIEventSource(undefined)
public readonly reviews: Store<(Review & { kid: string, signature: string, madeByLoggedInUser: Store<boolean> })[]> =
this._reviews
public readonly reviews: Store<
(Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> })[]
> = this._reviews
private readonly _lat: number
private readonly _lon: number
private readonly _uncertainty: number
@ -181,7 +181,7 @@ export default class FeatureReviews implements ReviewCollection {
options?: Readonly<{
nameKey?: "name" | string
fallbackName?: string
uncertaintyRadius?: number,
uncertaintyRadius?: number
}>,
testmode?: Store<boolean>,
reportError?: (msg: string, extra: string) => Promise<void>
@ -281,7 +281,6 @@ export default class FeatureReviews implements ReviewCollection {
})
}
/**
* Construct a featureReviewsFor or fetches it from the cache
*
@ -303,7 +302,7 @@ export default class FeatureReviews implements ReviewCollection {
tagsSource: UIEventSource<Record<string, string>>,
mangroveIdentity: MangroveIdentity,
options: { nameKey: string; fallbackName: string },
state?: SpecialVisualizationState & WithUserRelatedState,
state?: SpecialVisualizationState & WithUserRelatedState
): FeatureReviews {
const key =
feature.properties.id +
@ -378,8 +377,8 @@ export default class FeatureReviews implements ReviewCollection {
this._identity.addReview(reviewWithKid)
}
private initReviews(){
if(this._reviews.data === undefined){
private initReviews() {
if (this._reviews.data === undefined) {
this._reviews.set([])
}
}
@ -448,16 +447,13 @@ export default class FeatureReviews implements ReviewCollection {
}
}
public async deleteReview(review: Review & {signature: string}){
public async deleteReview(review: Review & { signature: string }) {
await MangroveReviews.deleteReview(await this._identity.getKeypair(), review)
this.removeReviewLocally(review)
}
public removeReviewLocally(review: Review): void {
this._reviews.set(
this._reviews.data?.filter(r => r !== review)
)
this._reviews.set(this._reviews.data?.filter((r) => r !== review))
}
/**

View file

@ -465,6 +465,6 @@ export class CombinedFetcher {
this.fetchImage(source, lat, lon, state, sink)
}
return { images: sink.mapD(imgs => Utils.DedupOnId(imgs, i => i["id"])), state }
return { images: sink.mapD((imgs) => Utils.DedupOnId(imgs, (i) => i["id"])), state }
}
}

View file

@ -64,10 +64,10 @@ export class Denomination {
throw `${context} uses the old 'default'-key. Use "useIfNoUnitGiven" or "useAsDefaultInput" instead`
}
const humanTexts = Translations.T(json.human, context + "human")
const humanTexts = Translations.T(json.human)
humanTexts.OnEveryLanguage((text, language) => {
if (text.indexOf("{quantity}") < 0) {
throw `In denomination: a human text should contain {quantity} (at ${context}.human.${language})`
throw `In denomination: a human text should contain {quantity} (at ${context}.human.${language}). The offending text is: ${text}`
}
return text
})

View file

@ -174,7 +174,6 @@ export class MenuState {
this._selectedElement.setData(undefined)
return true
}
} finally {
this.isClosingAll = false
}

View file

@ -27,39 +27,38 @@ export class AvailableRasterLayers {
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: {
text: "OpenStreetMap",
url: "https://openStreetMap.org/copyright"
url: "https://openStreetMap.org/copyright",
},
best: true,
max_zoom: 19,
min_zoom: 0,
category: "osmbasedmap"
category: "osmbasedmap",
}
public static readonly osmCarto: RasterLayerPolygon = {
type: "Feature",
properties: AvailableRasterLayers.osmCartoProperties,
geometry: BBox.global.asGeometry()
geometry: BBox.global.asGeometry(),
}
public static readonly sunnyOfflineProperties: RasterLayerProperties = {
"style": "./assets/sunny.json",
"best": true,
"id": "protomaps.sunny-self",
"name": "Protomaps Sunny",
"type": "vector",
"category": "osmbasedmap",
"attribution": {
"text": "Protomaps",
"url": "https://protomaps.com/"
style: "./assets/sunny.json",
best: true,
id: "protomaps.sunny-self",
name: "Protomaps Sunny",
type: "vector",
category: "osmbasedmap",
attribution: {
text: "Protomaps",
url: "https://protomaps.com/",
},
url: "https://mapcomplete.org/"
url: "https://mapcomplete.org/",
}
public static readonly sunnyOffline: RasterLayerPolygon = {
type: "Feature",
properties: AvailableRasterLayers.sunnyOfflineProperties,
geometry: BBox.global.asGeometry()
geometry: BBox.global.asGeometry(),
}
public static readonly globalLayers: ReadonlyArray<RasterLayerPolygon> =
AvailableRasterLayers.initGlobalLayers()
@ -93,7 +92,8 @@ export class AvailableRasterLayers {
/**
* The default background layer that any theme uses which does not explicitly define a background
*/
public static readonly defaultBackgroundLayer: RasterLayerPolygon = AvailableRasterLayers.sunnyOffline
public static readonly defaultBackgroundLayer: RasterLayerPolygon =
AvailableRasterLayers.sunnyOffline
public static layersAvailableAt(
location: Store<{ lon: number; lat: number }>,
@ -136,7 +136,8 @@ export class AvailableRasterLayers {
if (
!matching.some(
(l) =>
l.id === AvailableRasterLayers.defaultBackgroundLayer?.properties?.id
l.id ===
AvailableRasterLayers.defaultBackgroundLayer?.properties?.id
)
) {
matching.push(AvailableRasterLayers.defaultBackgroundLayer)

View file

@ -10,9 +10,7 @@ import ThemeConfig from "../../src/Models/ThemeConfig/ThemeConfig"
import { ThemeConfigJson } from "../../src/Models/ThemeConfig/Json/ThemeConfigJson"
import SpecialVisualizations from "../../src/UI/SpecialVisualizations"
import ValidationUtils from "../../src/Models/ThemeConfig/Conversion/ValidationUtils"
import {
QuestionableTagRenderingConfigJson,
} from "../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { QuestionableTagRenderingConfigJson } from "../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { LayerConfigJson } from "../../src/Models/ThemeConfig/Json/LayerConfigJson"
import { Lists } from "../Utils/Lists"
@ -69,9 +67,8 @@ export class SourceOverview {
}
const neededUrls = usedSpecialVisualisation.func.needsUrls ?? []
if (typeof neededUrls === "function") {
const needed: string | string[] | ServerSourceInfo | ServerSourceInfo[] = neededUrls(
usedSpecialVisualisation.args
)
const needed: string | string[] | ServerSourceInfo | ServerSourceInfo[] =
neededUrls(usedSpecialVisualisation.args)
if (Array.isArray(needed)) {
apiUrls.push(...needed)
} else {
@ -108,26 +105,34 @@ export class SourceOverview {
const url = f.properties.url
const match = url.match(regex)
const packageInInfo: (url: (string | string[])) => ServerSourceInfo[] = (urls: string | string[]) => {
const packageInInfo: (url: string | string[]) => ServerSourceInfo[] = (
urls: string | string[]
) => {
if (typeof urls === "string") {
urls = [urls]
}
return urls.map(url => <ServerSourceInfo>{
url,
description:
"Background layer source or supporting sources for " + f.properties.id,
trigger: ["specific_feature"],
category: "maplayer",
moreInfo: Lists.noEmpty([
"https://github.com/osmlab/editor-layer-index",
f.properties?.attribution?.url,
]),
})
return urls.map(
(url) =>
<ServerSourceInfo>{
url,
description:
"Background layer source or supporting sources for " +
f.properties.id,
trigger: ["specific_feature"],
category: "maplayer",
moreInfo: Lists.noEmpty([
"https://github.com/osmlab/editor-layer-index",
f.properties?.attribution?.url,
]),
}
)
}
urls.push(...packageInInfo(["https://protomaps.github.io"]))
const packageInInfoD: (url: (string | string[])) => (ServerSourceInfo[]) = (url: string | string[]) => {
const packageInInfoD: (url: string | string[]) => ServerSourceInfo[] = (
url: string | string[]
) => {
if (!url) {
return []
}
@ -177,7 +182,7 @@ export class SourceOverview {
urls.push(...packageInInfo(url))
if (urlClipped.endsWith(".json")) {
const tileInfo = await Utils.downloadJsonCached(url, 1000 * 120, {
Origins: "https://mapcomplete.org"
Origins: "https://mapcomplete.org",
})
urls.push(...packageInInfo(tileInfo["tiles"] ?? []))
}
@ -192,7 +197,10 @@ export class SourceOverview {
} catch (e) {
console.error(e)
console.error(
"ERROR: could not download a resource: " + url + "\nSome sprites might not be whitelisted and thus not load")
"ERROR: could not download a resource: " +
url +
"\nSome sprites might not be whitelisted and thus not load"
)
}
}
}

View file

@ -132,7 +132,7 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
if (typeof leaf !== "object") {
return leaf
}
if(leaf["_context"] !== undefined){
if (leaf["_context"] !== undefined) {
// Context is already set
return leaf
}
@ -145,8 +145,9 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
path[i] = breadcrumb["id"]
}
}
const pth = this._prefix + Lists.noEmpty(context.path.concat(path).map(x => "" + x)).join(".")
console.log("Setting _context to: ",pth)
const pth =
this._prefix +
Lists.noEmpty(context.path.concat(path).map((x) => "" + x)).join(".")
return {
...leaf,
_context: pth,

View file

@ -219,7 +219,7 @@ export class Concat<X, T> extends Conversion<X[], T[]> {
return <undefined | null>values
}
const vals: T[][] = new Each(this._step).convert(values, context.inOperation("concat"))
return vals.flatMap(l => l)
return vals.flatMap((l) => l)
}
}
@ -276,7 +276,6 @@ export class Fuse<T> extends DesugaringStep<T> {
this.steps = Lists.noNull(steps)
}
convert(json: T, context: ConversionContext): T {
for (let i = 0; i < this.steps.length; i++) {
const step = this.steps[i]

View file

@ -109,7 +109,9 @@ export class PruneFilters extends DesugaringStep<LayerConfigJson> {
const sourceTags = TagUtils.Tag(json.source["osmTags"])
return {
...json,
filter: Lists.noNull(json.filter?.map((obj) => this.prune(sourceTags, <FilterConfigJson>obj, context))),
filter: Lists.noNull(
json.filter?.map((obj) => this.prune(sourceTags, <FilterConfigJson>obj, context))
),
}
}
}

View file

@ -244,12 +244,16 @@ export class ExtractImages extends Conversion<
}
// Split "circle:white;./assets/layers/.../something.svg" into ["circle", "./assets/layers/.../something.svg"]
const allPaths = Lists.noNull(Lists.noEmpty(foundImage.path?.split(";")?.map((part) => {
if (part.startsWith("http")) {
return part
}
return part.split(":")[0]
})))
const allPaths = Lists.noNull(
Lists.noEmpty(
foundImage.path?.split(";")?.map((part) => {
if (part.startsWith("http")) {
return part
}
return part.split(":")[0]
})
)
)
for (const path of allPaths) {
cleanedImages.push({
path,

View file

@ -1,6 +1,18 @@
import { Concat, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault } from "./Conversion"
import {
Concat,
DesugaringContext,
DesugaringStep,
Each,
FirstOf,
Fuse,
On,
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"
@ -761,10 +773,10 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
for (const trElement of tr.mappings) {
const showIf = TagUtils.optimzeJson({
and: Lists.noNull([
condition,
{
or: Lists.noNull([trElement.alsoShowIf, trElement.if]),
},
condition,
{
or: Lists.noNull([trElement.alsoShowIf, trElement.if]),
},
]),
})
if (showIf === true) {
@ -973,12 +985,14 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
const allAutoIndex = json.titleIcons.indexOf(<any>"auto:*")
if (allAutoIndex >= 0) {
const generated = Lists.noNull(json.tagRenderings.map((tr) => {
if (typeof tr === "string") {
return undefined
}
return this.createTitleIconsBasedOn(<any>tr)
}))
const generated = Lists.noNull(
json.tagRenderings.map((tr) => {
if (typeof tr === "string") {
return undefined
}
return this.createTitleIconsBasedOn(<any>tr)
})
)
json.titleIcons.splice(allAutoIndex, 1, ...generated)
return json
}
@ -1085,9 +1099,18 @@ export class OrderTagRendering extends DesugaringStep<TagRenderingConfigJson | s
}
private static readonly tagRenderingAttributesOrder: ReadonlyArray<string> = [
"id", "labels", "description", "question", "questionHint", "render", "icon", "freeform", "mappings",
"condition", "metacondition", "filter",
"id",
"labels",
"description",
"question",
"questionHint",
"render",
"icon",
"freeform",
"mappings",
"condition",
"metacondition",
"filter",
]
convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
@ -1096,17 +1119,21 @@ export class OrderTagRendering extends DesugaringStep<TagRenderingConfigJson | s
}
return Utils.reorder(json, OrderTagRendering.tagRenderingAttributesOrder)
}
}
export class OrderLayer extends DesugaringStep<string | LayerConfigJson> {
private static readonly layerAttributesOrder: ReadonlyArray<string> = Lists.dedup((<ConfigMeta[]>layerconfig).filter((c) => c.path.length === 1).map((c) => c.path[0]))
private static readonly layerAttributesOrder: ReadonlyArray<string> = Lists.dedup(
(<ConfigMeta[]>layerconfig).filter((c) => c.path.length === 1).map((c) => c.path[0])
)
constructor() {
super("OrderLayer", "Reorders a tagRendering to the default order")
}
public convert(json: LayerConfigJson | string, context: ConversionContext): LayerConfigJson | string {
public convert(
json: LayerConfigJson | string,
context: ConversionContext
): LayerConfigJson | string {
if (typeof json !== "object") {
return json
}
@ -1115,14 +1142,14 @@ export class OrderLayer extends DesugaringStep<string | LayerConfigJson> {
}
const orderTag = new OrderTagRendering()
// @ts-ignore
json = new On<"tagRenderings", TagRenderingConfigJson[], LayerConfigJson>("tagRenderings", new Each(
orderTag,
)).convert(<LayerConfigJson>json, context)
json = new On<"tagRenderings", TagRenderingConfigJson[], LayerConfigJson>(
"tagRenderings",
new Each(orderTag)
).convert(<LayerConfigJson>json, context)
// @ts-ignore
json = new On("title", orderTag).convert(json, context)
return Utils.reorder(json, OrderLayer.layerAttributesOrder)
}
}
@ -1136,8 +1163,7 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
"Fully prepares and expands a layer for the LayerConfig.",
new DeriveSource(),
new On("tagRenderings", new Each(new RewriteSpecial())),
new On("tagRenderings", new Concat(new ExpandRewrite())
.andThenF(Utils.Flatten)),
new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
new On(
"tagRenderings",
(layer) =>

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 { OrderLayer, PrepareLayer, RewriteSpecial } from "./PrepareLayer"
import { LayerConfigJson } from "../Json/LayerConfigJson"
@ -603,12 +613,15 @@ class PostvalidateTheme extends DesugaringStep<ThemeConfigJson> {
}
}
export class OrderTheme extends Fuse<ThemeConfigJson> {
private static readonly themeAttributesOrder: ReadonlyArray<string> = Lists.dedup((<ConfigMeta[]>themeconfig).filter((c) => c.path.length === 1).map((c) => c.path[0]))
private static readonly themeAttributesOrder: ReadonlyArray<string> = Lists.dedup(
(<ConfigMeta[]>themeconfig).filter((c) => c.path.length === 1).map((c) => c.path[0])
)
constructor() {
super("Reorders the layer to the default order",
super(
"Reorders the layer to the default order",
new On("layers", new Each(new OrderLayer()))
)
)
}
public convert(json: ThemeConfigJson, context: ConversionContext): ThemeConfigJson {

View file

@ -208,7 +208,9 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
}
{
// duplicate ids in tagrenderings check
const duplicates = Lists.noNull(Utils.Duplicates(json.tagRenderings?.map((tr) => tr?.["id"])))
const duplicates = Lists.noNull(
Utils.Duplicates(json.tagRenderings?.map((tr) => tr?.["id"]))
)
if (duplicates?.length > 0) {
// It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list
context

View file

@ -52,8 +52,8 @@ export default class ValidationUtils {
)
}
const translations: any[] = Lists.noNull([
renderingConfig.render,
...(renderingConfig.mappings ?? []).map((m) => m.then),
renderingConfig.render,
...(renderingConfig.mappings ?? []).map((m) => m.then),
])
const all: RenderingSpecification[] = []
for (let translation of translations) {

View file

@ -231,12 +231,12 @@ export default class FilterConfig {
const isDefault = this.options.length > 1 && (this.defaultSelection ?? 0) == i
return <string[]>(
Lists.noNull([
this.id + "." + i,
isDefault ? `*${opt.question.txt}* (default)` : opt.question,
opt.osmTags?.asHumanString() ?? "",
opt.fields?.length > 0
? opt.fields.map((f) => f.name + " (" + f.type + ")").join(" ")
: undefined,
this.id + "." + i,
isDefault ? `*${opt.question.txt}* (default)` : opt.question,
opt.osmTags?.asHumanString() ?? "",
opt.fields?.length > 0
? opt.fields.map((f) => f.name + " (" + f.type + ")").join(" ")
: undefined,
])
)
})

View file

@ -569,30 +569,32 @@ export default class LayerConfig extends WithContextLoader {
)
}
const tableRows: string[][] = Lists.noNull(this.tagRenderings
.map((tr) => tr.FreeformValues())
.filter((values) => values !== undefined)
.filter((values) => values.key !== "id")
.map((values) => {
const embedded: string[] = values.values?.map((v) =>
OsmWiki.constructLinkMd(values.key, v)
) ?? ["_no preset options defined, or no values in them_"]
const statistics = `https://taghistory.raifer.tech/?#***/${encodeURIComponent(
values.key
)}/`
const tagInfo = `https://taginfo.openstreetmap.org/keys/${values.key}#values`
return [
[
`<a target="_blank" href='${tagInfo}'><img src='https://mapcomplete.org/assets/svg/search.svg' height='18px'></a>`,
`<a target="_blank" href='${statistics}'><img src='https://mapcomplete.org/assets/svg/statistics.svg' height='18px'></a>`,
OsmWiki.constructLinkMd(values.key),
].join(" "),
values.type === undefined
? "Multiple choice"
: `[${values.type}](../SpecialInputElements.md#${values.type})`,
embedded.join(" "),
]
}))
const tableRows: string[][] = Lists.noNull(
this.tagRenderings
.map((tr) => tr.FreeformValues())
.filter((values) => values !== undefined)
.filter((values) => values.key !== "id")
.map((values) => {
const embedded: string[] = values.values?.map((v) =>
OsmWiki.constructLinkMd(values.key, v)
) ?? ["_no preset options defined, or no values in them_"]
const statistics = `https://taghistory.raifer.tech/?#***/${encodeURIComponent(
values.key
)}/`
const tagInfo = `https://taginfo.openstreetmap.org/keys/${values.key}#values`
return [
[
`<a target="_blank" href='${tagInfo}'><img src='https://mapcomplete.org/assets/svg/search.svg' height='18px'></a>`,
`<a target="_blank" href='${statistics}'><img src='https://mapcomplete.org/assets/svg/statistics.svg' height='18px'></a>`,
OsmWiki.constructLinkMd(values.key),
].join(" "),
values.type === undefined
? "Multiple choice"
: `[${values.type}](../SpecialInputElements.md#${values.type})`,
embedded.join(" "),
]
})
)
let quickOverview: string[] = []
if (tableRows.length > 0) {

View file

@ -31,19 +31,28 @@ export class IconConfig extends WithContextLoader {
}
}
export const allowed_location_codes = ["point", "centroid", "start", "end", "projected_centerpoint", "polygon_centroid", "waypoints"] as const
export type PointRenderingLocation = typeof allowed_location_codes[number]
export const allowed_location_codes = [
"point",
"centroid",
"start",
"end",
"projected_centerpoint",
"polygon_centroid",
"waypoints",
] as const
export type PointRenderingLocation = (typeof allowed_location_codes)[number]
export default class PointRenderingConfig extends WithContextLoader {
static readonly allowed_location_codes_set: ReadonlySet<PointRenderingLocation> = new Set<PointRenderingLocation>([
"point",
"centroid",
"start",
"end",
"projected_centerpoint",
"polygon_centroid",
"waypoints",
])
static readonly allowed_location_codes_set: ReadonlySet<PointRenderingLocation> =
new Set<PointRenderingLocation>([
"point",
"centroid",
"start",
"end",
"projected_centerpoint",
"polygon_centroid",
"waypoints",
])
public readonly location: Set<PointRenderingLocation>
public readonly marker: IconConfig[]

View file

@ -5,7 +5,10 @@ import { TagUtils } from "../../Logic/Tags/TagUtils"
import { And } from "../../Logic/Tags/And"
import { Utils } from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag"
import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson"
import {
MappingConfigJson,
QuestionableTagRenderingConfigJson,
} from "./Json/QuestionableTagRenderingConfigJson"
import Validators, { ValidatorType } from "../../UI/InputElement/Validators"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
import { RegexTag } from "../../Logic/Tags/RegexTag"
@ -539,18 +542,20 @@ export default class TagRenderingConfig {
if?: TagsFilter
then: TypedTranslation<Record<string, string>>
img?: string
}[] = Lists.noNull((this.mappings ?? [])?.filter((mapping) => {
if (mapping.if === undefined) {
return true
}
if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) {
return true
}
if (mapping.alsoShowIf?.matchesProperties(tags)) {
return true
}
return false
}))
}[] = Lists.noNull(
(this.mappings ?? [])?.filter((mapping) => {
if (mapping.if === undefined) {
return true
}
if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) {
return true
}
if (mapping.alsoShowIf?.matchesProperties(tags)) {
return true
}
return false
})
)
if (freeformKeyDefined && tags[this.freeform.key] !== undefined) {
const usedFreeformValues = new Set<string>(
@ -655,7 +660,9 @@ export default class TagRenderingConfig {
const key = this.freeform?.key
const answerMappings = this.mappings?.filter((m) => m.hideInAnswer !== true)
if (key === undefined) {
const values: { k: string; v: string }[][] = Lists.noNull(answerMappings?.map((m) => m.if.asChange({})) ?? [])
const values: { k: string; v: string }[][] = Lists.noNull(
answerMappings?.map((m) => m.if.asChange({})) ?? []
)
if (values.length === 0) {
return
}
@ -671,13 +678,17 @@ export default class TagRenderingConfig {
}
return {
key: commonKey,
values: Lists.noNull(values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v)),
values: Lists.noNull(
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v)
),
}
}
let values = Lists.noNull(answerMappings?.map(
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v
) ?? [])
let values = Lists.noNull(
answerMappings?.map(
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v
) ?? []
)
if (values.length === undefined) {
values = undefined
}
@ -1018,17 +1029,17 @@ export default class TagRenderingConfig {
}
return Lists.noNull([
"### " + this.id,
this.description,
this.question !== undefined
? "The question is `" + this.question.txt + "`"
: "_This tagrendering has no question and is thus read-only_",
freeform,
mappings,
condition,
labels,
"",
reuse,
"### " + this.id,
this.description,
this.question !== undefined
? "The question is `" + this.question.txt + "`"
: "_This tagrendering has no question and is thus read-only_",
freeform,
mappings,
condition,
labels,
"",
reuse,
]).join("\n")
}

View file

@ -56,7 +56,9 @@ export class UserMapFeatureswitchState extends WithUserRelatedState {
readonly fullNodeDatabase?: FullNodeDatabaseSource
readonly offlineMapManager = OfflineBasemapManager.singleton
public readonly autoDownloadOfflineBasemap = UIEventSource.asBoolean(LocalStorageSource.get("autodownload-offline-basemaps", "true"))
public readonly autoDownloadOfflineBasemap = UIEventSource.asBoolean(
LocalStorageSource.get("autodownload-offline-basemaps", "true")
)
constructor(theme: ThemeConfig, selectedElement: Store<object>) {
const rasterLayer: UIEventSource<RasterLayerPolygon> =
new UIEventSource<RasterLayerPolygon>(undefined)
@ -290,15 +292,17 @@ export class UserMapFeatureswitchState extends WithUserRelatedState {
}
private downloadOfflineBasemaps() {
const tile = this.mapProperties.location.mapD(l => {
const tile = this.mapProperties.location.mapD(
(l) => {
if (!IsOnline.isOnline.data || !this.autoDownloadOfflineBasemap.data) {
return undefined
}
const z = Math.min(Math.floor(this.mapProperties.zoom.data), 10)
return Tiles.embedded_tile(l.lat, l.lon, z)
},
[IsOnline.isOnline, this.mapProperties.zoom, this.autoDownloadOfflineBasemap])
tile.addCallbackAndRunD(tile => {
[IsOnline.isOnline, this.mapProperties.zoom, this.autoDownloadOfflineBasemap]
)
tile.addCallbackAndRunD((tile) => {
this.offlineMapManager.autoInstall(tile)
})
}

View file

@ -1,7 +1,5 @@
import { Changes } from "../../Logic/Osm/Changes"
import {
NewGeometryFromChangesFeatureSource
} from "../../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
import { NewGeometryFromChangesFeatureSource } from "../../Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource"
import { WithLayoutSourceState } from "./WithLayoutSourceState"
import ThemeConfig from "../ThemeConfig/ThemeConfig"
import { Utils } from "../../Utils"
@ -20,7 +18,9 @@ import { Map as MlMap } from "maplibre-gl"
import FilteringFeatureSource from "../../Logic/FeatureSource/Sources/FilteringFeatureSource"
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
import SelectedElementTagsUpdater from "../../Logic/Actors/SelectedElementTagsUpdater"
import NoElementsInViewDetector, { FeatureViewState } from "../../Logic/Actors/NoElementsInViewDetector"
import NoElementsInViewDetector, {
FeatureViewState,
} from "../../Logic/Actors/NoElementsInViewDetector"
export class WithChangesState extends WithLayoutSourceState {
readonly changes: Changes
@ -219,16 +219,14 @@ export class WithChangesState extends WithLayoutSourceState {
)
filteringFeatureSource.set(layerName, filtered)
ShowDataLayer.showLayerClustered(map,
this,
{
layer: fs.layer.layerDef,
features: filtered,
doShowLayer,
metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement,
fetchStore: (id) => this.featureProperties.getStore(id)
})
ShowDataLayer.showLayerClustered(map, this, {
layer: fs.layer.layerDef,
features: filtered,
doShowLayer,
metaTags: this.userRelatedState.preferencesAsTags,
selectedElement: this.selectedElement,
fetchStore: (id) => this.featureProperties.getStore(id),
})
/*new ShowDataLayer(map, {
layer: fs.layer.layerDef,
features: filtered,

View file

@ -18,7 +18,7 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource"
import NearbyFeatureSource from "../../Logic/FeatureSource/Sources/NearbyFeatureSource"
import {
SummaryTileSource,
SummaryTileSourceRewriter
SummaryTileSourceRewriter,
} from "../../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource"
import { ShowDataLayerOptions } from "../../UI/Map/ShowDataLayerOptions"
import { ClusterGrouping } from "../../Logic/FeatureSource/TiledFeatureSource/ClusteringFeatureSource"
@ -115,7 +115,7 @@ export class WithSpecialLayers extends WithChangesState {
this.mapProperties,
{
isActive: this.mapProperties.zoom.map((z) => z < maxzoom),
availableLayers: this.mvtAvailableLayers
availableLayers: this.mvtAvailableLayers,
}
)
@ -141,7 +141,7 @@ export class WithSpecialLayers extends WithChangesState {
private setupClusterLayer(): void {
new ShowDataLayer(this.map, {
features: ClusterGrouping.singleton,
layer: new LayerConfig(<LayerConfigJson>(<unknown>summaryLayer), "summaryLayer")
layer: new LayerConfig(<LayerConfigJson>(<unknown>summaryLayer), "summaryLayer"),
})
}

View file

@ -20,7 +20,6 @@ export class WithUserRelatedState {
readonly userRelatedState: UserRelatedState
readonly loadReviews: Store<boolean>
readonly overlayLayerStates: ReadonlyMap<
string,
{ readonly isDisplayed: UIEventSource<boolean> }
@ -118,13 +117,13 @@ export class WithUserRelatedState {
return this.theme.getMatchingLayer(properties)
}
private constructIsLoadingReviewsAllowed() : UIEventSource<boolean>{
private constructIsLoadingReviewsAllowed(): UIEventSource<boolean> {
const loadingAllowed = new UIEventSource(false)
const themeIsSensitive = this.theme?.enableMorePrivacy ?? false
const settings =
this?.osmConnection?.getPreference<"always" | "yes" | "ask" | "hidden">(
"reviews-allowed",
"reviews-allowed"
) ?? new ImmutableStore("yes")
settings.addCallbackAndRun((s) => {
if (s === "hidden") {
@ -143,5 +142,4 @@ export class WithUserRelatedState {
})
return loadingAllowed
}
}

View file

@ -57,13 +57,13 @@ export class Tiles {
* @param x
* @param y
*/
static centerPointOf(z: number, x: number, y: number): [number, number] ;
static centerPointOf(tileId: number): [number, number] ;
static centerPointOf(z: number, x: number, y: number): [number, number]
static centerPointOf(tileId: number): [number, number]
static centerPointOf(zOrId: number, x?: number, y?: number): [number, number] {
let z: number
if (x === undefined) {
[z, x, y] = Tiles.tile_from_index(zOrId)
;[z, x, y] = Tiles.tile_from_index(zOrId)
} else {
z = zOrId
}

View file

@ -80,8 +80,6 @@ export class Unit {
}
}
isApplicableToKey(key: string | undefined): boolean {
if (key === undefined) {
return false

View file

@ -13,11 +13,11 @@ export class UnitUtils {
json:
| UnitConfigJson
| Record<
string,
string | { quantity: string; denominations: string[]; inverted?: boolean }
>,
string,
string | { quantity: string; denominations: string[]; inverted?: boolean }
>,
tagRenderings: TagRenderingConfig[],
ctx: string,
ctx: string
): Unit[] {
const types: Record<string, ValidatorType> = {}
for (const tagRendering of tagRenderings) {
@ -36,10 +36,10 @@ export class UnitUtils {
json: UnitConfigJson,
validator: Validator,
appliesToKey: string,
ctx: string,
ctx: string
): Unit {
const applicable = json.applicableUnits.map((u, i) =>
Denomination.fromJson(u, validator, `${ctx}.units[${i}]`),
Denomination.fromJson(u, validator, `${ctx}.units[${i}]`)
)
if (
@ -58,7 +58,7 @@ export class UnitUtils {
appliesToKey === undefined ? undefined : [appliesToKey],
applicable,
json.eraseInvalidValues ?? false,
validator,
validator
)
}
@ -103,7 +103,7 @@ export class UnitUtils {
private static parse(
json: UnitConfigJson,
types: Record<string, ValidatorType>,
ctx: string,
ctx: string
): Unit[] {
const appliesTo = json.appliesToKey
for (let i = 0; i < (appliesTo ?? []).length; i++) {
@ -132,7 +132,7 @@ export class UnitUtils {
private static initUnits(): Map<string, Unit> {
const m = new Map<string, Unit>()
const units = (<UnitConfigJson[]>unit.units).flatMap((json, i) =>
this.parse(json, {}, "unit.json.units." + i),
this.parse(json, {}, "unit.json.units." + i)
)
for (const unit of units) {
@ -163,7 +163,7 @@ export class UnitUtils {
| { quantity: string; denominations: string[]; canonical?: string; inverted?: boolean }
>,
types: Record<string, ValidatorType>,
ctx: string,
ctx: string
): Unit[] {
const units: Unit[] = []
for (const key in spec) {
@ -178,8 +178,8 @@ export class UnitUtils {
loaded.denominations,
loaded.eraseInvalid,
validator,
toLoad["inverted"],
),
toLoad["inverted"]
)
)
continue
}
@ -189,7 +189,7 @@ export class UnitUtils {
const fetchDenom = (d: string): Denomination => {
const found = loaded.denominations.find(
(denom) => denom.canonical.toLowerCase() === d,
(denom) => denom.canonical.toLowerCase() === d
)
if (!found) {
throw (
@ -223,8 +223,8 @@ export class UnitUtils {
denoms,
loaded.eraseInvalid,
validator,
toLoad["inverted"],
),
toLoad["inverted"]
)
)
}
return units

View file

@ -156,8 +156,8 @@
</script>
<main>
<div class="w-full low-interaction">
<InsetSpacer height={AndroidPolyfill.getInsetSizes().top}/>
<div class="low-interaction w-full">
<InsetSpacer height={AndroidPolyfill.getInsetSizes().top} />
</div>
<DrawerLeft shown={guistate.pageStates.menu}>
@ -186,7 +186,7 @@
<div class="link-underline flex w-full flex-col">
<div class="flex items-center">
<div class="m-1 flex-none md:hidden">
<Logo alt="MapComplete Logo" class="mr-1 sm:mr-2 md:mr-4 h-10 w-10 sm:h-20 sm:w-20" />
<Logo alt="MapComplete Logo" class="mr-1 h-10 w-10 sm:mr-2 sm:h-20 sm:w-20 md:mr-4" />
</div>
<div class="flex flex-col">
<h1 class="m-0 break-words font-extrabold tracking-tight md:text-6xl">
@ -275,8 +275,7 @@
v{Constants.vNumber}
</div>
<InsetSpacer height={AndroidPolyfill.getInsetSizes().bottom}/>
<InsetSpacer height={AndroidPolyfill.getInsetSizes().bottom} />
<div class="absolute top-0 h-0 w-0" style="margin-left: -10em">
<MenuDrawer onlyLink={false} state={menuDrawerState} />

View file

@ -36,11 +36,10 @@ export interface TagRenderingChartOptions {
groupToOtherCutoff?: 3 | number
sort?: boolean
hideUnkown?: boolean
hideNotApplicable?: boolean,
hideNotApplicable?: boolean
chartType?: "pie" | "bar" | "doughnut"
}
export class ChartJsUtils {
/**
* Gets the 'date' out of all features.properties,
* returns a range with all dates from 'earliest' to 'latest' as to get one continuous range
@ -178,7 +177,7 @@ export class ChartJsUtils {
}
data.push(...categoryCounts, ...otherData)
labels.push(...(mappings?.map((m) => m.then.txt) ?? []), ...otherLabels)
if(data.length === 0){
if (data.length === 0) {
return undefined
}
return { labels, data }
@ -196,139 +195,139 @@ export class ChartJsUtils {
* @param options
*/
static createPerDayConfigForTagRendering(
tr: TagRenderingConfig,
features: (OsmFeature & { properties: { date: string } })[],
options?: {
period: "day" | "month"
groupToOtherCutoff?: 3 | number
// If given, take the sum of these fields to get the feature weight
sumFields?: ReadonlyArray<string>
hideUnknown?: boolean
hideNotApplicable?: boolean
}
tr: TagRenderingConfig,
features: (OsmFeature & { properties: { date: string } })[],
options?: {
period: "day" | "month"
groupToOtherCutoff?: 3 | number
// If given, take the sum of these fields to get the feature weight
sumFields?: ReadonlyArray<string>
hideUnknown?: boolean
hideNotApplicable?: boolean
}
): ChartConfiguration {
const { labels, data } = ChartJsUtils.extractDataAndLabels(tr, features, {
sort: true,
groupToOtherCutoff: options?.groupToOtherCutoff,
hideNotApplicable: options?.hideNotApplicable,
hideUnkown: options?.hideUnknown,
})
if (labels === undefined || data === undefined) {
console.error(
"Could not extract data and labels for ",
tr,
" with features",
features,
": no labels or no data"
)
throw "No labels or data given..."
const { labels, data } = ChartJsUtils.extractDataAndLabels(tr, features, {
sort: true,
groupToOtherCutoff: options?.groupToOtherCutoff,
hideNotApplicable: options?.hideNotApplicable,
hideUnkown: options?.hideUnknown,
})
if (labels === undefined || data === undefined) {
console.error(
"Could not extract data and labels for ",
tr,
" with features",
features,
": no labels or no data"
)
throw "No labels or data given..."
}
for (let i = labels.length; i >= 0; i--) {
if (data[i]?.length != 0) {
continue
}
data.splice(i, 1)
labels.splice(i, 1)
}
const datasets: {
label: string /*themename*/
data: number[] /*counts per day*/
backgroundColor: string
}[] = []
const allDays = ChartJsUtils.getAllDays(features)
let trimmedDays = allDays.map((d) => d.substring(0, 10))
if (options?.period === "month") {
trimmedDays = trimmedDays.map((d) => d.substring(0, 7))
}
trimmedDays = Lists.dedup(trimmedDays)
for (let i = 0; i < labels.length; i++) {
const label = labels[i]
const changesetsForTheme = data[i]
const perDay: Record<string, OsmFeature[]> = {}
for (const changeset of changesetsForTheme) {
const csDate = new Date(changeset.properties.date)
Utils.SetMidnight(csDate)
let str = csDate.toISOString()
str = str.substr(0, 10)
if (options?.period === "month") {
str = str.substr(0, 7)
}
if (perDay[str] === undefined) {
perDay[str] = [changeset]
} else {
perDay[str].push(changeset)
}
}
for (let i = labels.length; i >= 0; i--) {
if (data[i]?.length != 0) {
const countsPerDay: number[] = []
for (let i = 0; i < trimmedDays.length; i++) {
const day = trimmedDays[i]
const featuresForDay = perDay[day]
if (!featuresForDay) {
continue
}
data.splice(i, 1)
labels.splice(i, 1)
}
const datasets: {
label: string /*themename*/
data: number[] /*counts per day*/
backgroundColor: string
}[] = []
const allDays = ChartJsUtils.getAllDays(features)
let trimmedDays = allDays.map((d) => d.substring(0, 10))
if (options?.period === "month") {
trimmedDays = trimmedDays.map((d) => d.substring(0, 7))
}
trimmedDays = Lists.dedup(trimmedDays)
for (let i = 0; i < labels.length; i++) {
const label = labels[i]
const changesetsForTheme = data[i]
const perDay: Record<string, OsmFeature[]> = {}
for (const changeset of changesetsForTheme) {
const csDate = new Date(changeset.properties.date)
Utils.SetMidnight(csDate)
let str = csDate.toISOString()
str = str.substr(0, 10)
if (options?.period === "month") {
str = str.substr(0, 7)
}
if (perDay[str] === undefined) {
perDay[str] = [changeset]
} else {
perDay[str].push(changeset)
}
}
const countsPerDay: number[] = []
for (let i = 0; i < trimmedDays.length; i++) {
const day = trimmedDays[i]
const featuresForDay = perDay[day]
if (!featuresForDay) {
continue
}
if (options.sumFields !== undefined) {
let sum = 0
for (const featuresForDayElement of featuresForDay) {
const props = featuresForDayElement.properties
for (const key of options.sumFields) {
if (!props[key]) {
continue
}
const v = Number(props[key])
if (isNaN(v)) {
continue
}
sum += v
if (options.sumFields !== undefined) {
let sum = 0
for (const featuresForDayElement of featuresForDay) {
const props = featuresForDayElement.properties
for (const key of options.sumFields) {
if (!props[key]) {
continue
}
const v = Number(props[key])
if (isNaN(v)) {
continue
}
sum += v
}
countsPerDay[i] = sum
} else {
countsPerDay[i] = featuresForDay?.length ?? 0
}
countsPerDay[i] = sum
} else {
countsPerDay[i] = featuresForDay?.length ?? 0
}
let backgroundColor =
ChartJsColours.borderColors[i % ChartJsColours.borderColors.length]
if (label === "Unknown") {
backgroundColor = ChartJsColours.unknownBorderColor
}
if (label === "Other") {
backgroundColor = ChartJsColours.otherBorderColor
}
datasets.push({
data: countsPerDay,
backgroundColor,
label,
})
}
const perDayData = {
labels: trimmedDays,
datasets,
let backgroundColor =
ChartJsColours.borderColors[i % ChartJsColours.borderColors.length]
if (label === "Unknown") {
backgroundColor = ChartJsColours.unknownBorderColor
}
if (label === "Other") {
backgroundColor = ChartJsColours.otherBorderColor
}
datasets.push({
data: countsPerDay,
backgroundColor,
label,
})
}
return <ChartConfiguration>{
type: "bar",
data: perDayData,
options: {
responsive: true,
legend: {
display: false,
const perDayData = {
labels: trimmedDays,
datasets,
}
return <ChartConfiguration>{
type: "bar",
data: perDayData,
options: {
responsive: true,
legend: {
display: false,
},
scales: {
x: {
stacked: true,
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
},
y: {
stacked: true,
},
},
}
},
}
}
/**
@ -336,17 +335,16 @@ export class ChartJsUtils {
*
* @returns undefined if not enough parameters
*/
static createConfigForTagRendering<T extends { properties: Record<string, string> }>(tagRendering: TagRenderingConfig, features: T[],
options?: TagRenderingChartOptions): ChartConfiguration{
static createConfigForTagRendering<T extends { properties: Record<string, string> }>(
tagRendering: TagRenderingConfig,
features: T[],
options?: TagRenderingChartOptions
): ChartConfiguration {
if (tagRendering.mappings?.length === 0 && tagRendering.freeform?.key === undefined) {
return undefined
}
const { labels, data } = ChartJsUtils.extractDataAndLabels(
tagRendering,
features,
options
)
const { labels, data } = ChartJsUtils.extractDataAndLabels(tagRendering, features, options)
if (labels === undefined || data === undefined) {
return undefined
}
@ -403,22 +401,18 @@ export class ChartJsUtils {
},
},
}
}
static createHistogramConfig(keys: string[], counts: Map<string, number>){
const borderColor = [
]
const backgroundColor = [
]
static createHistogramConfig(keys: string[], counts: Map<string, number>) {
const borderColor = []
const backgroundColor = []
while (borderColor.length < keys.length) {
borderColor.push(...ChartJsColours.borderColors)
backgroundColor.push(...ChartJsColours.backgroundColors)
}
return <ChartConfiguration>{
return <ChartConfiguration>{
type: "bar",
data: {
labels: keys,
@ -432,11 +426,12 @@ export class ChartJsUtils {
},
],
},
options: { scales: {
options: {
scales: {
y: {
ticks: {
stepSize: 1,
callback: (value) =>Number(value).toFixed(0),
callback: (value) => Number(value).toFixed(0),
},
},
},

View file

@ -12,7 +12,7 @@
export let state: ThemeViewState = undefined
async function shareCurrentLink() {
const title = state?.theme?.title?.txt ?? "MapComplete";
const title = state?.theme?.title?.txt ?? "MapComplete"
const textToShow = state?.theme?.description?.txt ?? ""
await navigator.share({
title,

View file

@ -4,12 +4,12 @@
export let size = "w-12 h-12"
</script>
<div class={size+" relative"}>
<div class="absolute top-0 left-0">
<div class={size + " relative"}>
<div class="absolute left-0 top-0">
<slot />
</div>
<div class="absolute top-0 left-0">
<div class="absolute left-0 top-0">
<Cross_bottom_right class={size} color="red" />
</div>
</div>

View file

@ -32,7 +32,10 @@
return { bearing, dist }
}
)
let bearingFromGps = state.geolocation.geolocationState.currentGPSLocation.mapD((coordinate) => GeoOperations.bearing([coordinate.longitude, coordinate.latitude], fcenter),onDestroy)
let bearingFromGps = state.geolocation.geolocationState.currentGPSLocation.mapD(
(coordinate) => GeoOperations.bearing([coordinate.longitude, coordinate.latitude], fcenter),
onDestroy
)
let compass = Orientation.singleton.alpha
let relativeDirections = Translations.t.general.visualFeedback.directionsRelative
@ -102,7 +105,8 @@
})
return mainTr.textFor(lang)
},
[compass, Locale.language], onDestroy
[compass, Locale.language],
onDestroy
)
let label = labelFromCenter.map(
@ -115,7 +119,8 @@
}
return labelFromCenter
},
[labelFromGps], onDestroy
[labelFromGps],
onDestroy
)
function focusMap() {

View file

@ -21,14 +21,13 @@
// Does not need a 'top-inset-spacer' as the code below will apply the padding automatically
let height = 0
function setHeight(){
function setHeight() {
let topbar = document.getElementById("top-bar")
height = (topbar?.clientHeight ?? 0) + AndroidPolyfill.getInsetSizes().top.data
}
onMount(() => setHeight())
AndroidPolyfill.getInsetSizes().top.addCallback(() => setHeight())
</script>
<Drawer
@ -43,15 +42,14 @@
rightOffset="inset-y-0 right-0"
bind:hidden
>
<div class="flex flex-col h-full">
<div class="low-interaction h-screen">
<div style={`padding-top: ${height}px`}>
<div class="flex h-full flex-col overflow-y-auto">
<slot />
<div class="flex h-full flex-col">
<div class="low-interaction h-screen">
<div style={`padding-top: ${height}px`}>
<div class="flex h-full flex-col overflow-y-auto">
<slot />
</div>
</div>
</div>
</div>
<InsetSpacer clss="low-interaction" height={AndroidPolyfill.getInsetSizes().bottom}/>
<InsetSpacer clss="low-interaction" height={AndroidPolyfill.getInsetSizes().bottom} />
</div>
</Drawer>

View file

@ -24,7 +24,9 @@
<div
class="pointer-events-none absolute bottom-0 right-0 h-full w-screen p-4 md:p-6"
style="z-index: 21"
on:click={() => { dispatch("close") }}
on:click={() => {
dispatch("close")
}}
>
<div
class="content normal-background pointer-events-auto relative h-full"
@ -44,9 +46,9 @@
</div>
<style>
.content {
border-radius: 0.5rem;
overflow-x: hidden;
box-shadow: 0 0 1rem #00000088;
}
.content {
border-radius: 0.5rem;
overflow-x: hidden;
box-shadow: 0 0 1rem #00000088;
}
</style>

View file

@ -8,4 +8,4 @@
export let clss: string = ""
</script>
<div class={clss+" shrink-0"} style={"height: "+$height+"px"} />
<div class={clss + " shrink-0"} style={"height: " + $height + "px"} />

View file

@ -40,15 +40,13 @@
{#if $badge}
{#if !$online}
{#if !hiddenFail}
<div class="alert">
Your device is offline
</div>
<div class="alert">Your device is offline</div>
{/if}
{:else if !ignoreLoading && !hiddenFail && $loadingStatus === "loading"}
<slot name="loading">
<Loading />
</slot>
{:else if ($loadingStatus === "error" || $apiState === "readonly" || $apiState === "offline")}
{:else if $loadingStatus === "error" || $apiState === "readonly" || $apiState === "offline"}
{#if !hiddenFail}
<slot name="error">
<div class="alert flex flex-col items-center">

View file

@ -34,16 +34,16 @@
</script>
{#if $showButton}
<button class="as-link sidebar-button" on:click={openJosm}>
<Josm_logo class="h-6 w-6" />
<Tr t={t.editJosm} />
</button>
<button class="as-link sidebar-button" on:click={openJosm}>
<Josm_logo class="h-6 w-6" />
<Tr t={t.editJosm} />
</button>
{#if $josmState === undefined}
<!-- empty -->
{:else if $josmState === "OK"}
<Tr cls="thanks shrink-0 w-fit" t={t.josmOpened} />
{:else}
<Tr cls="alert shrink-0 w-fit" t={t.josmNotOpened} />
{/if}
{#if $josmState === undefined}
<!-- empty -->
{:else if $josmState === "OK"}
<Tr cls="thanks shrink-0 w-fit" t={t.josmOpened} />
{:else}
<Tr cls="alert shrink-0 w-fit" t={t.josmNotOpened} />
{/if}
{/if}

View file

@ -18,7 +18,8 @@
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 border-4 border-red-500 " + shared
let defaultClass =
"relative flex flex-col mx-auto w-full divide-y border-4 border-red-500 " + shared
if (fullscreen) {
defaultClass = shared
}
@ -43,7 +44,6 @@
})
let marginTop = AndroidPolyfill.getInsetSizes().top
let marginBottom = AndroidPolyfill.getInsetSizes().bottom
</script>
<Modal

View file

@ -16,7 +16,8 @@
let layer = state.getMatchingLayer(selected.properties)
let stillMatches = tags.map(
(tags) => !layer?.source?.osmTags || layer?.source?.osmTags?.matchesProperties(tags), onDestroy
(tags) => !layer?.source?.osmTags || layer?.source?.osmTags?.matchesProperties(tags),
onDestroy
)
onDestroy(
stillMatches.addCallbackAndRunD((matches) => {

View file

@ -2,7 +2,6 @@ import BaseUIElement from "../BaseUIElement"
import { SvelteComponentTyped } from "svelte"
/**
* The SvelteUIComponent serves as a translating class which which wraps a SvelteElement into the BaseUIElement framework.
* Also see ToSvelte.svelte for the opposite conversion

View file

@ -79,7 +79,6 @@ export default abstract class BaseUIElement {
}
}
return el
} catch (e) {
const domExc = e as DOMException

View file

@ -1,56 +1,68 @@
<script lang="ts">/**
* Show a compass. The compass outline rotates with the map, the compass needle points to the actual north
*/
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Orientation } from "../../Sensors/Orientation"
import Compass_back from "../../assets/svg/Compass_back.svelte"
import Compass_needle from "../../assets/svg/Compass_needle.svelte"
import North_arrow from "../../assets/svg/North_arrow.svelte"
<script lang="ts">
/**
* Show a compass. The compass outline rotates with the map, the compass needle points to the actual north
*/
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Orientation } from "../../Sensors/Orientation"
import Compass_back from "../../assets/svg/Compass_back.svelte"
import Compass_needle from "../../assets/svg/Compass_needle.svelte"
import North_arrow from "../../assets/svg/North_arrow.svelte"
export let mapProperties: { rotation: UIEventSource<number>, allowRotating: Store<boolean> }
let orientation = Orientation.singleton.alpha
let gotNonZero = new UIEventSource(false)
orientation.addCallbackAndRunD(o => {
if (o !== undefined && o !== 0) {
gotNonZero.set(true)
}
})
let mapRotation = mapProperties.rotation
export let size = "h-10 w-10"
let wrapperClass = "absolute top-0 left-0 " + size
let compassLoaded = Orientation.singleton.gotMeasurement
let allowRotation = mapProperties.allowRotating
function clicked(e: Event) {
if (mapProperties.rotation.data === 0) {
if (mapProperties.allowRotating.data && compassLoaded.data) {
mapProperties.rotation.set(orientation.data)
export let mapProperties: { rotation: UIEventSource<number>; allowRotating: Store<boolean> }
let orientation = Orientation.singleton.alpha
let gotNonZero = new UIEventSource(false)
orientation.addCallbackAndRunD((o) => {
if (o !== undefined && o !== 0) {
gotNonZero.set(true)
}
} else {
mapProperties.rotation.set(0)
}
}
})
let mapRotation = mapProperties.rotation
export let size = "h-10 w-10"
let wrapperClass = "absolute top-0 left-0 " + size
let compassLoaded = Orientation.singleton.gotMeasurement
let allowRotation = mapProperties.allowRotating
function clicked(e: Event) {
if (mapProperties.rotation.data === 0) {
if (mapProperties.allowRotating.data && compassLoaded.data) {
mapProperties.rotation.set(orientation.data)
}
} else {
mapProperties.rotation.set(0)
}
}
</script>
{#if $allowRotation || $gotNonZero}
<button style="z-index: -1" class={"relative as-link pointer-events-auto "+size} on:click={(e) => clicked(e)}>
{#if $allowRotation && !$compassLoaded && !$gotNonZero}
<div class={"border-2 rounded-full border-gray-500 border-dotted "+wrapperClass}
style={`transform: rotate(${-$mapRotation}deg); transition: transform linear 500ms`}>
<North_arrow class="w-full" />
</div>
{:else}
{#if $allowRotation}
<div class={wrapperClass} style={`transform: rotate(${-$mapRotation}deg); transition: transform linear 500ms`}>
<Compass_back class="w-full" />
<button
style="z-index: -1"
class={"as-link pointer-events-auto relative " + size}
on:click={(e) => clicked(e)}
>
{#if $allowRotation && !$compassLoaded && !$gotNonZero}
<div
class={"rounded-full border-2 border-dotted border-gray-500 " + wrapperClass}
style={`transform: rotate(${-$mapRotation}deg); transition: transform linear 500ms`}
>
<North_arrow class="w-full" />
</div>
{:else}
{#if $allowRotation}
<div
class={wrapperClass}
style={`transform: rotate(${-$mapRotation}deg); transition: transform linear 500ms`}
>
<Compass_back class="w-full" />
</div>
{/if}
{#if $compassLoaded && $gotNonZero}
<div
class={wrapperClass + (!$allowRotation ? " rounded-full bg-white bg-opacity-50" : "")}
style={`transform: rotate(${-$orientation}deg)`}
>
<Compass_needle class="w-full" />
</div>
{/if}
{/if}
{#if $compassLoaded && $gotNonZero}
<div class={wrapperClass+ (!$allowRotation ? " rounded-full bg-white bg-opacity-50" : "")}
style={`transform: rotate(${-$orientation}deg)`}>
<Compass_needle class="w-full" />
</div>
{/if}
{/if}
</button>
</button>
{/if}

View file

@ -151,12 +151,12 @@
</div>
<div class="mt-8 flex flex-col items-center gap-x-2 border-t border-dashed border-gray-300 pt-4">
<div class="mr-4 flex w-96 flex-wrap md:flex-nowrap items-center justify-center">
<div class="mr-4 flex w-96 flex-wrap items-center justify-center md:flex-nowrap">
<a href="https://nlnet.nl/entrust" class="p-2">
<NGI0Entrust_tag class="grow-0 w-48"/>
<NGI0Entrust_tag class="w-48 grow-0" />
</a>
<a href="https://nlnet.nl" class="p-2">
<Nlnet class="grow-0 w-48"/>
<Nlnet class="w-48 grow-0" />
</a>
</div>
<span>

View file

@ -6,29 +6,31 @@
import { Lists } from "../../Utils/Lists"
export let values: Store<string[]>
let counts: Store<Map<string, number>> = values.map(
(values) => {
if (values === undefined) {
return undefined
}
let counts: Store<Map<string, number>> = values.map((values) => {
if (values === undefined) {
return undefined
}
values = Utils.noNull(values)
const counts = new Map<string, number>()
for (const value of values) {
const c = counts.get(value) ?? 0
counts.set(value, c + 1)
}
values = Utils.noNull(values)
const counts = new Map<string, number>()
for (const value of values) {
const c = counts.get(value) ?? 0
counts.set(value, c + 1)
}
return counts
})
return counts
})
let max: Store<number> = counts.mapD(counts => Math.max(...Array.from(counts.values())))
let keys: Store<string> = counts.mapD(counts => {
let max: Store<number> = counts.mapD((counts) => Math.max(...Array.from(counts.values())))
let keys: Store<string> = counts.mapD((counts) => {
const keys = Lists.dedup(counts.keys())
keys.sort(/*inplace sort*/)
return keys
})
let config: Store<ChartConfiguration> = keys.mapD(keys => ChartJsUtils.createHistogramConfig(keys, counts.data), [counts])
let config: Store<ChartConfiguration> = keys.mapD(
(keys) => ChartJsUtils.createHistogramConfig(keys, counts.data),
[counts]
)
</script>
{#if $config}

View file

@ -51,7 +51,6 @@
}
})
let hotkeys = Hotkeys._docs
</script>
<div class:h-0={!onlyLink} class:h-full={onlyLink} class="overflow-hidden">

View file

@ -95,7 +95,6 @@
let nrOfFailedImages = ImageUploadQueue.singleton.imagesInQueue
let failedImagesOpen = pg.failedImages
let loggedIn = state.osmConnection.isLoggedIn
</script>
<div class="low-interaction flex h-full flex-col overflow-hidden" class:hidden={!$shown}>
@ -129,14 +128,13 @@
<SidebarUnit>
<LoginToggle {state}>
<LoginButton clss="primary" osmConnection={state.osmConnection} slot="not-logged-in" />
<div class="flex items-center gap-x-4 w-full m-2">
<div class="m-2 flex w-full items-center gap-x-4">
{#if $userdetails.img}
<img alt="avatar" src={$userdetails.img} class="h-12 w-12 rounded-full" />
{:else}
<UserCircle class="h-14 w-14" color="gray"/>
{:else}
<UserCircle class="h-14 w-14" color="gray" />
{/if}
<div class="flex flex-col w-full gap-y-2">
<div class="flex w-full flex-col gap-y-2">
<b>{$userdetails.name}</b>
<LogoutButton clss="as-link small subtle text-sm" osmConnection={state.osmConnection} />
</div>
@ -205,7 +203,8 @@
<LanguagePicker
preferredLanguages={state.userRelatedState.osmConnection.userDetails.mapD(
(ud) => ud.languages, onDestroy
(ud) => ud.languages,
onDestroy
)}
/>
</SidebarUnit>
@ -219,12 +218,7 @@
<Tr t={Translations.t.general.menu.aboutMapComplete} />
</h3>
<a
class="flex"
href={($isAndroid
? "https://mapcomplete.org"
: ".")+"/studio.html"}
>
<a class="flex" href={($isAndroid ? "https://mapcomplete.org" : ".") + "/studio.html"}>
<Pencil class="mr-2 h-6 w-6" />
<Tr t={Translations.t.general.morescreen.createYourOwnTheme} />
</a>
@ -276,8 +270,11 @@
</a>
{#if !state.theme}
<a class="flex" href={($isAndroid ? "https://mapcomplete.org" : ".") +`/statistics.html`}
target="_blank">
<a
class="flex"
href={($isAndroid ? "https://mapcomplete.org" : ".") + `/statistics.html`}
target="_blank"
>
<ChartBar class="h-6 w-6" />
<Tr
t={Translations.t.general.attribution.openStatistics.Subs({ theme: "MapComplete" })}
@ -334,5 +331,4 @@
<InsetSpacer height={AndroidPolyfill.getInsetSizes().bottom} />
{/if}
</div>
</div>

View file

@ -114,7 +114,7 @@
})
}
const snappedLocation = new SnappingFeatureSource(
new FeatureSourceMerger(...(Lists.noNull(snapSources))),
new FeatureSourceMerger(...Lists.noNull(snapSources)),
// We snap to the (constantly updating) map location
mapProperties.location,
{

View file

@ -1,5 +1,4 @@
<script lang="ts">
import type { Map as MlMap } from "maplibre-gl"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
@ -22,7 +21,6 @@
import { OfflineBasemapManager } from "../../Logic/OfflineBasemapManager"
import Checkbox from "../Base/Checkbox.svelte"
export let state: ThemeViewState & SpecialVisualizationState = undefined
export let autoDownload = state.autoDownloadOfflineBasemap
@ -35,20 +33,30 @@
mapProperties.location.set(state.mapProperties.location.data)
mapProperties.allowRotating.set(false)
const offlineMapManager = OfflineBasemapManager.singleton
let installing: Store<ReadonlyMap<string, object>> = offlineMapManager.installing
let installed = offlineMapManager.installedAreas
let focusTile: Store<{
x: number;
y: number;
z: number
} | undefined> = mapProperties.location.mapD(location => Tiles.embedded_tile(location.lat, location.lon, focusZ))
let focusTileIsInstalled = focusTile.mapD(tile => offlineMapManager.isInstalled(tile), [installed])
let focusTileIsInstalling = focusTile.mapD(tile => {
const { x, y, z } = tile
return installing.data?.has(`${z}-${x}-${y}.pmtiles`)
}, [installing])
let focusTile: Store<
| {
x: number
y: number
z: number
}
| undefined
> = mapProperties.location.mapD((location) =>
Tiles.embedded_tile(location.lat, location.lon, focusZ)
)
let focusTileIsInstalled = focusTile.mapD(
(tile) => offlineMapManager.isInstalled(tile),
[installed]
)
let focusTileIsInstalling = focusTile.mapD(
(tile) => {
const { x, y, z } = tile
return installing.data?.has(`${z}-${x}-${y}.pmtiles`)
},
[installing]
)
async function del(areaDescr: AreaDescription) {
await offlineMapManager.deleteArea(areaDescr)
@ -63,111 +71,120 @@
const f = Tiles.asGeojson(z, x, y)
f.properties = {
id: "center_point_" + z + "_" + x + "_" + y,
txt: "Tile " + x + " " + y
txt: "Tile " + x + " " + y,
}
return [f]
})
let installedFeature: Store<Feature<Polygon>[]> = installed.map(meta =>
(meta ?? [])
.map(area => {
const f = Tiles.asGeojson(area.minzoom, area.x, area.y)
f.properties = {
id: area.minzoom + "-" + area.x + "-" + area.y,
downloaded: "yes",
text: area.name + " " + new Date(area.dataVersion).toLocaleDateString() + " " + Utils.toHumanByteSize(Number(area.size))
}
return f
}
)
let installedFeature: Store<Feature<Polygon>[]> = installed.map((meta) =>
(meta ?? []).map((area) => {
const f = Tiles.asGeojson(area.minzoom, area.x, area.y)
f.properties = {
id: area.minzoom + "-" + area.x + "-" + area.y,
downloaded: "yes",
text:
area.name +
" " +
new Date(area.dataVersion).toLocaleDateString() +
" " +
Utils.toHumanByteSize(Number(area.size)),
}
return f
})
)
new ShowDataLayer(map, {
features: new StaticFeatureSource(installedFeature),
layer: new LayerConfig({
id: "downloaded",
source: "special",
lineRendering: [{
color: "blue",
width: {
mappings: [
{
if: `id!~${focusZ}-.*`,
then: "1"
}
]
lineRendering: [
{
color: "blue",
width: {
mappings: [
{
if: `id!~${focusZ}-.*`,
then: "1",
},
],
},
fillColor: {
mappings: [
{
if: `id!~${focusZ}-.*`,
then: "#00000000",
},
],
},
},
fillColor: {
mappings: [
{
if: `id!~${focusZ}-.*`,
then: "#00000000"
}
]
}
}],
],
pointRendering: [
{
location: ["point", "centroid"],
label: "{text}",
labelCss: "width: w-min",
labelCssClasses: "bg-white rounded px-2 items-center flex flex-col"
}
]
})
labelCssClasses: "bg-white rounded px-2 items-center flex flex-col",
},
],
}),
})
new ShowDataLayer(map, {
features: new StaticFeatureSource(focusTileFeature),
layer: new LayerConfig({
id: "focustile",
source: "special",
lineRendering: [{
color: "black"
}],
lineRendering: [
{
color: "black",
},
],
pointRendering: [
{
location: ["point", "centroid"],
label: "{txt}",
labelCss: "width: max-content",
labelCssClasses: "bg-white rounded px-2 flex"
}
]
})
labelCssClasses: "bg-white rounded px-2 flex",
},
],
}),
})
</script>
<div class="flex flex-col h-full max-h-leave-room">
<Checkbox selected={autoDownload}>Automatically download the basemap when browsing around</Checkbox>
<div>
If checked, MapComplete will automatically download the basemap to the cache for the area.
This results in bigger initial data loads, but requires less internet over the long run.
If you plan to visit a region with less connectivity, you can also select the area you want to download below.
<div class="max-h-leave-room flex h-full flex-col">
<Checkbox selected={autoDownload}>
Automatically download the basemap when browsing around
</Checkbox>
<div>
If checked, MapComplete will automatically download the basemap to the cache for the area. This
results in bigger initial data loads, but requires less internet over the long run. If you plan
to visit a region with less connectivity, you can also select the area you want to download
below.
</div>
{#if $installed === undefined}
<Loading />
{:else}
<div class="h-full overflow-auto pb-16">
<Accordion class="" inactiveClass="text-black">
<AccordionItem paddingDefault="p-2">
<div slot="header">Map</div>
<div class="relative leave-room">
<div class="rounded-lg absolute top-0 left-0 h-full w-full">
<div class="leave-room relative">
<div class="absolute left-0 top-0 h-full w-full rounded-lg">
<MaplibreMap {map} {mapProperties} />
</div>
<div
class="absolute top-0 left-0 h-full w-full flex flex-col justify-center items-center pointer-events-none">
<div class="w-16 h-32 mb-16"></div>
class="pointer-events-none absolute left-0 top-0 flex h-full w-full flex-col items-center justify-center"
>
<div class="mb-16 h-32 w-16" />
{#if $focusTileIsInstalling}
<div class="normal-background rounded-lg">
<Loading>
Data is being downloaded
</Loading>
<Loading>Data is being downloaded</Loading>
</div>
{:else}
<button class="primary pointer-events-auto" on:click={() => download()}
class:disabled={$focusTileIsInstalled}>
<DownloadIcon class="w-8 h-8" />
<button
class="primary pointer-events-auto"
on:click={() => download()}
class:disabled={$focusTileIsInstalled}
>
<DownloadIcon class="h-8 w-8" />
Download
</button>
{/if}
@ -176,21 +193,19 @@
</AccordionItem>
<AccordionItem paddingDefault="p-2">
<div slot="header">
Offline tile management
</div>
<div slot="header">Offline tile management</div>
<div class="leave-room">
{Utils.toHumanByteSize(Utils.sum($installed.map(area => area.size)))}
<button on:click={() => {
installed?.data?.forEach(area => del(area))
}}>
{Utils.toHumanByteSize(Utils.sum($installed.map((area) => area.size)))}
<button
on:click={() => {
installed?.data?.forEach((area) => del(area))
}}
>
<TrashIcon class="w-6" />
Delete all
</button>
<table class="w-full ">
<table class="w-full">
<tr>
<th>Name</th>
<th>Map generation date</th>
@ -198,12 +213,13 @@
<th>Zoom ranges</th>
<th>Actions</th>
</tr>
{#each ($installed ?? []) as area }
{#each $installed ?? [] as area}
<tr>
<td>{area.name}</td>
<td>{area.dataVersion}</td>
<td>{Utils.toHumanByteSize(area.size ?? -1)}</td>
<td>{area.minzoom}
<td>
{area.minzoom}
{#if area.maxzoom !== undefined}
- {area.maxzoom}
{:else}
@ -218,10 +234,8 @@
</td>
</tr>
{/each}
</table>
</div>
</AccordionItem>
</Accordion>
</div>
@ -229,10 +243,10 @@
</div>
<style>
.leave-room {
height: calc(100vh - 18rem);
overflow-x: auto;
width: 100%;
color: var(--foreground-color);
}
.leave-room {
height: calc(100vh - 18rem);
overflow-x: auto;
width: 100%;
color: var(--foreground-color);
}
</style>

View file

@ -23,7 +23,8 @@
)
let isAddNew = tags.mapD(
(t) => t?.id?.startsWith(LastClickFeatureSource.newPointElementId) ?? false, onDestroy
(t) => t?.id?.startsWith(LastClickFeatureSource.newPointElementId) ?? false,
onDestroy
)
export let layer: LayerConfig

View file

@ -10,7 +10,15 @@
export let range: OpeningRange[][]
const wt = Translations.t.general.weekdays
const weekdays: Translation[] = [wt.sunday, wt.monday, wt.tuesday, wt.wednesday, wt.thursday, wt.friday, wt.saturday]
const weekdays: Translation[] = [
wt.sunday,
wt.monday,
wt.tuesday,
wt.wednesday,
wt.thursday,
wt.friday,
wt.saturday,
]
let allTheSame = OH.weekdaysIdentical(range, 0, range.length - 1)
let today = new Date().getDay()
@ -24,8 +32,8 @@
const day = moment.startDate.getDay()
dayZero = day - i
}
function isToday(i:number ){
i = (i+dayZero) % 7
function isToday(i: number) {
i = (i + dayZero) % 7
return i === today
}
</script>
@ -37,16 +45,21 @@
<div class="ml-8">{moment.startDate.toLocaleTimeString()}</div>
{/each}
</div>
{:else if dayZero >= 0 } <!-- /*If dayZero == -1, then we got no valid values at all*/ -->
{:else if dayZero >= 0}
<!-- /*If dayZero == -1, then we got no valid values at all*/ -->
{#each range as moments, i (moments)}
<div class="flex gap-x-4 justify-between w-full px-2" class:interactive={isToday(i)} class:text-bold={isToday(i)} >
<div
class="flex w-full justify-between gap-x-4 px-2"
class:interactive={isToday(i)}
class:text-bold={isToday(i)}
>
<Tr t={weekdays[(i + dayZero) % 7]} />
{#if range[i].length > 0}
{#each moments as moment (moment)}
<div class="ml-8">{moment.startDate.toLocaleTimeString()}</div>
{/each}
{:else}
<Tr cls="italic subtle" t={Translations.t.general.points_in_time.closed}/>
<Tr cls="italic subtle" t={Translations.t.general.points_in_time.closed} />
{/if}
</div>
{/each}

View file

@ -1,5 +1,4 @@
<script lang="ts">
import CollectionTimeRange from "./CollectionTimeRange.svelte"
import opening_hours from "opening_hours"
import { OH } from "../OpeningHours/OpeningHours"
@ -14,26 +13,23 @@
let weekdays = ranges.slice(0, 5)
let weekend = ranges.slice(5, 7)
let everyDaySame = OH.weekdaysIdentical(ranges, 0, ranges.length - 1)
let weekdaysAndWeekendsSame = OH.weekdaysIdentical(weekdays, 0, 4) && OH.weekdaysIdentical(weekend, 0, 1)
let weekdaysAndWeekendsSame =
OH.weekdaysIdentical(weekdays, 0, 4) && OH.weekdaysIdentical(weekend, 0, 1)
const t = Translations.t.general.points_in_time
</script>
<div class="m-4 border">
{#if everyDaySame || !weekdaysAndWeekendsSame}
<CollectionTimeRange range={ranges}>
<Tr t={t.daily} />
</CollectionTimeRange>
{:else if times.isWeekStable()}
<div class="flex flex-col w-fit">
<div class="flex w-fit flex-col">
<CollectionTimeRange range={weekdays}>
<Tr t={t.weekdays} />
</CollectionTimeRange>
<CollectionTimeRange range={weekend}>
<Tr t={t.weekends} />
</CollectionTimeRange>
</div>
{:else}

View file

@ -33,7 +33,7 @@
* A switch that signals that the information should be downloaded.
* The actual 'download' code is _not_ implemented here
*/
export let downloadInformation : UIEventSource<boolean>
export let downloadInformation: UIEventSource<boolean>
export let collapsed: boolean
const t = Translations.t.external
@ -53,7 +53,7 @@
<LoginToggle {state} silentFail>
{#if !$sourceUrl || !$enableLogin}
<!-- empty block -->
{:else if !$downloadInformation}
{:else if !$downloadInformation}
<button on:click={() => downloadInformation.set(true)}>
Attempt to download information from the website {$sourceUrl}
</button>

View file

@ -138,13 +138,13 @@
</ul>
{#if diff.tr}
<div class="h-48 w-48">
<ChartJs config={ChartJsUtils.createConfigForTagRendering(
diff.tr, diff.features,{
<ChartJs
config={ChartJsUtils.createConfigForTagRendering(diff.tr, diff.features, {
groupToOtherCutoff: 0,
chartType: "pie",
sort: true,
}
)} />
})}
/>
</div>
{:else}
Could not create a graph - this item type has no associated question

View file

@ -136,29 +136,29 @@
</Popup>
{#if error}
{#if $online}
<div class="interactive flex h-80 w-60 flex-col items-center justify-center p-4 text-center">
{#if notFound}
<div class="alert flex items-center">
<TriangleOutline class="h-8 w-8 shrink-0" />
Not found
</div>
This image is probably incorrect or deleted.
{image.url}
<slot name="not-found-extra" />
{:else}
<div class="alert flex items-center">
<TriangleOutline class="h-8 w-8 shrink-0" />
<Tr t={Translations.t.image.loadingFailed} />
</div>
{#if image.provider.name.toLowerCase() === "mapillary" && $isInStrictMode}
<Tr t={Translations.t.image.mapillaryTrackingProtection} />
{:else if $isInStrictMode}
<Tr t={Translations.t.image.strictProtectionDetected} />
{image.provider.name}
<div class="subtle mt-8 text-sm">{image.url}</div>
<div class="interactive flex h-80 w-60 flex-col items-center justify-center p-4 text-center">
{#if notFound}
<div class="alert flex items-center">
<TriangleOutline class="h-8 w-8 shrink-0" />
Not found
</div>
This image is probably incorrect or deleted.
{image.url}
<slot name="not-found-extra" />
{:else}
<div class="alert flex items-center">
<TriangleOutline class="h-8 w-8 shrink-0" />
<Tr t={Translations.t.image.loadingFailed} />
</div>
{#if image.provider.name.toLowerCase() === "mapillary" && $isInStrictMode}
<Tr t={Translations.t.image.mapillaryTrackingProtection} />
{:else if $isInStrictMode}
<Tr t={Translations.t.image.strictProtectionDetected} />
{image.provider.name}
<div class="subtle mt-8 text-sm">{image.url}</div>
{/if}
{/if}
{/if}
</div>
</div>
{/if}
{:else if image.status !== undefined && image.status !== "ready" && image.status !== "hidden"}
<div class="flex h-80 w-60 flex-col justify-center p-4">

View file

@ -75,8 +75,8 @@
)
let selected = new UIEventSource<P4CPicture>(undefined)
let selectedAsFeature = selected.mapD((s) =>
[
let selectedAsFeature = selected.mapD(
(s) => [
<Feature<Point>>{
type: "Feature",
geometry: {
@ -89,13 +89,17 @@
rotation: s.direction,
},
},
], onDestroy)
let someLoading = imageState.state.mapD((stateRecord) =>
Object.values(stateRecord).some((v) => v === "loading"), onDestroy
],
onDestroy
)
let errors = imageState.state.mapD((stateRecord) =>
Object.keys(stateRecord).filter((k) => stateRecord[k] === "error"), onDestroy
let someLoading = imageState.state.mapD(
(stateRecord) => Object.values(stateRecord).some((v) => v === "loading"),
onDestroy
)
let errors = imageState.state.mapD(
(stateRecord) => Object.keys(stateRecord).filter((k) => stateRecord[k] === "error"),
onDestroy
)
let highlighted = new UIEventSource<string>(undefined)

View file

@ -9,11 +9,11 @@
export let failed: number
export let state: SpecialVisualizationState
const t = Translations.t.image
let dispatch = createEventDispatcher<{retry}>()
let dispatch = createEventDispatcher<{ retry }>()
</script>
<div class="alert flex">
<div class="flex flex-col items-start w-full items-center">
<div class="flex w-full flex-col items-start items-center">
{#if failed === 1}
<Tr t={t.upload.one.failed} />
{:else}
@ -23,8 +23,8 @@
<Tr cls="text-xs" t={t.upload.failReasonsAdvanced} />
{#if state}
<button class="primary pointer-events-auto" on:click={() => dispatch("retry")}>
<ArrowPath class="w-6"/>
<Tr t={Translations.t.general.retry}/>
<ArrowPath class="w-6" />
<Tr t={Translations.t.general.retry} />
</button>
{/if}
</div>

View file

@ -25,7 +25,7 @@
Number of images uploaded succesfully
*/
function getCount(input: Store<string[]>): Store<number> {
if(!input){
if (!input) {
return new ImmutableStore(0)
}
if (featureId == "*") {
@ -62,10 +62,9 @@
let progress = state.imageUploadManager.progressCurrentImage
function retry(){
function retry() {
state.imageUploadManager.uploadQueue()
}
</script>
{#if $debugging}
@ -99,7 +98,14 @@
{/if}
{#if $failed > dismissed}
<UploadFailedMessage failed={$failed} on:click={() => (dismissed = $failed)} on:retry={() => {retry()}} {state} />
<UploadFailedMessage
failed={$failed}
on:click={() => (dismissed = $failed)}
on:retry={() => {
retry()
}}
{state}
/>
{/if}
{#if showThankYou}

View file

@ -1,48 +1,58 @@
<script lang="ts">/**
* Multiple 'SingleCollectionTime'-rules togehter
*/
import { Stores, UIEventSource } from "../../../../Logic/UIEventSource"
import SingleCollectionTime from "./SingleCollectionTime.svelte"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Lists } from "../../../../Utils/Lists"
<script lang="ts">
/**
* Multiple 'SingleCollectionTime'-rules togehter
*/
import { Stores, UIEventSource } from "../../../../Logic/UIEventSource"
import SingleCollectionTime from "./SingleCollectionTime.svelte"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Lists } from "../../../../Utils/Lists"
export let value: UIEventSource<string>
let initialRules: string[] = Lists.noEmpty(value.data?.split(";")?.map(v => v.trim()))
let singleRules: UIEventSource<UIEventSource<string>[]> = new UIEventSource(
initialRules?.map(v => new UIEventSource(v)) ?? []
)
if(singleRules.data.length === 0){
singleRules.data.push(new UIEventSource(undefined))
}
singleRules.bindD(stores => Stores.concat(stores)).addCallbackAndRunD(subrules => {
console.log("Setting subrules", subrules)
value.set(Lists.noEmpty(subrules).join("; "))
})
function rm(rule: UIEventSource){
const index = singleRules.data.indexOf(rule)
singleRules.data.splice(index, 1)
singleRules.ping()
}
export let value: UIEventSource<string>
let initialRules: string[] = Lists.noEmpty(value.data?.split(";")?.map((v) => v.trim()))
let singleRules: UIEventSource<UIEventSource<string>[]> = new UIEventSource(
initialRules?.map((v) => new UIEventSource(v)) ?? []
)
if (singleRules.data.length === 0) {
singleRules.data.push(new UIEventSource(undefined))
}
singleRules
.bindD((stores) => Stores.concat(stores))
.addCallbackAndRunD((subrules) => {
console.log("Setting subrules", subrules)
value.set(Lists.noEmpty(subrules).join("; "))
})
function rm(rule: UIEventSource) {
const index = singleRules.data.indexOf(rule)
singleRules.data.splice(index, 1)
singleRules.ping()
}
</script>
<div class="interactive">
{#each $singleRules as rule}
<SingleCollectionTime value={rule}>
<svelte:fragment slot="right">
{#if $singleRules.length > 1}
<button on:click={() => { rm(rule) }} class="as-link">
<TrashIcon class="w-6 h-6" />
</button>
{/if}
</svelte:fragment>
</SingleCollectionTime>
<SingleCollectionTime value={rule}>
<svelte:fragment slot="right">
{#if $singleRules.length > 1}
<button
on:click={() => {
rm(rule)
}}
class="as-link"
>
<TrashIcon class="h-6 w-6" />
</button>
{/if}
</svelte:fragment>
</SingleCollectionTime>
{/each}
<button on:click={() => {singleRules.data.push(new UIEventSource(undefined)); singleRules.ping()}}>Add schedule
<button
on:click={() => {
singleRules.data.push(new UIEventSource(undefined))
singleRules.ping()
}}
>
Add schedule
</button>
</div>

View file

@ -1,5 +1,4 @@
<script lang="ts">
import TimeInput from "../TimeInput.svelte"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import Checkbox from "../../../Base/Checkbox.svelte"
@ -16,16 +15,28 @@
/*
Single rule for collection times, e.g. "Mo-Fr 10:00, 17:00"
*/
let weekdays: Translation[] = [wt.monday, wt.tuesday, wt.wednesday, wt.thursday, wt.friday, wt.saturday, wt.sunday, Translations.t.general.opening_hours.ph]
let weekdays: Translation[] = [
wt.monday,
wt.tuesday,
wt.wednesday,
wt.thursday,
wt.friday,
wt.saturday,
wt.sunday,
Translations.t.general.opening_hours.ph,
]
let initialTimes= Lists.noEmpty(value.data?.split(" ")?.[1]?.split(",")?.map(s => s.trim()) ?? [])
let values = new UIEventSource(initialTimes.map(t => new UIEventSource(t)))
if(values.data.length === 0){
let initialTimes = Lists.noEmpty(
value.data
?.split(" ")?.[1]
?.split(",")
?.map((s) => s.trim()) ?? []
)
let values = new UIEventSource(initialTimes.map((t) => new UIEventSource(t)))
if (values.data.length === 0) {
values.data.push(new UIEventSource(""))
}
let daysOfTheWeek = [...OH.days, "PH"]
let selectedDays = daysOfTheWeek.map(() => new UIEventSource(false))
@ -39,7 +50,6 @@
selectedDays[i]?.set(true)
}
} else {
let index = daysOfTheWeek.indexOf(initialDay)
if (index >= 0) {
selectedDays[index]?.set(true)
@ -47,19 +57,26 @@
}
}
let selectedDaysBound = Stores.concat(selectedDays)
.mapD(days => Lists.noNull(days.map((selected, i) => selected ? daysOfTheWeek[i] : undefined)))
let valuesConcat: Store<string[]> = values.bindD(values => Stores.concat(values))
.mapD(values => Lists.noEmpty(values))
valuesConcat.mapD(times => {
console.log(times)
times = Lists.noNull(times)
if (!times || times?.length === 0) {
return undefined
}
times?.sort(/*concatted, new array*/)
return (Lists.noEmpty(selectedDaysBound.data).join(",") + " " + times).trim()
}, [selectedDaysBound]).addCallbackAndRunD(v => value.set(v))
let selectedDaysBound = Stores.concat(selectedDays).mapD((days) =>
Lists.noNull(days.map((selected, i) => (selected ? daysOfTheWeek[i] : undefined)))
)
let valuesConcat: Store<string[]> = values
.bindD((values) => Stores.concat(values))
.mapD((values) => Lists.noEmpty(values))
valuesConcat
.mapD(
(times) => {
console.log(times)
times = Lists.noNull(times)
if (!times || times?.length === 0) {
return undefined
}
times?.sort(/*concatted, new array*/)
return (Lists.noEmpty(selectedDaysBound.data).join(",") + " " + times).trim()
},
[selectedDaysBound]
)
.addCallbackAndRunD((v) => value.set(v))
function selectWeekdays() {
for (let i = 0; i < 5; i++) {
@ -75,27 +92,28 @@
selectedDays[i].set(false)
}
}
</script>
<div class="rounded-xl my-2 p-2 low-interaction flex w-full justify-between flex-wrap">
<div class="low-interaction my-2 flex w-full flex-wrap justify-between rounded-xl p-2">
<div class="flex flex-col">
<div class="flex flex-wrap">
{#each $values as value, i}
<div class="flex mx-4 gap-x-1 items-center">
<div class="mx-4 flex items-center gap-x-1">
<TimeInput {value} />
{#if $values.length > 1}
<button class="as-link">
<TrashIcon class="w-6 h-6" />
<TrashIcon class="h-6 w-6" />
</button>
{/if}
</div>
{/each}
<button on:click={() => {values.data.push(new UIEventSource(undefined)); values.ping()}}>
<PlusCircle class="w-6 h-6" />
<button
on:click={() => {
values.data.push(new UIEventSource(undefined))
values.ping()
}}
>
<PlusCircle class="h-6 w-6" />
Add time
</button>
</div>
@ -108,14 +126,11 @@
</div>
{/each}
</div>
</div>
<div class="flex flex-wrap justify-between w-full">
<div class="flex w-full flex-wrap justify-between">
<div class="flex flex-wrap gap-x-4">
<button class="as-link text-sm" on:click={() => selectWeekdays()}>Select weekdays</button>
<button class="as-link text-sm" on:click={() => clearDays()}>Clear days</button>
</div>
<slot name="right" />
</div>

View file

@ -27,11 +27,14 @@
mla.allowMoving.setData(false)
mla.allowZooming.setData(false)
let rotation = new UIEventSource(value.data)
rotation.addCallbackD(rotation => {
const r = (rotation + mapProperties.rotation.data + 360) % 360
console.log("Setting value to", r)
value.setData(""+Math.floor(r))
}, [mapProperties.rotation])
rotation.addCallbackD(
(rotation) => {
const r = (rotation + mapProperties.rotation.data + 360) % 360
console.log("Setting value to", r)
value.setData("" + Math.floor(r))
},
[mapProperties.rotation]
)
let directionElem: HTMLElement | undefined
$: rotation.addCallbackAndRunD((degrees) => {
if (!directionElem?.style) {
@ -42,7 +45,6 @@
let mainElem: HTMLElement
function onPosChange(x: number, y: number) {
const rect = mainElem.getBoundingClientRect()
const dx = -(rect.left + rect.right) / 2 + x
@ -57,7 +59,7 @@
<div
bind:this={mainElem}
class="relative h-48 min-w-48 w-full cursor-pointer overflow-hidden rounded-xl"
class="relative h-48 w-full min-w-48 cursor-pointer overflow-hidden rounded-xl"
on:click={(e) => onPosChange(e.x, e.y)}
on:mousedown={(e) => {
isDown = true

View file

@ -85,7 +85,8 @@
},
]
},
[mapLocation], onDestroy
[mapLocation],
onDestroy
)
new ShowDataLayer(map, {

View file

@ -35,7 +35,7 @@
const state = new OpeningHoursState(value, prefix, postfix)
let expanded = new UIEventSource(false)
function abort(){
function abort() {
expanded.set(false)
}
</script>
@ -44,17 +44,19 @@
<OHTable value={state.normalOhs} />
<div class="absolute flex w-full justify-center" style="bottom: -3rem">
<button on:click={() => abort()}>
<XMark class="m-0 h-6 w-6"/>
<XMark class="m-0 h-6 w-6" />
<Tr t={Translations.t.general.cancel} />
</button>
<button class=" primary" on:click={() => expanded.set(false)}>
<Check class="m-0 h-6 w-6 shrink-0 p-0" color="white" />
<Tr t={Translations.t.general.confirm} />
</button>
</div>
<CloseButton class="absolute top-0 right-0 z-10" style="margin-top: -1.0rem" on:click={() => abort()} />
<CloseButton
class="absolute right-0 top-0 z-10"
style="margin-top: -1.0rem"
on:click={() => abort()}
/>
</Popup>
<button on:click={() => expanded.set(true)}>Pick opening hours</button>

View file

@ -78,7 +78,8 @@
previewDegrees.setData(beta + "°")
previewPercentage.setData(degreesToPercentage(beta))
},
[valuesign, beta], onDestroy
[valuesign, beta],
onDestroy
)
function onSave() {

View file

@ -44,8 +44,12 @@
})
)
let instanceOf: number[] = Lists.noNull((options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i)))
let notInstanceOf: number[] = Lists.noNull((options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i)))
let instanceOf: number[] = Lists.noNull(
(options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
let notInstanceOf: number[] = Lists.noNull(
(options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
let allowMultipleArg = options?.["multiple"] ?? "yes"
let allowMultiple = allowMultipleArg === "yes" || "" + allowMultipleArg === "true"

View file

@ -1,5 +1,4 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import type { ValidatorType } from "./Validators"
import Validators from "./Validators"

View file

@ -91,7 +91,7 @@
if (!range) {
return true
}
if(typeof canonicalValue === "string"){
if (typeof canonicalValue === "string") {
canonicalValue = Number(canonicalValue)
}
if (canonicalValue < range.warnBelow) {

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