forked from MapComplete/MapComplete
chore: automated housekeeping...
This commit is contained in:
parent
9cd7ad597d
commit
69ab755f29
520 changed files with 16616 additions and 13483 deletions
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>>
|
||||
|
|
|
@ -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([])
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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("&")
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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})`
|
||||
),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})`
|
||||
}
|
||||
|
|
|
@ -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) }))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[]> {
|
||||
|
|
|
@ -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> = {
|
||||
|
|
|
@ -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 }))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,14 +1,42 @@
|
|||
import { Utils } from "../../Utils"
|
||||
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
|
||||
export class ThemeMetaTagging {
|
||||
public static readonly themeName = "usersettings"
|
||||
public static readonly themeName = "usersettings"
|
||||
|
||||
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
|
||||
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
|
||||
feat.properties['__current_backgroun'] = 'initial_value'
|
||||
}
|
||||
}
|
||||
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
|
||||
feat.properties._description
|
||||
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
|
||||
?.at(1)
|
||||
)
|
||||
Utils.AddLazyProperty(
|
||||
feat.properties,
|
||||
"_d",
|
||||
() => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? ""
|
||||
)
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
|
||||
((feat) => {
|
||||
const e = document.createElement("div")
|
||||
e.innerHTML = feat.properties._d
|
||||
return Array.from(e.getElementsByTagName("a")).filter(
|
||||
(a) => a.href.match(/mastodon|en.osm.town/) !== null
|
||||
)[0]?.href
|
||||
})(feat)
|
||||
)
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
|
||||
((feat) => {
|
||||
const e = document.createElement("div")
|
||||
e.innerHTML = feat.properties._d
|
||||
return Array.from(e.getElementsByTagName("a")).filter(
|
||||
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
|
||||
)[0]?.href
|
||||
})(feat)
|
||||
)
|
||||
Utils.AddLazyProperty(
|
||||
feat.properties,
|
||||
"_mastodon_candidate",
|
||||
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
|
||||
)
|
||||
feat.properties["__current_backgroun"] = "initial_value"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>` +
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -174,7 +174,6 @@ export class MenuState {
|
|||
this._selectedElement.setData(undefined)
|
||||
return true
|
||||
}
|
||||
|
||||
} finally {
|
||||
this.isClosingAll = false
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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))
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
])
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -80,8 +80,6 @@ export class Unit {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
isApplicableToKey(key: string | undefined): boolean {
|
||||
if (key === undefined) {
|
||||
return false
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"} />
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -79,7 +79,6 @@ export default abstract class BaseUIElement {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
return el
|
||||
} catch (e) {
|
||||
const domExc = e as DOMException
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -51,7 +51,6 @@
|
|||
}
|
||||
})
|
||||
let hotkeys = Hotkeys._docs
|
||||
|
||||
</script>
|
||||
|
||||
<div class:h-0={!onlyLink} class:h-full={onlyLink} class="overflow-hidden">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -85,7 +85,8 @@
|
|||
},
|
||||
]
|
||||
},
|
||||
[mapLocation], onDestroy
|
||||
[mapLocation],
|
||||
onDestroy
|
||||
)
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -78,7 +78,8 @@
|
|||
previewDegrees.setData(beta + "°")
|
||||
previewPercentage.setData(degreesToPercentage(beta))
|
||||
},
|
||||
[valuesign, beta], onDestroy
|
||||
[valuesign, beta],
|
||||
onDestroy
|
||||
)
|
||||
|
||||
function onSave() {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { ValidatorType } from "./Validators"
|
||||
import Validators from "./Validators"
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue