forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
00c233a2eb
188 changed files with 4982 additions and 1745 deletions
|
@ -8,10 +8,13 @@ import { FeatureSource, WritableFeatureSource } from "../FeatureSource/FeatureSo
|
|||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import StaticFeatureSource, {
|
||||
WritableStaticFeatureSource,
|
||||
} from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import { MapProperties } from "../../Models/MapProperties"
|
||||
import { Orientation } from "../../Sensors/Orientation"
|
||||
|
||||
;("use strict")
|
||||
/**
|
||||
* The geolocation-handler takes a map-location and a geolocation state.
|
||||
* It'll move the map as appropriate given the state of the geolocation-API
|
||||
|
@ -43,13 +46,13 @@ export default class GeoLocationHandler {
|
|||
public readonly mapHasMoved: UIEventSource<Date | undefined> = new UIEventSource<
|
||||
Date | undefined
|
||||
>(undefined)
|
||||
private readonly selectedElement: UIEventSource<any>
|
||||
private readonly selectedElement: UIEventSource<Feature>
|
||||
private readonly mapProperties?: MapProperties
|
||||
private readonly gpsLocationHistoryRetentionTime?: UIEventSource<number>
|
||||
|
||||
constructor(
|
||||
geolocationState: GeoLocationState,
|
||||
selectedElement: UIEventSource<any>,
|
||||
selectedElement: UIEventSource<Feature>,
|
||||
mapProperties?: MapProperties,
|
||||
gpsLocationHistoryRetentionTime?: UIEventSource<number>
|
||||
) {
|
||||
|
@ -59,13 +62,12 @@ export default class GeoLocationHandler {
|
|||
this.mapProperties = mapProperties
|
||||
this.gpsLocationHistoryRetentionTime = gpsLocationHistoryRetentionTime
|
||||
// Did an interaction move the map?
|
||||
let self = this
|
||||
let initTime = new Date()
|
||||
mapLocation.addCallbackD((_) => {
|
||||
const initTime = new Date()
|
||||
mapLocation.addCallbackD(() => {
|
||||
if (new Date().getTime() - initTime.getTime() < 250) {
|
||||
return
|
||||
}
|
||||
self.mapHasMoved.setData(new Date())
|
||||
this.mapHasMoved.setData(new Date())
|
||||
return true // Unsubscribe
|
||||
})
|
||||
|
||||
|
@ -76,12 +78,12 @@ export default class GeoLocationHandler {
|
|||
this.mapHasMoved.setData(new Date())
|
||||
}
|
||||
|
||||
this.geolocationState.currentGPSLocation.addCallbackAndRunD((_) => {
|
||||
this.geolocationState.currentGPSLocation.addCallbackAndRunD(() => {
|
||||
const timeSinceLastRequest =
|
||||
(new Date().getTime() - geolocationState.requestMoment.data?.getTime() ?? 0) / 1000
|
||||
if (!this.mapHasMoved.data) {
|
||||
// The map hasn't moved yet; we received our first coordinates, so let's move there!
|
||||
self.MoveMapToCurrentLocation()
|
||||
this.MoveMapToCurrentLocation()
|
||||
}
|
||||
if (
|
||||
timeSinceLastRequest < Constants.zoomToLocationTimeout &&
|
||||
|
@ -90,12 +92,12 @@ export default class GeoLocationHandler {
|
|||
geolocationState.requestMoment.data?.getTime())
|
||||
) {
|
||||
// still within request time and the map hasn't moved since requesting to jump to the current location
|
||||
self.MoveMapToCurrentLocation()
|
||||
this.MoveMapToCurrentLocation()
|
||||
}
|
||||
|
||||
if (!this.geolocationState.allowMoving.data) {
|
||||
// Jup, the map is locked to the bound location: move automatically
|
||||
self.MoveMapToCurrentLocation(0)
|
||||
this.MoveMapToCurrentLocation(0)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
@ -183,7 +185,7 @@ export default class GeoLocationHandler {
|
|||
}
|
||||
|
||||
private initUserLocationTrail() {
|
||||
const features = LocalStorageSource.getParsed<Feature[]>("gps_location_history", [])
|
||||
const features = LocalStorageSource.getParsed<Feature<Point>[]>("gps_location_history", [])
|
||||
const now = new Date().getTime()
|
||||
features.data = features.data.filter((ff) => {
|
||||
if (ff.properties === undefined) {
|
||||
|
@ -230,7 +232,7 @@ export default class GeoLocationHandler {
|
|||
features.ping()
|
||||
})
|
||||
|
||||
this.historicalUserLocations = <any>new StaticFeatureSource(features)
|
||||
this.historicalUserLocations = new WritableStaticFeatureSource<Feature<Point>>(features)
|
||||
|
||||
const asLine = features.map((allPoints) => {
|
||||
if (allPoints === undefined || allPoints.length < 2) {
|
||||
|
|
|
@ -4,10 +4,12 @@ import { LocalStorageSource } from "../Web/LocalStorageSource"
|
|||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import Hash from "../Web/Hash"
|
||||
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
|
||||
import { OsmObject } from "../Osm/OsmObject"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { Utils } from "../../Utils"
|
||||
import { GeoLocationState } from "../State/GeoLocationState"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
|
||||
;("use strict")
|
||||
|
||||
/**
|
||||
* This actor is responsible to set the map location.
|
||||
|
@ -25,7 +27,11 @@ export default class InitialMapPositioning {
|
|||
public location: UIEventSource<{ lon: number; lat: number }>
|
||||
public useTerrain: Store<boolean>
|
||||
|
||||
constructor(layoutToUse: ThemeConfig, geolocationState: GeoLocationState) {
|
||||
constructor(
|
||||
layoutToUse: ThemeConfig,
|
||||
geolocationState: GeoLocationState,
|
||||
osmConnection: OsmConnection
|
||||
) {
|
||||
function localStorageSynced(
|
||||
key: string,
|
||||
deflt: number,
|
||||
|
@ -47,8 +53,6 @@ export default class InitialMapPositioning {
|
|||
return src
|
||||
}
|
||||
|
||||
const initialHash = Hash.hash.data
|
||||
|
||||
// -- Location control initialization
|
||||
this.zoom = localStorageSynced(
|
||||
"z",
|
||||
|
@ -72,6 +76,7 @@ export default class InitialMapPositioning {
|
|||
})
|
||||
this.useTerrain = new ImmutableStore<boolean>(layoutToUse.enableTerrain)
|
||||
|
||||
const initialHash = Hash.hash.data
|
||||
if (initialHash?.match(/^(node|way|relation)\/[0-9]+$/)) {
|
||||
// We pan to the selected element
|
||||
const [type, id] = initialHash.split("/")
|
||||
|
@ -88,6 +93,16 @@ export default class InitialMapPositioning {
|
|||
const [lat, lon] = osmObject.centerpoint()
|
||||
this.location.setData({ lon, lat })
|
||||
})
|
||||
} else if (layoutToUse.id === "notes" && initialHash?.match(/[0-9]+/)) {
|
||||
console.log("Loading note", initialHash)
|
||||
const noteId = Number(initialHash)
|
||||
if (osmConnection.isLoggedIn.data) {
|
||||
osmConnection.getNote(noteId).then((note) => {
|
||||
const [lon, lat] = note.geometry.coordinates
|
||||
console.log("Got note:", note)
|
||||
this.location.set({ lon, lat })
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
Constants.GeoIpServer &&
|
||||
lat.data === defaultLat &&
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Feature } from "geojson"
|
||||
import { Feature, Geometry } from "geojson"
|
||||
import { UpdatableFeatureSource } from "../FeatureSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
|
@ -7,6 +7,9 @@ import { Overpass } from "../../Osm/Overpass"
|
|||
import { Utils } from "../../../Utils"
|
||||
import { TagsFilter } from "../../Tags/TagsFilter"
|
||||
import { BBox } from "../../BBox"
|
||||
import { FeatureCollection } from "@turf/turf"
|
||||
import { OsmTags } from "../../../Models/OsmFeature"
|
||||
;("use strict")
|
||||
|
||||
/**
|
||||
* A wrapper around the 'Overpass'-object.
|
||||
|
@ -56,17 +59,13 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
|
|||
this.state = state
|
||||
this._isActive = options?.isActive ?? new ImmutableStore(true)
|
||||
this.padToZoomLevel = options?.padToTiles
|
||||
const self = this
|
||||
this._layersToDownload = options?.ignoreZoom
|
||||
? new ImmutableStore(state.layers)
|
||||
: state.zoom.map((zoom) => this.layersToDownload(zoom))
|
||||
|
||||
state.bounds.mapD(
|
||||
(_) => {
|
||||
self.updateAsyncIfNeeded()
|
||||
},
|
||||
[this._layersToDownload]
|
||||
)
|
||||
state.bounds.mapD(() => {
|
||||
this.updateAsyncIfNeeded()
|
||||
}, [this._layersToDownload])
|
||||
}
|
||||
|
||||
private layersToDownload(zoom: number): LayerConfig[] {
|
||||
|
@ -104,10 +103,11 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
|
|||
|
||||
/**
|
||||
* Download the relevant data from overpass. Attempt to use a different server if one fails; only downloads the relevant layers
|
||||
* Will always attempt to download, even is 'options.isActive.data' is 'false', the zoom level is incorrect, ...
|
||||
* @private
|
||||
*/
|
||||
public async updateAsync(overrideBounds?: BBox): Promise<void> {
|
||||
let data: any = undefined
|
||||
let data: FeatureCollection<Geometry, OsmTags> = undefined
|
||||
let lastUsed = 0
|
||||
const start = new Date()
|
||||
const layersToDownload = this._layersToDownload.data
|
||||
|
@ -116,8 +116,7 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
|
|||
return
|
||||
}
|
||||
|
||||
const self = this
|
||||
const overpassUrls = self.state.overpassUrl.data
|
||||
const overpassUrls = this.state.overpassUrl.data
|
||||
if (overpassUrls === undefined || overpassUrls.length === 0) {
|
||||
throw "Panic: overpassFeatureSource didn't receive any overpassUrls"
|
||||
}
|
||||
|
@ -140,10 +139,11 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
|
|||
return undefined
|
||||
}
|
||||
this.runningQuery.setData(true)
|
||||
console.trace("Overpass feature source: querying geojson")
|
||||
data = (await overpass.queryGeoJson(bounds))[0]
|
||||
} catch (e) {
|
||||
self.retries.data++
|
||||
self.retries.ping()
|
||||
this.retries.data++
|
||||
this.retries.ping()
|
||||
console.error(`QUERY FAILED due to`, e)
|
||||
|
||||
await Utils.waitFor(1000)
|
||||
|
@ -153,12 +153,12 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
|
|||
console.log("Trying next time with", overpassUrls[lastUsed])
|
||||
} else {
|
||||
lastUsed = 0
|
||||
self.timeout.setData(self.retries.data * 5)
|
||||
this.timeout.setData(this.retries.data * 5)
|
||||
|
||||
while (self.timeout.data > 0) {
|
||||
while (this.timeout.data > 0) {
|
||||
await Utils.waitFor(1000)
|
||||
self.timeout.data--
|
||||
self.timeout.ping()
|
||||
this.timeout.data--
|
||||
this.timeout.ping()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -180,14 +180,14 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
|
|||
timeNeeded,
|
||||
"seconds"
|
||||
)
|
||||
self.features.setData(data.features)
|
||||
this.features.setData(data.features)
|
||||
this._lastQueryBBox = bounds
|
||||
this._lastRequestedLayers = layersToDownload
|
||||
} catch (e) {
|
||||
console.error("Got the overpass response, but could not process it: ", e, e.stack)
|
||||
} finally {
|
||||
self.retries.setData(0)
|
||||
self.runningQuery.setData(false)
|
||||
this.retries.setData(0)
|
||||
this.runningQuery.setData(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { FeatureSource } from "../FeatureSource"
|
||||
import { ImmutableStore, Store } from "../../UIEventSource"
|
||||
import { FeatureSource, WritableFeatureSource } from "../FeatureSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
|
||||
import { Feature } from "geojson"
|
||||
|
||||
;("use strict")
|
||||
/**
|
||||
* A simple, read only feature store.
|
||||
*/
|
||||
|
@ -30,3 +31,29 @@ export default class StaticFeatureSource<T extends Feature = Feature> implements
|
|||
return new StaticFeatureSource(geojson)
|
||||
}
|
||||
}
|
||||
|
||||
export class WritableStaticFeatureSource<T extends Feature = Feature>
|
||||
implements WritableFeatureSource<T>
|
||||
{
|
||||
public readonly features: UIEventSource<T[]> = undefined
|
||||
|
||||
constructor(features: UIEventSource<T[]> | T[] | { features: T[] } | { features: Store<T[]> }) {
|
||||
if (features === undefined) {
|
||||
throw "Static feature source received undefined as source"
|
||||
}
|
||||
|
||||
let feats: T[] | UIEventSource<T[]>
|
||||
|
||||
if (features["features"]) {
|
||||
feats = features["features"]
|
||||
} else {
|
||||
feats = <T[] | UIEventSource<T[]>>features
|
||||
}
|
||||
|
||||
if (Array.isArray(feats)) {
|
||||
this.features = new UIEventSource<T[]>(feats)
|
||||
} else {
|
||||
this.features = feats
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,8 +24,6 @@ export default class ThemeSource extends FeatureSourceMerger {
|
|||
*/
|
||||
public readonly isLoading: Store<boolean>
|
||||
|
||||
private readonly supportsForceDownload: UpdatableFeatureSource[]
|
||||
|
||||
public static readonly fromCacheZoomLevel = 15
|
||||
|
||||
/**
|
||||
|
@ -44,8 +42,6 @@ export default class ThemeSource extends FeatureSourceMerger {
|
|||
mvtAvailableLayers: Set<string>,
|
||||
fullNodeDatabaseSource?: FullNodeDatabaseSource
|
||||
) {
|
||||
const supportsForceDownload: UpdatableFeatureSource[] = []
|
||||
|
||||
const { bounds, zoom } = mapProperties
|
||||
// remove all 'special' layers
|
||||
layers = layers.filter((layer) => layer.source !== null && layer.source !== undefined)
|
||||
|
@ -95,7 +91,6 @@ export default class ThemeSource extends FeatureSourceMerger {
|
|||
)
|
||||
overpassSource = ThemeSource.setupOverpass(osmLayers, bounds, zoom, featureSwitches)
|
||||
nonMvtSources.push(overpassSource)
|
||||
supportsForceDownload.push(overpassSource)
|
||||
}
|
||||
|
||||
function setIsLoading() {
|
||||
|
@ -110,7 +105,6 @@ export default class ThemeSource extends FeatureSourceMerger {
|
|||
ThemeSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
|
||||
)
|
||||
|
||||
const downloadAllBounds: UIEventSource<BBox> = new UIEventSource<BBox>(undefined)
|
||||
const downloadAll = new OverpassFeatureSource(
|
||||
{
|
||||
layers: layers.filter((l) => l.isNormal()),
|
||||
|
@ -123,6 +117,7 @@ export default class ThemeSource extends FeatureSourceMerger {
|
|||
},
|
||||
{
|
||||
ignoreZoom: true,
|
||||
isActive: new ImmutableStore(false),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -135,13 +130,8 @@ export default class ThemeSource extends FeatureSourceMerger {
|
|||
)
|
||||
|
||||
this.isLoading = isLoading
|
||||
supportsForceDownload.push(...geojsonSources)
|
||||
supportsForceDownload.push(...mvtSources) // Non-mvt sources are handled by overpass
|
||||
|
||||
this._mapBounds = mapProperties.bounds
|
||||
this._downloadAll = downloadAll
|
||||
|
||||
this.supportsForceDownload = supportsForceDownload
|
||||
this._mapBounds = mapProperties.bounds
|
||||
}
|
||||
|
||||
private static setupMvtSource(
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { BBox } from "./BBox"
|
||||
import * as turf from "@turf/turf"
|
||||
import { AllGeoJSON, booleanWithin, Coord, Lines } from "@turf/turf"
|
||||
import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf"
|
||||
import {
|
||||
Feature,
|
||||
FeatureCollection,
|
||||
GeoJSON,
|
||||
Geometry,
|
||||
LineString,
|
||||
MultiLineString,
|
||||
MultiPolygon,
|
||||
|
@ -15,6 +14,9 @@ import {
|
|||
} from "geojson"
|
||||
import { Tiles } from "../Models/TileRange"
|
||||
import { Utils } from "../Utils"
|
||||
import { NearestPointOnLine } from "@turf/nearest-point-on-line"
|
||||
|
||||
;("use strict")
|
||||
|
||||
export class GeoOperations {
|
||||
private static readonly _earthRadius = 6378137
|
||||
|
@ -52,15 +54,21 @@ export class GeoOperations {
|
|||
/**
|
||||
* Create a union between two features
|
||||
*/
|
||||
public static union(f0: Feature, f1: Feature): Feature<Polygon | MultiPolygon> | null {
|
||||
return turf.union(<any>f0, <any>f1)
|
||||
public static union(
|
||||
f0: Feature<Polygon | MultiPolygon>,
|
||||
f1: Feature<Polygon | MultiPolygon>
|
||||
): Feature<Polygon | MultiPolygon> | null {
|
||||
return turf.union(f0, f1)
|
||||
}
|
||||
|
||||
public static intersect(f0: Feature, f1: Feature): Feature<Polygon | MultiPolygon> | null {
|
||||
return turf.intersect(<any>f0, <any>f1)
|
||||
public static intersect(
|
||||
f0: Feature<Polygon | MultiPolygon>,
|
||||
f1: Feature<Polygon | MultiPolygon>
|
||||
): Feature<Polygon | MultiPolygon> | null {
|
||||
return turf.intersect(f0, f1)
|
||||
}
|
||||
|
||||
static surfaceAreaInSqMeters(feature: any) {
|
||||
static surfaceAreaInSqMeters(feature: Feature<Polygon | MultiPolygon>): number {
|
||||
return turf.area(feature)
|
||||
}
|
||||
|
||||
|
@ -68,19 +76,31 @@ export class GeoOperations {
|
|||
* Converts a GeoJson feature to a point GeoJson feature
|
||||
* @param feature
|
||||
*/
|
||||
static centerpoint(feature: any): Feature<Point> {
|
||||
const newFeature: Feature<Point> = turf.center(feature)
|
||||
static centerpoint(feature: Feature): Feature<Point> {
|
||||
const newFeature: Feature<Point> = turf.center(<turf.Feature>feature)
|
||||
newFeature.properties = feature.properties
|
||||
newFeature.id = feature.id
|
||||
return newFeature
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [lon,lat] coordinates
|
||||
* Returns [lon,lat] coordinates.
|
||||
* @param feature
|
||||
*
|
||||
* GeoOperations.centerpointCoordinates(undefined) // => undefined
|
||||
*/
|
||||
static centerpointCoordinates(feature: undefined | null): undefined
|
||||
static centerpointCoordinates(
|
||||
feature: AllGeoJSON | GeoJSON | undefined
|
||||
): [number, number] | undefined
|
||||
static centerpointCoordinates(
|
||||
feature: NonNullable<AllGeoJSON> | NonNullable<GeoJSON>
|
||||
): NonNullable<[number, number]>
|
||||
static centerpointCoordinates(feature: AllGeoJSON | GeoJSON): [number, number] {
|
||||
return <[number, number]>turf.center(<any>feature).geometry.coordinates
|
||||
if (feature === undefined || feature === null) {
|
||||
return undefined
|
||||
}
|
||||
return <[number, number]>turf.center(<turf.Feature>feature).geometry.coordinates
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -132,11 +152,14 @@ export class GeoOperations {
|
|||
* const overlap0 = GeoOperations.calculateOverlap(line0, [polygon]);
|
||||
* overlap.length // => 1
|
||||
*/
|
||||
static calculateOverlap(feature: any, otherFeatures: any[]): { feat: any; overlap: number }[] {
|
||||
static calculateOverlap(
|
||||
feature: Feature,
|
||||
otherFeatures: Feature[]
|
||||
): { feat: Feature; overlap: number }[] {
|
||||
const featureBBox = BBox.get(feature)
|
||||
const result: { feat: any; overlap: number }[] = []
|
||||
const result: { feat: Feature; overlap: number }[] = []
|
||||
if (feature.geometry.type === "Point") {
|
||||
const coor = feature.geometry.coordinates
|
||||
const coor = <[number, number]>feature.geometry.coordinates
|
||||
for (const otherFeature of otherFeatures) {
|
||||
if (
|
||||
feature.properties.id !== undefined &&
|
||||
|
@ -189,7 +212,7 @@ export class GeoOperations {
|
|||
}
|
||||
|
||||
if (otherFeature.geometry.type === "Point") {
|
||||
if (this.inside(otherFeature, feature)) {
|
||||
if (this.inside(<Feature<Point>>otherFeature, feature)) {
|
||||
result.push({ feat: otherFeature, overlap: undefined })
|
||||
}
|
||||
continue
|
||||
|
@ -255,9 +278,10 @@ export class GeoOperations {
|
|||
const y: number = pointCoordinate[1]
|
||||
|
||||
if (feature.geometry.type === "MultiPolygon") {
|
||||
const coordinatess = feature.geometry.coordinates
|
||||
const coordinatess: [number, number][][][] = <[number, number][][][]>(
|
||||
feature.geometry.coordinates
|
||||
)
|
||||
for (const coordinates of coordinatess) {
|
||||
// @ts-ignore
|
||||
const inThisPolygon = GeoOperations.pointInPolygonCoordinates(x, y, coordinates)
|
||||
if (inThisPolygon) {
|
||||
return true
|
||||
|
@ -267,24 +291,30 @@ export class GeoOperations {
|
|||
}
|
||||
|
||||
if (feature.geometry.type === "Polygon") {
|
||||
// @ts-ignore
|
||||
return GeoOperations.pointInPolygonCoordinates(x, y, feature.geometry.coordinates)
|
||||
return GeoOperations.pointInPolygonCoordinates(
|
||||
x,
|
||||
y,
|
||||
<[number, number][][]>feature.geometry.coordinates
|
||||
)
|
||||
}
|
||||
|
||||
throw "GeoOperations.inside: unsupported geometry type " + feature.geometry.type
|
||||
}
|
||||
|
||||
static lengthInMeters(feature: any) {
|
||||
static lengthInMeters(feature: Feature): number {
|
||||
return turf.length(feature) * 1000
|
||||
}
|
||||
|
||||
static buffer(feature: any, bufferSizeInMeter: number) {
|
||||
static buffer(
|
||||
feature: Feature,
|
||||
bufferSizeInMeter: number
|
||||
): Feature<Polygon | MultiPolygon> | FeatureCollection<Polygon | MultiPolygon> {
|
||||
return turf.buffer(feature, bufferSizeInMeter / 1000, {
|
||||
units: "kilometers",
|
||||
})
|
||||
}
|
||||
|
||||
static bbox(feature: Feature | FeatureCollection): Feature<LineString, {}> {
|
||||
static bbox(feature: Feature | FeatureCollection): Feature<LineString> {
|
||||
const [lon, lat, lon0, lat0] = turf.bbox(feature)
|
||||
return {
|
||||
type: "Feature",
|
||||
|
@ -316,17 +346,8 @@ export class GeoOperations {
|
|||
public static nearestPoint(
|
||||
way: Feature<LineString>,
|
||||
point: [number, number]
|
||||
): Feature<
|
||||
Point,
|
||||
{
|
||||
index: number
|
||||
dist: number
|
||||
location: number
|
||||
}
|
||||
> {
|
||||
return <any>(
|
||||
turf.nearestPointOnLine(<Feature<LineString>>way, point, { units: "kilometers" })
|
||||
)
|
||||
): NearestPointOnLine {
|
||||
return turf.nearestPointOnLine(<Feature<LineString>>way, point, { units: "kilometers" })
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -344,18 +365,32 @@ export class GeoOperations {
|
|||
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
|
||||
): Feature<LineString | MultiLineString> {
|
||||
if (way.geometry.type === "Polygon") {
|
||||
way = { ...way }
|
||||
way.geometry = { ...way.geometry }
|
||||
way.geometry.type = "LineString"
|
||||
way.geometry.coordinates = (<Polygon>way.geometry).coordinates[0]
|
||||
} else if (way.geometry.type === "MultiPolygon") {
|
||||
way = { ...way }
|
||||
way.geometry = { ...way.geometry }
|
||||
way.geometry.type = "MultiLineString"
|
||||
way.geometry.coordinates = (<MultiPolygon>way.geometry).coordinates[0]
|
||||
return <Feature<LineString>>{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: way.geometry.coordinates[0],
|
||||
},
|
||||
properties: way.properties,
|
||||
}
|
||||
}
|
||||
|
||||
return <any>way
|
||||
if (way.geometry.type === "MultiPolygon") {
|
||||
return <Feature<MultiLineString>>{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "MultiLineString",
|
||||
coordinates: way.geometry.coordinates[0],
|
||||
},
|
||||
properties: way.properties,
|
||||
}
|
||||
}
|
||||
if (way.geometry.type === "LineString") {
|
||||
return <Feature<LineString>>way
|
||||
}
|
||||
if (way.geometry.type === "MultiLineString") {
|
||||
return <Feature<MultiLineString>>way
|
||||
}
|
||||
throw "Invalid geometry to create a way from this"
|
||||
}
|
||||
|
||||
public static toCSV(
|
||||
|
@ -394,9 +429,6 @@ export class GeoOperations {
|
|||
for (const feature of _features) {
|
||||
const properties = feature.properties
|
||||
for (const key in properties) {
|
||||
if (!properties.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
addH(key)
|
||||
}
|
||||
}
|
||||
|
@ -602,7 +634,7 @@ export class GeoOperations {
|
|||
* const copy = GeoOperations.removeOvernoding(feature)
|
||||
* expect(copy.geometry.coordinates[0]).deep.equal([[4.477944199999975,51.02783550000022],[4.477987899999996,51.027818800000034],[4.478004500000021,51.02783399999988],[4.478025499999962,51.02782489999994],[4.478079099999993,51.027873899999896],[4.47801040000006,51.027903799999955],[4.477944199999975,51.02783550000022]])
|
||||
*/
|
||||
static removeOvernoding(feature: any) {
|
||||
static removeOvernoding(feature: Feature<LineString | Polygon>) {
|
||||
if (feature.geometry.type !== "LineString" && feature.geometry.type !== "Polygon") {
|
||||
throw "Overnode removal is only supported on linestrings and polygons"
|
||||
}
|
||||
|
@ -613,10 +645,10 @@ export class GeoOperations {
|
|||
}
|
||||
let coordinates: [number, number][]
|
||||
if (feature.geometry.type === "LineString") {
|
||||
coordinates = [...feature.geometry.coordinates]
|
||||
coordinates = <[number, number][]>[...feature.geometry.coordinates]
|
||||
copy.geometry.coordinates = coordinates
|
||||
} else {
|
||||
coordinates = [...feature.geometry.coordinates[0]]
|
||||
coordinates = <[number, number][]>[...feature.geometry.coordinates[0]]
|
||||
copy.geometry.coordinates[0] = coordinates
|
||||
}
|
||||
|
||||
|
@ -663,7 +695,7 @@ export class GeoOperations {
|
|||
|
||||
public static along(a: Coord, b: Coord, distanceMeter: number): Coord {
|
||||
return turf.along(
|
||||
<any>{
|
||||
<Feature<LineString>>{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
|
@ -705,8 +737,8 @@ export class GeoOperations {
|
|||
* GeoOperations.completelyWithin(park, pond) // => false
|
||||
*/
|
||||
static completelyWithin(
|
||||
feature: Feature<Geometry, any>,
|
||||
possiblyEnclosingFeature: Feature<Polygon | MultiPolygon, any>
|
||||
feature: Feature,
|
||||
possiblyEnclosingFeature: Feature<Polygon | MultiPolygon>
|
||||
): boolean {
|
||||
return booleanWithin(feature, possiblyEnclosingFeature)
|
||||
}
|
||||
|
@ -1169,7 +1201,7 @@ export class GeoOperations {
|
|||
|
||||
// Calculate the length of the intersection
|
||||
|
||||
let intersectionPoints = turf.lineIntersect(feature, otherFeature)
|
||||
const intersectionPoints = turf.lineIntersect(feature, otherFeature)
|
||||
if (intersectionPoints.features.length == 0) {
|
||||
// No intersections.
|
||||
// If one point is inside of the polygon, all points are
|
||||
|
@ -1182,7 +1214,7 @@ export class GeoOperations {
|
|||
|
||||
return null
|
||||
}
|
||||
let intersectionPointsArray = intersectionPoints.features.map((d) => {
|
||||
const intersectionPointsArray = intersectionPoints.features.map((d) => {
|
||||
return d.geometry.coordinates
|
||||
})
|
||||
|
||||
|
@ -1204,7 +1236,7 @@ export class GeoOperations {
|
|||
}
|
||||
}
|
||||
|
||||
let intersection = turf.lineSlice(
|
||||
const intersection = turf.lineSlice(
|
||||
turf.point(intersectionPointsArray[0]),
|
||||
turf.point(intersectionPointsArray[1]),
|
||||
feature
|
||||
|
|
|
@ -107,12 +107,15 @@ export class ImageUploadManager {
|
|||
* @param file a jpg file to upload
|
||||
* @param tagsStore The tags of the feature
|
||||
* @param targetKey Use this key to save the attribute under. Default: 'image'
|
||||
* @param noblur if true, then the api call will indicate that the image is already blurred. The server won't apply blurring in this case
|
||||
* @param feature the feature this image is about. Will be used as fallback to get the GPS-coordinates
|
||||
*/
|
||||
public async uploadImageAndApply(
|
||||
file: File,
|
||||
tagsStore: UIEventSource<OsmTags>,
|
||||
targetKey: string,
|
||||
noblur: boolean
|
||||
noblur: boolean,
|
||||
feature: Feature
|
||||
): Promise<void> {
|
||||
const canBeUploaded = this.canBeUploaded(file)
|
||||
if (canBeUploaded !== true) {
|
||||
|
@ -130,7 +133,8 @@ export class ImageUploadManager {
|
|||
author,
|
||||
file,
|
||||
targetKey,
|
||||
noblur
|
||||
noblur,
|
||||
feature
|
||||
)
|
||||
if (!uploadResult) {
|
||||
return
|
||||
|
@ -157,7 +161,7 @@ export class ImageUploadManager {
|
|||
blob: File,
|
||||
targetKey: string | undefined,
|
||||
noblur: boolean,
|
||||
feature?: Feature,
|
||||
feature: Feature,
|
||||
ignoreGps: boolean = false
|
||||
): Promise<UploadResult> {
|
||||
this.increaseCountFor(this._uploadStarted, featureId)
|
||||
|
@ -168,9 +172,22 @@ export class ImageUploadManager {
|
|||
if (this._gps.data && !ignoreGps) {
|
||||
location = [this._gps.data.longitude, this._gps.data.latitude]
|
||||
}
|
||||
if (location === undefined || location?.some((l) => l === undefined)) {
|
||||
{
|
||||
feature ??= this._indexedFeatures.featuresById.data.get(featureId)
|
||||
location = GeoOperations.centerpointCoordinates(feature)
|
||||
if (feature === undefined) {
|
||||
throw "ImageUploadManager: no feature given and no feature found in the indexedFeature. Cannot upload this image"
|
||||
}
|
||||
const featureCenterpoint = GeoOperations.centerpointCoordinates(feature)
|
||||
if (
|
||||
location === undefined ||
|
||||
location?.some((l) => l === undefined) ||
|
||||
GeoOperations.distanceBetween(location, featureCenterpoint) > 150
|
||||
) {
|
||||
/* GPS location is either unknown or very far away from the photographed location.
|
||||
* Default to the centerpoint
|
||||
*/
|
||||
location = featureCenterpoint
|
||||
}
|
||||
}
|
||||
try {
|
||||
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(
|
||||
|
|
|
@ -130,7 +130,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
}
|
||||
|
||||
public async getInfo(hash: string): Promise<ProvidedImage> {
|
||||
return await this.getInfoFor(hash).then((r) => this.featureToImage(<any>r))
|
||||
return await this.getInfoFor(hash).then((r) => this.featureToImage(<any>r))
|
||||
}
|
||||
|
||||
getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
|
||||
|
@ -234,8 +234,19 @@ export class PanoramaxUploader implements ImageUploader {
|
|||
) {
|
||||
lat = exifLat
|
||||
lon = exifLon
|
||||
if (tags?.GPSLatitudeRef?.value?.[0] === "S") {
|
||||
lat *= -1
|
||||
}
|
||||
if (tags?.GPSLongitudeRef?.value?.[0] === "W") {
|
||||
lon *= -1
|
||||
}
|
||||
}
|
||||
const [date, time] =( tags.DateTime.value[0] ?? tags.DateTimeOriginal.value[0] ?? tags.GPSDateStamp ?? tags["Date Created"]).split(" ")
|
||||
const [date, time] = (
|
||||
tags.DateTime.value[0] ??
|
||||
tags.DateTimeOriginal.value[0] ??
|
||||
tags.GPSDateStamp ??
|
||||
tags["Date Created"]
|
||||
).split(" ")
|
||||
const exifDatetime = new Date(date.replaceAll(":", "-") + "T" + time)
|
||||
if (exifDatetime.getFullYear() === 1970) {
|
||||
// The data probably got reset to the epoch
|
||||
|
|
|
@ -1,9 +1,24 @@
|
|||
import Constants from "../Models/Constants"
|
||||
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
|
||||
|
||||
export interface MaprouletteTask {
|
||||
name: string
|
||||
description: string
|
||||
instruction: string
|
||||
}
|
||||
export const maprouletteStatus = [
|
||||
"Open",
|
||||
"Fixed",
|
||||
"False_positive",
|
||||
"Skipped",
|
||||
"Deleted",
|
||||
"Already fixed",
|
||||
"Too_Hard",
|
||||
"Disabled",
|
||||
] as const
|
||||
|
||||
export type MaprouletteStatus = (typeof maprouletteStatus)[number]
|
||||
|
||||
export default class Maproulette {
|
||||
public static readonly defaultEndpoint = "https://maproulette.org/api/v2"
|
||||
|
||||
|
@ -16,16 +31,6 @@ export default class Maproulette {
|
|||
public static readonly STATUS_TOO_HARD = 6
|
||||
public static readonly STATUS_DISABLED = 9
|
||||
|
||||
public static readonly STATUS_MEANING = {
|
||||
0: "Open",
|
||||
1: "Fixed",
|
||||
2: "False_positive",
|
||||
3: "Skipped",
|
||||
4: "Deleted",
|
||||
5: "Already fixed",
|
||||
6: "Too_Hard",
|
||||
9: "Disabled",
|
||||
}
|
||||
public static singleton = new Maproulette()
|
||||
/*
|
||||
* The API endpoint to use
|
||||
|
@ -59,12 +64,11 @@ export default class Maproulette {
|
|||
if (code === "Created") {
|
||||
return Maproulette.STATUS_OPEN
|
||||
}
|
||||
for (let i = 0; i < 9; i++) {
|
||||
if (Maproulette.STATUS_MEANING["" + i] === code) {
|
||||
return i
|
||||
}
|
||||
const i = maprouletteStatus.indexOf(<any>code)
|
||||
if (i < 0) {
|
||||
return undefined
|
||||
}
|
||||
return undefined
|
||||
return i
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -78,6 +82,7 @@ export default class Maproulette {
|
|||
async closeTask(
|
||||
taskId: number,
|
||||
status = Maproulette.STATUS_FIXED,
|
||||
state: SpecialVisualizationState,
|
||||
options?: {
|
||||
comment?: string
|
||||
tags?: string
|
||||
|
@ -86,13 +91,16 @@ export default class Maproulette {
|
|||
}
|
||||
): Promise<void> {
|
||||
console.log("Maproulette: setting", `${this.endpoint}/task/${taskId}/${status}`, options)
|
||||
options ??= {}
|
||||
const userdetails = state.osmConnection.userDetails.data
|
||||
options.tags = `MapComplete MapComplete:${state.theme.id}; userid: ${userdetails?.uid}; username: ${userdetails?.name}`
|
||||
const response = await fetch(`${this.endpoint}/task/${taskId}/${status}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
apiKey: this.apiKey,
|
||||
},
|
||||
body: options !== undefined ? JSON.stringify(options) : undefined,
|
||||
body: JSON.stringify(options),
|
||||
})
|
||||
if (response.status !== 204) {
|
||||
console.log(`Failed to close task: ${response.status}`)
|
||||
|
|
|
@ -2,7 +2,7 @@ import ChangeTagAction from "./ChangeTagAction"
|
|||
import { Tag } from "../../Tags/Tag"
|
||||
import OsmChangeAction from "./OsmChangeAction"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import { Store, UIEventSource } from "../../UIEventSource"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
|
||||
export default class LinkImageAction extends OsmChangeAction {
|
||||
private readonly _proposedKey: "image" | "mapillary" | "wiki_commons" | string
|
||||
|
|
|
@ -600,7 +600,7 @@ export class Changes {
|
|||
" trying again before dropping it from the changes (" +
|
||||
e +
|
||||
")"
|
||||
this._reportError(msg)
|
||||
// this._reportError(msg) // We don't report this yet, might be a temporary fluke
|
||||
const osmObj = await downloader.DownloadObjectAsync(id, 0)
|
||||
return { id, osmObj }
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { LocalStorageSource } from "../Web/LocalStorageSource"
|
|||
import { AuthConfig } from "./AuthConfig"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { AndroidPolyfill } from "../Web/AndroidPolyfill"
|
||||
import { Feature, Point } from "geojson"
|
||||
|
||||
interface OsmUserInfo {
|
||||
id: number
|
||||
|
@ -41,6 +42,39 @@ export default class UserDetails {
|
|||
|
||||
export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable"
|
||||
|
||||
interface CapabilityResult {
|
||||
version: "0.6" | string
|
||||
generator: "OpenStreetMap server" | string
|
||||
copyright: "OpenStreetMap and contributors" | string
|
||||
attribution: "http://www.openstreetmap.org/copyright" | string
|
||||
license: "http://opendatacommons.org/licenses/odbl/1-0/" | string
|
||||
api: {
|
||||
version: { minimum: "0.6"; maximum: "0.6" }
|
||||
area: { maximum: 0.25 | number }
|
||||
note_area: { maximum: 25 | number }
|
||||
tracepoints: { per_page: 5000 | number }
|
||||
waynodes: { maximum: 2000 | number }
|
||||
relationmembers: { maximum: 32000 | number }
|
||||
changesets: {
|
||||
maximum_elements: 10000 | number
|
||||
default_query_limit: 100 | number
|
||||
maximum_query_limit: 100 | number
|
||||
}
|
||||
notes: { default_query_limit: 100 | number; maximum_query_limit: 10000 | number }
|
||||
timeout: { seconds: 300 | number }
|
||||
status: {
|
||||
database: OsmServiceState
|
||||
api: OsmServiceState
|
||||
gpx: OsmServiceState
|
||||
}
|
||||
}
|
||||
policy: {
|
||||
imagery: {
|
||||
blacklist: { regex: string }[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OsmConnection {
|
||||
public auth: osmAuth
|
||||
public userDetails: UIEventSource<UserDetails>
|
||||
|
@ -424,6 +458,10 @@ export class OsmConnection {
|
|||
return id
|
||||
}
|
||||
|
||||
public async getNote(id: number): Promise<Feature<Point>> {
|
||||
return JSON.parse(await this.get("notes/" + id + ".json"))
|
||||
}
|
||||
|
||||
public static GpxTrackVisibility = ["private", "public", "trackable", "identifiable"] as const
|
||||
|
||||
public async uploadGpxTrack(
|
||||
|
@ -466,7 +504,7 @@ export class OsmConnection {
|
|||
(options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
|
||||
"\"\r\nContent-Type: application/gpx+xml",
|
||||
}
|
||||
|
||||
user
|
||||
const boundary = "987654"
|
||||
|
||||
let body = ""
|
||||
|
@ -608,20 +646,26 @@ export class OsmConnection {
|
|||
return parsed
|
||||
}
|
||||
|
||||
private async FetchCapabilities(): Promise<{ api: OsmServiceState; gpx: OsmServiceState }> {
|
||||
private async FetchCapabilities(): Promise<{
|
||||
api: OsmServiceState
|
||||
gpx: OsmServiceState
|
||||
database: OsmServiceState
|
||||
}> {
|
||||
if (Utils.runningFromConsole) {
|
||||
return { api: "online", gpx: "online" }
|
||||
return { api: "online", gpx: "online", database: "online" }
|
||||
}
|
||||
const result = await Utils.downloadAdvanced(this.Backend() + "/api/0.6/capabilities")
|
||||
if (result["content"] === undefined) {
|
||||
console.log("Something went wrong:", result)
|
||||
return { api: "unreachable", gpx: "unreachable" }
|
||||
try {
|
||||
const result = await Utils.downloadJson<CapabilityResult>(
|
||||
this.Backend() + "/api/0.6/capabilities.json"
|
||||
)
|
||||
if (result?.api?.status === undefined) {
|
||||
console.log("Something went wrong:", result)
|
||||
return { api: "unreachable", gpx: "unreachable", database: "unreachable" }
|
||||
}
|
||||
return result.api.status
|
||||
} catch (e) {
|
||||
console.error("Could not fetch capabilities")
|
||||
return { api: "offline", gpx: "offline", database: "online" }
|
||||
}
|
||||
const xmlRaw = result["content"]
|
||||
const parsed = new DOMParser().parseFromString(xmlRaw, "text/xml")
|
||||
const statusEl = parsed.getElementsByTagName("status")[0]
|
||||
const api = <OsmServiceState>statusEl.getAttribute("api")
|
||||
const gpx = <OsmServiceState>statusEl.getAttribute("gpx")
|
||||
return { api, gpx }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import osmtogeojson from "osmtogeojson"
|
|||
import { FeatureCollection } from "@turf/turf"
|
||||
import { Geometry } from "geojson"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
|
||||
;("use strict")
|
||||
/**
|
||||
* Interfaces overpass to get all the latest data
|
||||
*/
|
||||
|
@ -74,9 +74,9 @@ export class Overpass {
|
|||
console.warn("No features for", json)
|
||||
}
|
||||
|
||||
const geojson = osmtogeojson(json)
|
||||
const geojson = <FeatureCollection<Geometry, OsmTags>>osmtogeojson(json)
|
||||
const osmTime = new Date(json.osm3s.timestamp_osm_base)
|
||||
return [<any>geojson, osmTime]
|
||||
return [geojson, osmTime]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Utils } from "../Utils"
|
||||
import { Readable, Subscriber, Unsubscriber, Updater, Writable } from "svelte/store"
|
||||
|
||||
/**
|
||||
* Various static utils
|
||||
*/
|
||||
|
@ -276,7 +275,7 @@ export abstract class Store<T> implements Readable<T> {
|
|||
|
||||
public bindD<X>(
|
||||
f: (t: Exclude<T, undefined | null>) => Store<X>,
|
||||
extraSources: UIEventSource<object>[] = []
|
||||
extraSources: Store<any>[] = []
|
||||
): Store<X> {
|
||||
return this.bind((t) => {
|
||||
if (t === null) {
|
||||
|
@ -952,15 +951,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
g: (j: J, t: T) => T,
|
||||
allowUnregister = false
|
||||
): UIEventSource<J> {
|
||||
const self = this
|
||||
|
||||
const stack = new Error().stack.split("\n")
|
||||
const callee = stack[1]
|
||||
|
||||
const newSource = new UIEventSource<J>(f(this.data), "map(" + this.tag + ")@" + callee)
|
||||
|
||||
const update = function () {
|
||||
newSource.setData(f(self.data))
|
||||
const update = () => {
|
||||
newSource.setData(f(this.data))
|
||||
return allowUnregister && newSource._callbacks.length() === 0
|
||||
}
|
||||
|
||||
|
@ -971,7 +968,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
|
||||
if (g !== undefined) {
|
||||
newSource.addCallback((latest) => {
|
||||
self.setData(g(latest, self.data))
|
||||
this.setData(g(latest, this.data))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -980,8 +977,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
|
||||
public syncWith(otherSource: UIEventSource<T>, reverseOverride = false): UIEventSource<T> {
|
||||
this.addCallback((latest) => otherSource.setData(latest))
|
||||
const self = this
|
||||
otherSource.addCallback((latest) => self.setData(latest))
|
||||
otherSource.addCallback((latest) => this.setData(latest))
|
||||
if (reverseOverride) {
|
||||
if (otherSource.data !== undefined) {
|
||||
this.setData(otherSource.data)
|
||||
|
|
|
@ -421,6 +421,7 @@ export default class LinkedDataLoader {
|
|||
delete output["chargeEnd"]
|
||||
delete output["chargeStart"]
|
||||
delete output["timeUnit"]
|
||||
delete output["id"]
|
||||
|
||||
asBoolean("covered")
|
||||
asBoolean("fee", true)
|
||||
|
@ -518,6 +519,9 @@ export default class LinkedDataLoader {
|
|||
property: string,
|
||||
variable?: string
|
||||
): Promise<SparqlResult<T, G>> {
|
||||
if (property === "schema:photos") {
|
||||
console.log(">> Getting photos")
|
||||
}
|
||||
const results = await new TypedSparql().typedSparql<T, G>(
|
||||
{
|
||||
schema: "http://schema.org/",
|
||||
|
@ -531,15 +535,23 @@ export default class LinkedDataLoader {
|
|||
" ?parking a <http://schema.mobivoc.org/BicycleParkingStation>",
|
||||
"?parking " + property + " " + (variable ?? "")
|
||||
)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param url
|
||||
* @param property
|
||||
* @param subExpr
|
||||
* @private
|
||||
*/
|
||||
private static async fetchVeloparkGraphProperty<T extends string>(
|
||||
url: string,
|
||||
property: string,
|
||||
subExpr?: string
|
||||
): Promise<SparqlResult<T, "g">> {
|
||||
return await new TypedSparql().typedSparql<T, "g">(
|
||||
const result = await new TypedSparql().typedSparql<T, "g">(
|
||||
{
|
||||
schema: "http://schema.org/",
|
||||
mv: "http://schema.mobivoc.org/",
|
||||
|
@ -551,8 +563,15 @@ export default class LinkedDataLoader {
|
|||
"g",
|
||||
" ?parking a <http://schema.mobivoc.org/BicycleParkingStation>",
|
||||
|
||||
S.graph("g", "?section " + property + " " + (subExpr ?? ""), "?section a ?type")
|
||||
S.graph(
|
||||
"g",
|
||||
"?section " + property + " " + (subExpr ?? ""),
|
||||
"?section a ?type",
|
||||
"BIND(STR(?section) AS ?id)"
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -569,26 +588,69 @@ export default class LinkedDataLoader {
|
|||
continue
|
||||
}
|
||||
for (const sectionKey in subResult) {
|
||||
if (!r[sectionKey]) {
|
||||
r[sectionKey] = {}
|
||||
}
|
||||
const section = subResult[sectionKey]
|
||||
for (const key in section) {
|
||||
r[sectionKey][key] ??= section[key]
|
||||
if (sectionKey === "default") {
|
||||
r["default"] ??= {}
|
||||
const section = subResult["default"]
|
||||
for (const key in section) {
|
||||
r["default"][key] ??= section[key]
|
||||
}
|
||||
} else {
|
||||
const section = subResult[sectionKey]
|
||||
const actualId = Array.from(section["id"] ?? [])[0] ?? sectionKey
|
||||
r[actualId] ??= {}
|
||||
for (const key in section) {
|
||||
r[actualId][key] ??= section[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (r["default"] !== undefined && Object.keys(r).length > 1) {
|
||||
/**
|
||||
* Copy all values from the section with name "key" into the other sections,
|
||||
* remove section "key" afterwards
|
||||
* @param key
|
||||
*/
|
||||
function spreadSection(key: string) {
|
||||
for (const section in r) {
|
||||
if (section === "default") {
|
||||
if (section === key) {
|
||||
continue
|
||||
}
|
||||
for (const k in r.default) {
|
||||
r[section][k] ??= r.default[k]
|
||||
for (const k in r[key]) {
|
||||
r[section][k] ??= r[key][k]
|
||||
}
|
||||
}
|
||||
delete r.default
|
||||
delete r[key]
|
||||
}
|
||||
|
||||
// The "default" part of the result contains all general info
|
||||
// The other 'sections' need to get those copied! Then, we delete the "default"-section
|
||||
if (r["default"] !== undefined && Object.keys(r).length > 1) {
|
||||
spreadSection("default")
|
||||
}
|
||||
if (Object.keys(r).length > 1) {
|
||||
// This result has multiple sections
|
||||
// We should check that the naked URL got distributed and scrapped
|
||||
const keys = Object.keys(r)
|
||||
if (Object.keys(r).length > 2) {
|
||||
console.log("Multiple sections detected: ", JSON.stringify(keys))
|
||||
}
|
||||
const shortestKeyLength: number = Math.min(...keys.map((k) => k.length))
|
||||
const key = keys.find((k) => k.length === shortestKeyLength)
|
||||
if (keys.some((k) => !k.startsWith(key))) {
|
||||
throw (
|
||||
"Invalid multi-object: the shortest key is not the start of all the others: " +
|
||||
JSON.stringify(keys)
|
||||
)
|
||||
}
|
||||
spreadSection(key)
|
||||
}
|
||||
if (Object.keys(r).length == 1) {
|
||||
const key = Object.keys(r)[0]
|
||||
if (key.indexOf("#") > 0) {
|
||||
const newKey = key.split("#")[0]
|
||||
r[newKey] = r[key]
|
||||
delete r[key]
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
@ -675,6 +737,7 @@ export default class LinkedDataLoader {
|
|||
/**
|
||||
* Fetches all data relevant to velopark.
|
||||
* The id will be saved as `ref:velopark`
|
||||
* If the entry has multiple sections, this will return multiple items
|
||||
* @param url
|
||||
*/
|
||||
public static async fetchVeloparkEntry(
|
||||
|
@ -685,6 +748,7 @@ export default class LinkedDataLoader {
|
|||
if (this.veloparkCache[cacheKey]) {
|
||||
return this.veloparkCache[cacheKey]
|
||||
}
|
||||
// Note: the proxy doesn't make any changes in this case
|
||||
const withProxyUrl = Constants.linkedDataProxy.replace("{url}", encodeURIComponent(url))
|
||||
const optionalPaths: Record<string, string | Record<string, string>> = {
|
||||
"schema:interactionService": {
|
||||
|
@ -697,6 +761,7 @@ export default class LinkedDataLoader {
|
|||
"schema:email": "email",
|
||||
"schema:telephone": "phone",
|
||||
},
|
||||
// "schema:photos": "images",
|
||||
"schema:dateModified": "_last_edit_timestamp",
|
||||
}
|
||||
if (includeExtras) {
|
||||
|
@ -740,9 +805,22 @@ export default class LinkedDataLoader {
|
|||
graphOptionalPaths,
|
||||
extra
|
||||
)
|
||||
for (const unpatchedKey in unpatched) {
|
||||
// Dirty hack
|
||||
const rawData = await Utils.downloadJsonCached<object>(url, 1000 * 60 * 60)
|
||||
const images = rawData["photos"]?.map((ph) => <string>ph.image)
|
||||
if (images) {
|
||||
unpatched[unpatchedKey].images = new Set<string>(images)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Got unpatched:", unpatched)
|
||||
const patched: Feature[] = []
|
||||
for (const section in unpatched) {
|
||||
for (let section in unpatched) {
|
||||
const p = LinkedDataLoader.patchVeloparkProperties(unpatched[section])
|
||||
if (Object.keys(unpatched).length === 1 && section.endsWith("#section1")) {
|
||||
section = section.split("#")[0]
|
||||
}
|
||||
p["ref:velopark"] = [section]
|
||||
patched.push(LinkedDataLoader.asGeojson(p))
|
||||
}
|
||||
|
|
|
@ -67,13 +67,14 @@ export default class TypedSparql {
|
|||
bindings.forEach((item) => {
|
||||
const result = <Record<VARS | G, Set<string>>>{}
|
||||
item.forEach((value, key) => {
|
||||
if (!result[key.value]) {
|
||||
result[key.value] = new Set()
|
||||
}
|
||||
result[key.value] ??= new Set()
|
||||
result[key.value].add(value.value)
|
||||
})
|
||||
if (graphVariable && result[graphVariable]?.size > 0) {
|
||||
const id = Array.from(result[graphVariable])?.[0] ?? "default"
|
||||
const id: string =
|
||||
<string>Array.from(result["id"] ?? [])?.[0] ??
|
||||
Array.from(result[graphVariable] ?? [])?.[0] ??
|
||||
"default"
|
||||
resultAllGraphs[id] = result
|
||||
} else {
|
||||
resultAllGraphs["default"] = result
|
||||
|
|
|
@ -40,7 +40,7 @@ export default class Constants {
|
|||
"import_candidate",
|
||||
"usersettings",
|
||||
"icons",
|
||||
"filters"
|
||||
"filters",
|
||||
] as const
|
||||
/**
|
||||
* Layer IDs of layers which have special properties through built-in hooks
|
||||
|
|
|
@ -232,7 +232,7 @@ export default class FilteredLayer {
|
|||
}
|
||||
}
|
||||
{
|
||||
if(!this.isDisplayed.data){
|
||||
if (!this.isDisplayed.data) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -306,7 +306,10 @@ export default class ThemeConfig implements ThemeInformation {
|
|||
return { untranslated, total }
|
||||
}
|
||||
|
||||
public getMatchingLayer(tags: Record<string, string>, blacklistLayers?: Set<string>): LayerConfig | undefined {
|
||||
public getMatchingLayer(
|
||||
tags: Record<string, string>,
|
||||
blacklistLayers?: Set<string>
|
||||
): LayerConfig | undefined {
|
||||
if (tags === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -314,7 +317,7 @@ export default class ThemeConfig implements ThemeInformation {
|
|||
return this.getLayer("current_view")
|
||||
}
|
||||
for (const layer of this.layers) {
|
||||
if(blacklistLayers?.has(layer.id)){
|
||||
if (blacklistLayers?.has(layer.id)) {
|
||||
continue
|
||||
}
|
||||
if (!layer.source) {
|
||||
|
|
|
@ -177,12 +177,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
)
|
||||
this.map = new UIEventSource<MlMap>(undefined)
|
||||
const geolocationState = new GeoLocationState()
|
||||
const initial = new InitialMapPositioning(layout, geolocationState)
|
||||
this.mapProperties = new MapLibreAdaptor(this.map, initial, { correctClick: 20 })
|
||||
|
||||
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
|
||||
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
|
||||
|
||||
this.osmConnection = new OsmConnection({
|
||||
dryRun: this.featureSwitches.featureSwitchIsTesting,
|
||||
fakeUser: this.featureSwitches.featureSwitchFakeUser.data,
|
||||
|
@ -192,6 +186,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
"Used to complete the login"
|
||||
),
|
||||
})
|
||||
const initial = new InitialMapPositioning(layout, geolocationState, this.osmConnection)
|
||||
this.mapProperties = new MapLibreAdaptor(this.map, initial, { correctClick: 20 })
|
||||
|
||||
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
|
||||
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
|
||||
|
||||
this.userRelatedState = new UserRelatedState(
|
||||
this.osmConnection,
|
||||
layout,
|
||||
|
@ -788,7 +788,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
|
||||
const layers = this.theme.layers.filter(
|
||||
(l) =>
|
||||
Constants.priviliged_layers.indexOf(<any>l.id) < 0 &&
|
||||
(<string[]>(<unknown>Constants.priviliged_layers)).indexOf(l.id) < 0 &&
|
||||
l.source.geojsonSource === undefined &&
|
||||
l.doCount
|
||||
)
|
||||
|
@ -840,7 +840,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
|
||||
this.closestFeatures.registerSource(specialLayers.favourite, "favourite")
|
||||
if (this.theme?.lockLocation) {
|
||||
const bbox = new BBox(<any>this.theme.lockLocation)
|
||||
const bbox = new BBox(<[[number, number], [number, number]]>this.theme.lockLocation)
|
||||
this.mapProperties.maxbounds.setData(bbox)
|
||||
ShowDataLayer.showRange(
|
||||
this.map,
|
||||
|
|
|
@ -53,6 +53,8 @@
|
|||
fill: var(--button-background-hover);
|
||||
transition: fill 350ms linear;
|
||||
cursor: pointer;
|
||||
stroke-width: 0.8;
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
:global(.dots-menu:hover > path, .dots-menu-opened > path) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import { Translation } from "../i18n/Translation"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "./Tr.svelte"
|
||||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Invalid from "../../assets/svg/Invalid.svelte"
|
||||
import ArrowPath from "@babeard/svelte-heroicons/mini/ArrowPath"
|
||||
|
||||
|
@ -21,6 +21,10 @@
|
|||
* Only show the 'successful' state, don't show loading or error messages
|
||||
*/
|
||||
export let silentFail: boolean = false
|
||||
/**
|
||||
* If set and the OSM-api fails, do _not_ show any error messages nor the successful state, just hide
|
||||
*/
|
||||
export let hiddenFail: boolean = false
|
||||
let loadingStatus = state?.osmConnection?.loadingStatus ?? new ImmutableStore("logged-in")
|
||||
let badge = state?.featureSwitches?.featureSwitchEnableLogin ?? new ImmutableStore(true)
|
||||
const t = Translations.t.general
|
||||
|
@ -30,7 +34,7 @@
|
|||
unknown: t.loginFailedUnreachableMode,
|
||||
readonly: t.loginFailedReadonlyMode,
|
||||
}
|
||||
const apiState =
|
||||
const apiState: Store<string> =
|
||||
state?.osmConnection?.apiIsOnline ?? new ImmutableStore<OsmServiceState>("online")
|
||||
</script>
|
||||
|
||||
|
@ -39,19 +43,21 @@
|
|||
<slot name="loading">
|
||||
<Loading />
|
||||
</slot>
|
||||
{:else if !silentFail && $loadingStatus === "error"}
|
||||
<slot name="error">
|
||||
<div class="alert flex flex-col items-center">
|
||||
<div class="max-w-64 flex items-center">
|
||||
<Invalid class="m-2 h-8 w-8 shrink-0" />
|
||||
<Tr t={offlineModes[$apiState] ?? t.loginFailedUnreachableMode} />
|
||||
{:else if !silentFail && ($loadingStatus === "error" || $apiState === "readonly" || $apiState === "offline")}
|
||||
{#if !hiddenFail}
|
||||
<slot name="error">
|
||||
<div class="alert flex flex-col items-center">
|
||||
<div class="max-w-64 flex items-center">
|
||||
<Invalid class="m-2 h-8 w-8 shrink-0" />
|
||||
<Tr t={offlineModes[$apiState] ?? t.loginFailedUnreachableMode} />
|
||||
</div>
|
||||
<button class="h-fit" on:click={() => state.osmConnection.AttemptLogin()}>
|
||||
<ArrowPath class="h-6 w-6" />
|
||||
<Tr t={t.retry} />
|
||||
</button>
|
||||
</div>
|
||||
<button class="h-fit" on:click={() => state.osmConnection.AttemptLogin()}>
|
||||
<ArrowPath class="h-6 w-6" />
|
||||
<Tr t={t.retry} />
|
||||
</button>
|
||||
</div>
|
||||
</slot>
|
||||
</slot>
|
||||
{/if}
|
||||
{:else if $loadingStatus === "logged-in"}
|
||||
<slot />
|
||||
{:else if !silentFail && $loadingStatus === "not-attempted"}
|
||||
|
|
|
@ -28,9 +28,9 @@
|
|||
(s) =>
|
||||
(s === "yes" &&
|
||||
state?.userRelatedState?.osmConnection?.userDetails?.data?.csCount >=
|
||||
Constants.userJourney.tagsVisibleAt) ||
|
||||
Constants.userJourney.tagsVisibleAt) ||
|
||||
s === "always" ||
|
||||
s === "full",
|
||||
s === "full"
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -80,9 +80,9 @@
|
|||
<Checkbox selected={getBooleanStateFor(filter)}>
|
||||
<Tr t={filter.options[0].question} />
|
||||
{#if $showTags && filter.options[0].osmTags !== undefined}
|
||||
<span class="subtle">
|
||||
{filter.options[0].osmTags.asHumanString()}
|
||||
</span>
|
||||
<span class="subtle">
|
||||
{filter.options[0].osmTags.asHumanString()}
|
||||
</span>
|
||||
{/if}
|
||||
</Checkbox>
|
||||
{/if}
|
||||
|
|
|
@ -126,7 +126,7 @@
|
|||
</svelte:fragment>
|
||||
|
||||
<!-- All shown components are set by 'usersettings.json', which happily uses some special visualisations created specifically for it -->
|
||||
<LoginToggle {state}>
|
||||
<LoginToggle {state} silentFail>
|
||||
<div class="flex flex-col" slot="not-logged-in">
|
||||
<LanguagePicker availableLanguages={theme.language} />
|
||||
<Tr cls="alert" t={Translations.t.userinfo.notLoggedIn} />
|
||||
|
@ -146,7 +146,7 @@
|
|||
</LoginToggle>
|
||||
</Page>
|
||||
|
||||
<LoginToggle {state}>
|
||||
<LoginToggle {state} silentFail>
|
||||
<Page {onlyLink} shown={pg.favourites}>
|
||||
<svelte:fragment slot="header">
|
||||
<HeartIcon />
|
||||
|
|
|
@ -128,17 +128,20 @@
|
|||
|
||||
{#if $unknownImages.length > 0}
|
||||
{#if readonly}
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div class="flex h-32 w-max gap-x-2">
|
||||
{#each $unknownImages as image (image)}
|
||||
<div
|
||||
class="flex w-full space-x-2 overflow-x-auto border border-gray-600 p-1"
|
||||
style="scroll-snap-type: x proximity; border: 1px solid black"
|
||||
>
|
||||
{#each $unknownImages as image (image)}
|
||||
<div class="relative flex w-fit items-center bg-gray-200">
|
||||
<AttributedImage
|
||||
{state}
|
||||
imgClass="h-32 w-max shrink-0"
|
||||
image={{ url: image }}
|
||||
imgClass="h-32 shrink-0"
|
||||
image={{ url: image, id: image }}
|
||||
previewedImage={state.previewedImage}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
{#each $unknownImages as image (image)}
|
||||
|
|
|
@ -12,25 +12,31 @@
|
|||
|
||||
const downloader = new OsmObjectDownloader()
|
||||
let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.FromPromise(
|
||||
Promise.all(features.map(f => downloader.downloadHistory(f.properties.id)))
|
||||
Promise.all(features.map((f) => downloader.downloadHistory(f.properties.id)))
|
||||
)
|
||||
let imageKeys = new Set(
|
||||
...["panoramax", "image:streetsign", "image:menu"].map((k) => {
|
||||
const result: string[] = [k]
|
||||
for (let i = 0; i < 10; i++) {
|
||||
result.push(k + ":" + i)
|
||||
}
|
||||
return result
|
||||
})
|
||||
)
|
||||
let imageKeys = new Set(...["panoramax", "image:streetsign", "image:menu"].map(k => {
|
||||
const result: string[] = [k]
|
||||
for (let i = 0; i < 10; i++) {
|
||||
result.push(k + ":" + i)
|
||||
}
|
||||
return result
|
||||
}))
|
||||
let usernamesSet = new Set(onlyShowUsername)
|
||||
let allDiffs: Store<{
|
||||
key: string;
|
||||
value?: string;
|
||||
oldValue?: string
|
||||
}[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, usernamesSet))
|
||||
|
||||
let addedImages = allDiffs.mapD(diffs => [].concat(...diffs.filter(({ key }) => imageKeys.has(key))))
|
||||
let allDiffs: Store<
|
||||
{
|
||||
key: string
|
||||
value?: string
|
||||
oldValue?: string
|
||||
}[]
|
||||
> = allHistories.mapD((histories) => HistoryUtils.fullHistoryDiff(histories, usernamesSet))
|
||||
|
||||
let addedImages = allDiffs.mapD((diffs) =>
|
||||
[].concat(...diffs.filter(({ key }) => imageKeys.has(key)))
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if $allDiffs === undefined}
|
||||
<Loading />
|
||||
{:else if $addedImages.length === 0}
|
||||
|
@ -38,7 +44,7 @@
|
|||
{:else}
|
||||
<div class="flex">
|
||||
{#each $addedImages as imgDiff}
|
||||
<div class="w-48 h-48">
|
||||
<div class="h-48 w-48">
|
||||
<AttributedPanoramaxImage hash={imgDiff.value} />
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
@ -18,26 +18,30 @@
|
|||
|
||||
const downloader = new OsmObjectDownloader()
|
||||
let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.FromPromise(
|
||||
Promise.all(features.map(f => downloader.downloadHistory(f.properties.id)))
|
||||
Promise.all(features.map((f) => downloader.downloadHistory(f.properties.id)))
|
||||
)
|
||||
let allDiffs: Store<{
|
||||
key: string;
|
||||
value?: string;
|
||||
oldValue?: string
|
||||
}[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, usernames))
|
||||
let allDiffs: Store<
|
||||
{
|
||||
key: string
|
||||
value?: string
|
||||
oldValue?: string
|
||||
}[]
|
||||
> = allHistories.mapD((histories) => HistoryUtils.fullHistoryDiff(histories, usernames))
|
||||
|
||||
const trs = shared_questions.tagRenderings.map(tr => new TagRenderingConfig(tr))
|
||||
const trs = shared_questions.tagRenderings.map((tr) => new TagRenderingConfig(tr))
|
||||
|
||||
function detectQuestion(key: string): TagRenderingConfig {
|
||||
return trs.find(tr => tr.freeform?.key === key)
|
||||
return trs.find((tr) => tr.freeform?.key === key)
|
||||
}
|
||||
|
||||
const mergedCount: Store<{
|
||||
key: string;
|
||||
tr: TagRenderingConfig;
|
||||
count: number;
|
||||
values: { value: string; count: number }[]
|
||||
}[]> = allDiffs.mapD(allDiffs => {
|
||||
const mergedCount: Store<
|
||||
{
|
||||
key: string
|
||||
tr: TagRenderingConfig
|
||||
count: number
|
||||
values: { value: string; count: number }[]
|
||||
}[]
|
||||
> = allDiffs.mapD((allDiffs) => {
|
||||
const keyCounts = new Map<string, Map<string, number>>()
|
||||
for (const diff of allDiffs) {
|
||||
const k = diff.key
|
||||
|
@ -50,11 +54,13 @@
|
|||
}
|
||||
|
||||
const perKey: {
|
||||
key: string, tr: TagRenderingConfig, count: number, values:
|
||||
{ value: string, count: number }[]
|
||||
key: string
|
||||
tr: TagRenderingConfig
|
||||
count: number
|
||||
values: { value: string; count: number }[]
|
||||
}[] = []
|
||||
keyCounts.forEach((values, key) => {
|
||||
const keyTotal: { value: string, count: number }[] = []
|
||||
const keyTotal: { value: string; count: number }[] = []
|
||||
values.forEach((count, value) => {
|
||||
keyTotal.push({ value, count })
|
||||
})
|
||||
|
@ -72,7 +78,6 @@
|
|||
})
|
||||
|
||||
const t = Translations.t.inspector
|
||||
|
||||
</script>
|
||||
|
||||
{#if allHistories === undefined}
|
||||
|
@ -88,7 +93,7 @@
|
|||
</h3>
|
||||
<AccordionSingle>
|
||||
<span slot="header">
|
||||
<Tr t={t.answeredCountTimes.Subs(diff)} />
|
||||
<Tr t={t.answeredCountTimes.Subs(diff)} />
|
||||
</span>
|
||||
<ul>
|
||||
{#each diff.values as value}
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
|
||||
|
||||
export let hash: string
|
||||
let image: UIEventSource<ProvidedImage> = UIEventSource.FromPromise(PanoramaxImageProvider.singleton.getInfo(hash))
|
||||
let image: UIEventSource<ProvidedImage> = UIEventSource.FromPromise(
|
||||
PanoramaxImageProvider.singleton.getInfo(hash)
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if $image !== undefined}
|
||||
<AttributedImage image={$image}></AttributedImage>
|
||||
<AttributedImage image={$image} />
|
||||
{/if}
|
||||
|
|
|
@ -17,41 +17,51 @@
|
|||
let usernames = new Set(onlyShowChangesBy)
|
||||
let fullHistory = UIEventSource.FromPromise(new OsmObjectDownloader().downloadHistory(id))
|
||||
|
||||
let partOfLayer = fullHistory.mapD(history => history.map(step => ({
|
||||
step,
|
||||
layer: HistoryUtils.determineLayer(step.tags)
|
||||
})))
|
||||
let filteredHistory = partOfLayer.mapD(history =>
|
||||
history.filter(({ step }) => {
|
||||
if (usernames.size == 0) {
|
||||
return true
|
||||
}
|
||||
console.log("Checking if ", step.tags["_last_edit:contributor"],"is contained in", onlyShowChangesBy)
|
||||
return usernames.has(step.tags["_last_edit:contributor"])
|
||||
|
||||
}).map(({ step, layer }) => {
|
||||
const diff = HistoryUtils.tagHistoryDiff(step, fullHistory.data)
|
||||
return { step, layer, diff }
|
||||
let partOfLayer = fullHistory.mapD((history) =>
|
||||
history.map((step) => ({
|
||||
step,
|
||||
layer: HistoryUtils.determineLayer(step.tags),
|
||||
}))
|
||||
)
|
||||
let filteredHistory = partOfLayer.mapD((history) =>
|
||||
history
|
||||
.filter(({ step }) => {
|
||||
if (usernames.size == 0) {
|
||||
return true
|
||||
}
|
||||
console.log(
|
||||
"Checking if ",
|
||||
step.tags["_last_edit:contributor"],
|
||||
"is contained in",
|
||||
onlyShowChangesBy
|
||||
)
|
||||
return usernames.has(step.tags["_last_edit:contributor"])
|
||||
})
|
||||
.map(({ step, layer }) => {
|
||||
const diff = HistoryUtils.tagHistoryDiff(step, fullHistory.data)
|
||||
return { step, layer, diff }
|
||||
})
|
||||
)
|
||||
|
||||
let lastStep = filteredHistory.mapD(history => history.at(-1))
|
||||
let allGeometry = filteredHistory.mapD(all => !all.some(x => x.diff.length > 0))
|
||||
let lastStep = filteredHistory.mapD((history) => history.at(-1))
|
||||
let allGeometry = filteredHistory.mapD((all) => !all.some((x) => x.diff.length > 0))
|
||||
/**
|
||||
* These layers are only shown if there are tag changes as well
|
||||
*/
|
||||
const ignoreLayersIfNoChanges: ReadonlySet<string> = new Set(["walls_and_buildings"])
|
||||
const t = Translations.t.inspector.previousContributors
|
||||
|
||||
</script>
|
||||
|
||||
{#if !$allGeometry || !ignoreLayersIfNoChanges.has($lastStep?.layer?.id)}
|
||||
{#if $lastStep?.layer}
|
||||
<a href={"https://openstreetmap.org/" + $lastStep.step.tags.id} target="_blank">
|
||||
<h3 class="flex items-center gap-x-2">
|
||||
<div class="w-8 h-8 shrink-0 inline-block">
|
||||
<div class="inline-block h-8 w-8 shrink-0">
|
||||
<ToSvelte construct={$lastStep.layer?.defaultIcon($lastStep.step.tags)} />
|
||||
</div>
|
||||
<Tr t={$lastStep.layer?.title?.GetRenderValue($lastStep.step.tags)?.Subs($lastStep.step.tags)} />
|
||||
<Tr
|
||||
t={$lastStep.layer?.title?.GetRenderValue($lastStep.step.tags)?.Subs($lastStep.step.tags)}
|
||||
/>
|
||||
</h3>
|
||||
</a>
|
||||
{/if}
|
||||
|
@ -61,42 +71,48 @@
|
|||
{:else if $filteredHistory.length === 0}
|
||||
<Tr t={t.onlyGeometry} />
|
||||
{:else}
|
||||
<table class="w-full m-1">
|
||||
<table class="m-1 w-full">
|
||||
{#each $filteredHistory as { step, layer }}
|
||||
|
||||
{#if step.version === 1}
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<h3>
|
||||
<Tr t={t.createdBy.Subs({contributor: step.tags["_last_edit:contributor"]})} />
|
||||
<Tr t={t.createdBy.Subs({ contributor: step.tags["_last_edit:contributor"] })} />
|
||||
</h3>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if HistoryUtils.tagHistoryDiff(step, $fullHistory).length === 0}
|
||||
<tr>
|
||||
<td class="font-bold justify-center flex w-full" colspan="3">
|
||||
<td class="flex w-full justify-center font-bold" colspan="3">
|
||||
<Tr t={t.onlyGeometry} />
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#each HistoryUtils.tagHistoryDiff(step, $fullHistory) as diff}
|
||||
<tr>
|
||||
<td><a href={"https://osm.org/changeset/"+step.tags["_last_edit:changeset"]}
|
||||
target="_blank">{step.version}</a></td>
|
||||
<td>
|
||||
<a
|
||||
href={"https://osm.org/changeset/" + step.tags["_last_edit:changeset"]}
|
||||
target="_blank"
|
||||
>
|
||||
{step.version}
|
||||
</a>
|
||||
</td>
|
||||
<td>{layer?.id ?? "Unknown layer"}</td>
|
||||
{#if diff.oldValue === undefined}
|
||||
<td>{diff.key}</td>
|
||||
<td>{diff.value}</td>
|
||||
{:else if diff.value === undefined }
|
||||
{:else if diff.value === undefined}
|
||||
<td>{diff.key}</td>
|
||||
<td class="line-through"> {diff.value}</td>
|
||||
<td class="line-through">{diff.value}</td>
|
||||
{:else}
|
||||
<td>{diff.key}</td>
|
||||
<td><span class="line-through"> {diff.oldValue}</span> → {diff.value}</td>
|
||||
<td>
|
||||
<span class="line-through">{diff.oldValue}</span>
|
||||
→ {diff.value}
|
||||
</td>
|
||||
{/if}
|
||||
|
||||
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
|
|
|
@ -3,49 +3,63 @@ import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
|
|||
import { OsmObject } from "../../Logic/Osm/OsmObject"
|
||||
|
||||
export class HistoryUtils {
|
||||
|
||||
public static readonly personalTheme = new ThemeConfig(<any> all_layers, true)
|
||||
public static readonly personalTheme = new ThemeConfig(<any>all_layers, true)
|
||||
private static ignoredLayers = new Set<string>(["fixme"])
|
||||
public static determineLayer(properties: Record<string, string>){
|
||||
public static determineLayer(properties: Record<string, string>) {
|
||||
return this.personalTheme.getMatchingLayer(properties, this.ignoredLayers)
|
||||
}
|
||||
|
||||
public static tagHistoryDiff(step: OsmObject, history: OsmObject[]): {
|
||||
key: string,
|
||||
value?: string,
|
||||
oldValue?: string,
|
||||
public static tagHistoryDiff(
|
||||
step: OsmObject,
|
||||
history: OsmObject[]
|
||||
): {
|
||||
key: string
|
||||
value?: string
|
||||
oldValue?: string
|
||||
step: OsmObject
|
||||
}[] {
|
||||
const previous = history[step.version - 2]
|
||||
if (!previous) {
|
||||
return Object.keys(step.tags).filter(key => !key.startsWith("_") && key !== "id").map(key => ({
|
||||
key, value: step.tags[key], step
|
||||
}))
|
||||
return Object.keys(step.tags)
|
||||
.filter((key) => !key.startsWith("_") && key !== "id")
|
||||
.map((key) => ({
|
||||
key,
|
||||
value: step.tags[key],
|
||||
step,
|
||||
}))
|
||||
}
|
||||
const previousTags = previous.tags
|
||||
return Object.keys(step.tags).filter(key => !key.startsWith("_") )
|
||||
.map(key => {
|
||||
return Object.keys(step.tags)
|
||||
.filter((key) => !key.startsWith("_"))
|
||||
.map((key) => {
|
||||
const value = step.tags[key]
|
||||
const oldValue = previousTags[key]
|
||||
return {
|
||||
key, value, oldValue, step
|
||||
key,
|
||||
value,
|
||||
oldValue,
|
||||
step,
|
||||
}
|
||||
}).filter(ch => ch.oldValue !== ch.value)
|
||||
})
|
||||
.filter((ch) => ch.oldValue !== ch.value)
|
||||
}
|
||||
|
||||
public static fullHistoryDiff(histories: OsmObject[][], onlyShowUsername?: Set<string>){
|
||||
const allDiffs: {key: string, oldValue?: string, value?: string}[] = [].concat(...histories.map(
|
||||
history => {
|
||||
const filtered = history.filter(step => !onlyShowUsername || onlyShowUsername?.has(step.tags["_last_edit:contributor"] ))
|
||||
public static fullHistoryDiff(histories: OsmObject[][], onlyShowUsername?: Set<string>) {
|
||||
const allDiffs: { key: string; oldValue?: string; value?: string }[] = [].concat(
|
||||
...histories.map((history) => {
|
||||
const filtered = history.filter(
|
||||
(step) =>
|
||||
!onlyShowUsername ||
|
||||
onlyShowUsername?.has(step.tags["_last_edit:contributor"])
|
||||
)
|
||||
const diffs: {
|
||||
key: string;
|
||||
value?: string;
|
||||
key: string
|
||||
value?: string
|
||||
oldValue?: string
|
||||
}[][] = filtered.map(step => HistoryUtils.tagHistoryDiff(step, history))
|
||||
}[][] = filtered.map((step) => HistoryUtils.tagHistoryDiff(step, history))
|
||||
return [].concat(...diffs)
|
||||
}
|
||||
))
|
||||
})
|
||||
)
|
||||
return allDiffs
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
|
@ -9,18 +8,23 @@
|
|||
import Dropdown from "../Base/Dropdown.svelte"
|
||||
|
||||
export let osmConnection: OsmConnection
|
||||
export let inspectedContributors: UIEventSource<{
|
||||
name: string,
|
||||
visitedTime: string,
|
||||
label: string
|
||||
}[]>
|
||||
export let inspectedContributors: UIEventSource<
|
||||
{
|
||||
name: string
|
||||
visitedTime: string
|
||||
label: string
|
||||
}[]
|
||||
>
|
||||
let dispatch = createEventDispatcher<{ selectUser: string }>()
|
||||
|
||||
let labels = UIEventSource.asObject<string[]>(osmConnection.getPreference("previously-spied-labels"), [])
|
||||
let labels = UIEventSource.asObject<string[]>(
|
||||
osmConnection.getPreference("previously-spied-labels"),
|
||||
[]
|
||||
)
|
||||
let labelField = ""
|
||||
|
||||
function remove(user: string) {
|
||||
inspectedContributors.set(inspectedContributors.data.filter(entry => entry.name !== user))
|
||||
inspectedContributors.set(inspectedContributors.data.filter((entry) => entry.name !== user))
|
||||
}
|
||||
|
||||
function addLabel() {
|
||||
|
@ -39,16 +43,13 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<LoginToggle ignoreLoading state={{osmConnection}}>
|
||||
<LoginToggle ignoreLoading state={{ osmConnection }}>
|
||||
<table class="w-full">
|
||||
<tr>
|
||||
<td>
|
||||
<button class="as-link cursor-pointer" on:click={() => sort("name")}>
|
||||
Contributor
|
||||
</button>
|
||||
<button class="as-link cursor-pointer" on:click={() => sort("name")}>Contributor</button>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<button class="as-link cursor-pointer" on:click={() => sort("visitedTime")}>
|
||||
Visited time
|
||||
</button>
|
||||
|
@ -75,32 +76,46 @@
|
|||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<XCircleIcon class="w-6 h-6" on:click={() => remove(c.name)} />
|
||||
<XCircleIcon class="h-6 w-6" on:click={() => remove(c.name)} />
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
|
||||
<AccordionSingle>
|
||||
|
||||
<div slot="header">Labels</div>
|
||||
{#if $labels.length === 0}
|
||||
No labels
|
||||
{:else}
|
||||
{#each $labels as label}
|
||||
<div class="mx-2">{label}
|
||||
<button class:disabled={!$inspectedContributors.some(c => c.label === label)} on:click={() => {dispatch("selectUser",
|
||||
inspectedContributors.data.filter(c =>c.label === label).map(c => c .name).join(";")
|
||||
)}}>See all changes for these users
|
||||
<div class="mx-2">
|
||||
{label}
|
||||
<button
|
||||
class:disabled={!$inspectedContributors.some((c) => c.label === label)}
|
||||
on:click={() => {
|
||||
dispatch(
|
||||
"selectUser",
|
||||
inspectedContributors.data
|
||||
.filter((c) => c.label === label)
|
||||
.map((c) => c.name)
|
||||
.join(";")
|
||||
)
|
||||
}}
|
||||
>
|
||||
See all changes for these users
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="interactive flex m-2 items-center gap-x-2 rounded-lg p-2">
|
||||
<div class="interactive m-2 flex items-center gap-x-2 rounded-lg p-2">
|
||||
<div class="shrink-0">Create a new label</div>
|
||||
<input bind:value={labelField} type="text" />
|
||||
<button on:click={() => addLabel()} class:disabled={!(labelField?.length > 0) } class="disabled shrink-0">Add
|
||||
label
|
||||
<button
|
||||
on:click={() => addLabel()}
|
||||
class:disabled={!(labelField?.length > 0)}
|
||||
class="disabled shrink-0"
|
||||
>
|
||||
Add label
|
||||
</button>
|
||||
</div>
|
||||
</AccordionSingle>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
export let imgClass: string = undefined
|
||||
export let state: SpecialVisualizationState = undefined
|
||||
export let attributionFormat: "minimal" | "medium" | "large" = "medium"
|
||||
export let previewedImage: UIEventSource<ProvidedImage> = undefined
|
||||
export let previewedImage: UIEventSource<Partial<ProvidedImage>> = undefined
|
||||
export let canZoom = previewedImage !== undefined
|
||||
let loaded = false
|
||||
let showBigPreview = new UIEventSource(false)
|
||||
|
@ -37,14 +37,14 @@
|
|||
if (!shown) {
|
||||
previewedImage?.set(undefined)
|
||||
}
|
||||
})
|
||||
)
|
||||
if(previewedImage){
|
||||
onDestroy(
|
||||
previewedImage.addCallbackAndRun((previewedImage) => {
|
||||
showBigPreview.set(previewedImage?.id === image.id)
|
||||
})
|
||||
}),
|
||||
)
|
||||
if (previewedImage) {
|
||||
onDestroy(
|
||||
previewedImage.addCallbackAndRun((previewedImage) => {
|
||||
showBigPreview.set(previewedImage !== undefined && (previewedImage?.id ?? previewedImage?.url) === (image.id ?? image.url))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function highlight(entered: boolean = true) {
|
||||
|
@ -89,6 +89,8 @@
|
|||
/>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
|
||||
{#if image.status !== undefined && image.status !== "ready" && image.status !== "hidden"}
|
||||
<div class="flex h-full flex-col justify-center">
|
||||
<Loading>
|
||||
|
@ -113,6 +115,7 @@
|
|||
class={imgClass ?? ""}
|
||||
class:cursor-zoom-in={canZoom}
|
||||
on:click={() => {
|
||||
console.log("Setting",image.url)
|
||||
previewedImage?.set(image)
|
||||
}}
|
||||
on:error={() => {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { Store } from "../../Logic/UIEventSource.js"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource.js"
|
||||
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import DeletableImage from "./DeletableImage.svelte"
|
||||
|
||||
export let images: Store<ProvidedImage[]>
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: Store<Record<string, string>>
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
</script>
|
||||
|
||||
<div class="flex w-full space-x-2 overflow-x-auto" style="scroll-snap-type: x proximity">
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import DotMenu from "../Base/DotMenu.svelte"
|
||||
|
||||
export let image: ProvidedImage
|
||||
export let image: Partial<ProvidedImage> & ({ id: string, url: string })
|
||||
export let clss: string = undefined
|
||||
|
||||
let isLoaded = new UIEventSource(false)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import Zoomcontrol from "../Zoomcontrol"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
export let image: ProvidedImage
|
||||
export let image: Partial<ProvidedImage>
|
||||
let panzoomInstance = undefined
|
||||
let panzoomEl: HTMLElement
|
||||
export let isLoaded: UIEventSource<boolean> = undefined
|
||||
|
|
|
@ -143,6 +143,18 @@
|
|||
highlighted.set(feature.properties.id)
|
||||
},
|
||||
})
|
||||
onDestroy(
|
||||
tags.addCallbackAndRunD((tags) => {
|
||||
if (
|
||||
tags.id.startsWith("node/") ||
|
||||
tags.id.startsWith("way/") ||
|
||||
tags.id.startsWith("relation/")
|
||||
) {
|
||||
return
|
||||
}
|
||||
linkable = false
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col">
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
import FileSelector from "../Base/FileSelector.svelte"
|
||||
import LoginButton from "../Base/LoginButton.svelte"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Camera from "@babeard/svelte-heroicons/solid/Camera"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import NoteCommentElement from "../Popup/Notes/NoteCommentElement"
|
||||
import type { Feature } from "geojson"
|
||||
import Camera from "@babeard/svelte-heroicons/mini/Camera"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
|
@ -24,7 +24,7 @@
|
|||
export let targetKey: string = undefined
|
||||
export let layer: LayerConfig
|
||||
export let noBlur: boolean = false
|
||||
export let feature: Feature = undefined
|
||||
export let feature: Feature
|
||||
/**
|
||||
* Image to show in the button
|
||||
* NOT the image to upload!
|
||||
|
@ -65,13 +65,13 @@
|
|||
}
|
||||
const url = uploadResult.absoluteUrl
|
||||
await state.osmConnection.addCommentToNote(tags.data.id, url)
|
||||
NoteCommentElement.addCommentTo(url, <UIEventSource<any>>tags, {
|
||||
NoteCommentElement.addCommentTo(url, <UIEventSource<OsmTags>>tags, {
|
||||
osmConnection: state.osmConnection,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await state?.imageUploadManager?.uploadImageAndApply(file, tags, targetKey, noBlur)
|
||||
await state?.imageUploadManager?.uploadImageAndApply(file, tags, targetKey, noBlur, feature)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
state.reportError(e, "Could not upload image")
|
||||
|
@ -133,9 +133,9 @@
|
|||
cls="flex justify-center md:hidden button"
|
||||
multiple={true}
|
||||
on:submit={(e) => {
|
||||
return handleFiles(e.detail, true)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return handleFiles(e.detail, true)
|
||||
}}
|
||||
>
|
||||
<Tr t={t.selectFile} />
|
||||
|
|
|
@ -41,14 +41,14 @@
|
|||
zoom,
|
||||
location: new UIEventSource<{ lon: number; lat: number }>({ lat: lat.data, lon: lon.data }),
|
||||
})
|
||||
maplibremap.location.stabilized(500).addCallbackAndRunD(l => {
|
||||
maplibremap.location.stabilized(500).addCallbackAndRunD((l) => {
|
||||
lat.set(l.lat)
|
||||
lon.set(l.lon)
|
||||
})
|
||||
|
||||
let allLayers = HistoryUtils.personalTheme.layers
|
||||
let layersNoFixme = allLayers.filter(l => l.id !== "fixme")
|
||||
let fixme = allLayers.find(l => l.id === "fixme")
|
||||
let layersNoFixme = allLayers.filter((l) => l.id !== "fixme")
|
||||
let fixme = allLayers.find((l) => l.id === "fixme")
|
||||
let featuresStore = new UIEventSource<Feature[]>([])
|
||||
let features = new StaticFeatureSource(featuresStore)
|
||||
ShowDataLayer.showMultipleLayers(map, features, [...layersNoFixme, fixme], {
|
||||
|
@ -62,19 +62,19 @@
|
|||
})
|
||||
|
||||
let osmConnection = new OsmConnection()
|
||||
let inspectedContributors: UIEventSource<{
|
||||
name: string,
|
||||
visitedTime: string,
|
||||
label: string
|
||||
}[]> = UIEventSource.asObject(
|
||||
osmConnection.getPreference("spied-upon-users"), [])
|
||||
let inspectedContributors: UIEventSource<
|
||||
{
|
||||
name: string
|
||||
visitedTime: string
|
||||
label: string
|
||||
}[]
|
||||
> = UIEventSource.asObject(osmConnection.getPreference("spied-upon-users"), [])
|
||||
|
||||
async function load() {
|
||||
const user = username.data
|
||||
if (user.indexOf(";") < 0) {
|
||||
|
||||
const inspectedData = inspectedContributors.data
|
||||
const previousEntry = inspectedData.find(e => e.name === user)
|
||||
const previousEntry = inspectedData.find((e) => e.name === user)
|
||||
if (previousEntry) {
|
||||
previousEntry.visitedTime = new Date().toISOString()
|
||||
} else {
|
||||
|
@ -89,7 +89,11 @@
|
|||
|
||||
step.setData("loading")
|
||||
featuresStore.set([])
|
||||
const overpass = new Overpass(undefined, user.split(";").map(user => "nw(user_touched:\"" + user + "\");"), Constants.defaultOverpassUrls[0])
|
||||
const overpass = new Overpass(
|
||||
undefined,
|
||||
user.split(";").map((user) => 'nw(user_touched:"' + user + '");'),
|
||||
Constants.defaultOverpassUrls[0]
|
||||
)
|
||||
if (!maplibremap.bounds.data) {
|
||||
return
|
||||
}
|
||||
|
@ -117,11 +121,10 @@
|
|||
const t = Translations.t.inspector
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col w-full h-full">
|
||||
|
||||
<div class="flex gap-x-2 items-center low-interaction p-2">
|
||||
<MagnifyingGlassCircle class="w-12 h-12" />
|
||||
<h1 class="flex-shrink-0 m-0 mx-2">
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<div class="low-interaction flex items-center gap-x-2 p-2">
|
||||
<MagnifyingGlassCircle class="h-12 w-12" />
|
||||
<h1 class="m-0 mx-2 flex-shrink-0">
|
||||
<Tr t={t.title} />
|
||||
</h1>
|
||||
<ValidatedInput type="string" value={username} on:submit={() => load()} />
|
||||
|
@ -141,16 +144,16 @@
|
|||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<button class:primary={mode === "map"} on:click={() => mode = "map"}>
|
||||
<button class:primary={mode === "map"} on:click={() => (mode = "map")}>
|
||||
<Tr t={t.mapView} />
|
||||
</button>
|
||||
<button class:primary={mode === "table"} on:click={() => mode = "table"}>
|
||||
<button class:primary={mode === "table"} on:click={() => (mode = "table")}>
|
||||
<Tr t={t.tableView} />
|
||||
</button>
|
||||
<button class:primary={mode === "aggregate"} on:click={() => mode = "aggregate"}>
|
||||
<button class:primary={mode === "aggregate"} on:click={() => (mode = "aggregate")}>
|
||||
<Tr t={t.aggregateView} />
|
||||
</button>
|
||||
<button class:primary={mode === "images"} on:click={() => mode = "images"}>
|
||||
<button class:primary={mode === "images"} on:click={() => (mode = "images")}>
|
||||
<Tr t={t.images} />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -167,32 +170,35 @@
|
|||
width="w-full md:w-6/12 lg:w-5/12 xl:w-4/12"
|
||||
rightOffset="inset-y-0 right-0"
|
||||
transitionParams={{
|
||||
x: 640,
|
||||
duration: 0,
|
||||
easing: linear,
|
||||
}}
|
||||
x: 640,
|
||||
duration: 0,
|
||||
easing: linear,
|
||||
}}
|
||||
divClass="overflow-y-auto z-50 bg-white"
|
||||
hidden={$selectedElement === undefined}
|
||||
on:close={() => {
|
||||
selectedElement.setData(undefined)
|
||||
}}
|
||||
selectedElement.setData(undefined)
|
||||
}}
|
||||
>
|
||||
|
||||
<TitledPanel>
|
||||
<div slot="title" class="flex justify-between">
|
||||
|
||||
<a target="_blank" rel="noopener"
|
||||
href={"https://osm.org/"+$selectedElement.properties.id}>{$selectedElement.properties.id}</a>
|
||||
<XCircleIcon class="w-6 h-6" on:click={() => selectedElement.set(undefined)} />
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href={"https://osm.org/" + $selectedElement.properties.id}
|
||||
>
|
||||
{$selectedElement.properties.id}
|
||||
</a>
|
||||
<XCircleIcon class="h-6 w-6" on:click={() => selectedElement.set(undefined)} />
|
||||
</div>
|
||||
|
||||
<History onlyShowChangesBy={$username} id={$selectedElement.properties.id}></History>
|
||||
<History onlyShowChangesBy={$username} id={$selectedElement.properties.id} />
|
||||
</TitledPanel>
|
||||
</Drawer>
|
||||
{/if}
|
||||
|
||||
<div class="flex-grow overflow-hidden m-1 rounded-xl">
|
||||
<MaplibreMap map={map} mapProperties={maplibremap} autorecovery={true} />
|
||||
<div class="m-1 flex-grow overflow-hidden rounded-xl">
|
||||
<MaplibreMap {map} mapProperties={maplibremap} autorecovery={true} />
|
||||
</div>
|
||||
{:else if mode === "table"}
|
||||
<div class="m-2 h-full overflow-y-auto">
|
||||
|
@ -213,7 +219,13 @@
|
|||
|
||||
<Page shown={showPreviouslyVisited}>
|
||||
<div slot="header">Earlier inspected constributors</div>
|
||||
<PreviouslySpiedUsers {osmConnection} {inspectedContributors} on:selectUser={(e) => {
|
||||
username.set(e.detail); load();showPreviouslyVisited.set(false)
|
||||
}} />
|
||||
<PreviouslySpiedUsers
|
||||
{osmConnection}
|
||||
{inspectedContributors}
|
||||
on:selectUser={(e) => {
|
||||
username.set(e.detail)
|
||||
load()
|
||||
showPreviouslyVisited.set(false)
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import Maproulette from "../../Logic/Maproulette"
|
||||
import Maproulette, { maprouletteStatus } from "../../Logic/Maproulette"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
|
||||
/**
|
||||
|
@ -38,11 +38,11 @@
|
|||
async function apply() {
|
||||
const maproulette_id = tags.data[maproulette_id_key] ?? tags.data.mr_taskId ?? tags.data.id
|
||||
try {
|
||||
await Maproulette.singleton.closeTask(Number(maproulette_id), Number(statusToSet), {
|
||||
tags: `MapComplete MapComplete:${state.theme.id}`,
|
||||
const statusIndex = Maproulette.codeToIndex(statusToSet) ?? Number(statusToSet)
|
||||
await Maproulette.singleton.closeTask(Number(maproulette_id), statusIndex, state, {
|
||||
comment: feedback,
|
||||
})
|
||||
tags.data["mr_taskStatus"] = Maproulette.STATUS_MEANING[Number(statusToSet)]
|
||||
tags.data["mr_taskStatus"] = maprouletteStatus[statusIndex]
|
||||
tags.data.status = statusToSet
|
||||
tags.ping()
|
||||
} catch (e) {
|
||||
|
|
|
@ -951,13 +951,13 @@ export class ToTextualDescription {
|
|||
* const oh = new opening_hours("mon 12:00-16:00")
|
||||
* const ranges = OH.createRangesForApplicableWeek(oh)
|
||||
* const tr = ToTextualDescription.createTextualDescriptionFor(oh, ranges.ranges)
|
||||
* tr.textFor("en") // => "On monday from 12:00 till 16:00"
|
||||
* tr.textFor("en") // => "On Monday from 12:00 till 16:00"
|
||||
* tr.textFor("nl") // => "Op maandag van 12:00 tot 16:00"
|
||||
*
|
||||
* const oh = new opening_hours("mon 12:00-16:00; tu 13:00-14:00")
|
||||
* const ranges = OH.createRangesForApplicableWeek(oh)
|
||||
* const tr = ToTextualDescription.createTextualDescriptionFor(oh, ranges.ranges)
|
||||
* tr.textFor("en") // => "On monday from 12:00 till 16:00. On tuesday from 13:00 till 14:00"
|
||||
* tr.textFor("en") // => "On Monday from 12:00 till 16:00. On Tuesday from 13:00 till 14:00"
|
||||
* tr.textFor("nl") // => "Op maandag van 12:00 tot 16:00. Op dinsdag van 13:00 tot 14:00"
|
||||
*/
|
||||
public static createTextualDescriptionFor(
|
||||
|
|
|
@ -3,15 +3,17 @@
|
|||
import { Stores } from "../../Logic/UIEventSource"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
|
||||
/**
|
||||
* Shows _all_ disabled questions
|
||||
*/
|
||||
export let state
|
||||
let layers = state.layout.layers.filter((l) => l.isNormal())
|
||||
export let state: ThemeViewState
|
||||
let layers = state.theme.layers.filter((l) => l.isNormal())
|
||||
|
||||
let allDisabled = Stores.concat<string>(
|
||||
layers.map((l) => state.userRelatedState.getThemeDisabled(state.layout.id, l.id))
|
||||
layers.map((l) => state.userRelatedState.getThemeDisabled(state.theme.id, l.id))
|
||||
).map((l) => [].concat(...l))
|
||||
const t = Translations.t.general.questions
|
||||
</script>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
/**
|
||||
* Gives an overview of questions which are disabled for the given theme
|
||||
*/
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
@ -13,7 +12,7 @@
|
|||
export let layer: LayerConfig
|
||||
export let state: ThemeViewState
|
||||
|
||||
let disabledQuestions = state.userRelatedState.getThemeDisabled(state.layout.id, layer.id)
|
||||
let disabledQuestions = state.userRelatedState.getThemeDisabled(state.theme.id, layer.id)
|
||||
|
||||
function getQuestion(id: string): Translation {
|
||||
return layer.tagRenderings.find((q) => q.id === id).question.Subs({})
|
||||
|
|
|
@ -9,6 +9,7 @@ import { PointImportFlowArguments, PointImportFlowState } from "./PointImportFlo
|
|||
import { Utils } from "../../../Utils"
|
||||
import { ImportFlowUtils } from "./ImportFlow"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { GeoOperations } from "../../../Logic/GeoOperations"
|
||||
|
||||
/**
|
||||
* The wrapper to make the special visualisation for the PointImportFlow
|
||||
|
@ -44,6 +45,10 @@ export class PointImportButtonViz implements SpecialVisualization {
|
|||
name: "maproulette_id",
|
||||
doc: "The property name of the maproulette_id - this is probably `mr_taskId`. If given, the maproulette challenge will be marked as fixed. Only use this if part of a maproulette-layer.",
|
||||
},
|
||||
{
|
||||
name: "to_point",
|
||||
doc: "If set, a feature will be converted to a centerpoint",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -53,8 +58,14 @@ export class PointImportButtonViz implements SpecialVisualization {
|
|||
argument: string[],
|
||||
feature: Feature
|
||||
): BaseUIElement {
|
||||
const to_point_index = this.args.findIndex((arg) => arg.name === "to_point")
|
||||
const summarizePointArg = argument[to_point_index].toLowerCase()
|
||||
if (feature.geometry.type !== "Point") {
|
||||
return Translations.t.general.add.import.wrongType.SetClass("alert")
|
||||
if (summarizePointArg !== "no" && summarizePointArg !== "false") {
|
||||
feature = GeoOperations.centerpoint(feature)
|
||||
} else {
|
||||
return Translations.t.general.add.import.wrongType.SetClass("alert")
|
||||
}
|
||||
}
|
||||
const baseArgs: PointImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, baseArgs)
|
||||
|
|
|
@ -19,7 +19,6 @@ export interface PointImportFlowArguments extends ImportFlowArguments {
|
|||
|
||||
export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
|
||||
public readonly startCoordinate: [number, number]
|
||||
private readonly _originalFeature: Feature<Point>
|
||||
|
||||
constructor(
|
||||
state: SpecialVisualizationState,
|
||||
|
@ -29,7 +28,6 @@ export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
|
|||
originalFeatureTags: UIEventSource<Record<string, string>>
|
||||
) {
|
||||
super(state, args, tagsToApply, originalFeatureTags)
|
||||
this._originalFeature = originalFeature
|
||||
this.startCoordinate = GeoOperations.centerpointCoordinates(originalFeature)
|
||||
}
|
||||
|
||||
|
@ -80,7 +78,7 @@ export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
|
|||
originalFeatureTags.ping()
|
||||
}
|
||||
|
||||
let maproulette_id = originalFeatureTags.data[this.args.maproulette_id]
|
||||
const maproulette_id = originalFeatureTags.data[this.args.maproulette_id]
|
||||
if (maproulette_id !== undefined) {
|
||||
if (this.state.featureSwitchIsTesting.data) {
|
||||
console.log(
|
||||
|
@ -90,7 +88,11 @@ export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
|
|||
)
|
||||
} else {
|
||||
console.log("Marking maproulette task as fixed")
|
||||
await Maproulette.singleton.closeTask(Number(maproulette_id))
|
||||
await Maproulette.singleton.closeTask(
|
||||
Number(maproulette_id),
|
||||
Maproulette.STATUS_FIXED,
|
||||
this.state
|
||||
)
|
||||
originalFeatureTags.data["mr_taskStatus"] = "Fixed"
|
||||
originalFeatureTags.ping()
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
<LoginToggle ignoreLoading={true} hiddenFail {state}>
|
||||
{#if $isFavourite}
|
||||
<button
|
||||
class="soft no-image-background m-0 h-8 w-8 p-0"
|
||||
|
|
|
@ -159,9 +159,14 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
const maproulette_id = tags.data[maproulette_id_key]
|
||||
const maproulette_feature = state.indexedFeatures.featuresById.data.get(maproulette_id)
|
||||
const maproulette_task_id = Number(maproulette_feature.properties.mr_taskId)
|
||||
await Maproulette.singleton.closeTask(maproulette_task_id, Maproulette.STATUS_FIXED, {
|
||||
comment: "Tags are copied onto " + targetId + " with MapComplete",
|
||||
})
|
||||
await Maproulette.singleton.closeTask(
|
||||
maproulette_task_id,
|
||||
Maproulette.STATUS_FIXED,
|
||||
state,
|
||||
{
|
||||
comment: "Tags are copied onto " + targetId + " with MapComplete",
|
||||
}
|
||||
)
|
||||
maproulette_feature.properties["mr_taskStatus"] = "Fixed"
|
||||
state.featureProperties.getStore(maproulette_id).ping()
|
||||
}
|
||||
|
|
|
@ -85,6 +85,8 @@
|
|||
}
|
||||
let answerId = "answer-" + Utils.randomString(5)
|
||||
let debug = state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false)
|
||||
|
||||
let apiState: Store<string> = state?.osmConnection?.apiIsOnline ?? new ImmutableStore("online")
|
||||
</script>
|
||||
|
||||
<div bind:this={htmlElem} class={twMerge(clss, "tr-" + config.id)}>
|
||||
|
@ -126,7 +128,7 @@
|
|||
{layer}
|
||||
extraClasses="my-2"
|
||||
/>
|
||||
{#if !editingEnabled || $editingEnabled}
|
||||
{#if (!editingEnabled || $editingEnabled) && $apiState !== "readonly" && $apiState !== "offline"}
|
||||
<EditButton
|
||||
arialabel={config.editButtonAriaLabel}
|
||||
ariaLabelledBy={answerId}
|
||||
|
|
|
@ -355,9 +355,11 @@
|
|||
disabledInTheme.set(newList)
|
||||
menuIsOpened.set(false)
|
||||
}
|
||||
|
||||
let apiState = state.osmConnection.apiIsOnline
|
||||
</script>
|
||||
|
||||
{#if question !== undefined}
|
||||
{#if question !== undefined && $apiState !== "readonly" && $apiState !== "offline"}
|
||||
<div class={clss}>
|
||||
{#if layer.isNormal()}
|
||||
<LoginToggle {state}>
|
||||
|
|
|
@ -12,7 +12,6 @@ import { ExportableMap, MapProperties } from "../Models/MapProperties"
|
|||
import LayerState from "../Logic/State/LayerState"
|
||||
import { Feature, Geometry, Point, Polygon } from "geojson"
|
||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
|
||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
|
@ -22,14 +21,12 @@ import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
|||
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
||||
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
|
||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||
import { SummaryTileSourceRewriter } from "../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource"
|
||||
import ThemeSource from "../Logic/FeatureSource/Sources/ThemeSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import ShowDataLayer from "./Map/ShowDataLayer"
|
||||
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
|
||||
import SearchState from "../Logic/State/SearchState"
|
||||
import UserRelatedState, { OptionallySyncedHistory } from "../Logic/State/UserRelatedState"
|
||||
import GeocodeResult from "./Search/GeocodeResult.svelte"
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
||||
import FeaturePropertiesStore from "../Logic/FeatureSource/Actors/FeaturePropertiesStore"
|
||||
|
||||
/**
|
||||
|
|
|
@ -112,7 +112,7 @@ class NearbyImageVis implements SpecialVisualization {
|
|||
{
|
||||
name: "readonly",
|
||||
required: false,
|
||||
doc: "If 'readonly', will not show the 'link'-button",
|
||||
doc: "If 'readonly' or 'yes', will not show the 'link'-button",
|
||||
},
|
||||
]
|
||||
docs =
|
||||
|
@ -128,7 +128,7 @@ class NearbyImageVis implements SpecialVisualization {
|
|||
layer: LayerConfig
|
||||
): SvelteUIElement {
|
||||
const isOpen = args[0] === "open"
|
||||
const readonly = args[1] === "readonly"
|
||||
const readonly = args[1] === "readonly" || args[1] === "yes"
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||
return new SvelteUIElement(isOpen ? NearbyImages : NearbyImagesCollapsed, {
|
||||
tags,
|
||||
|
@ -744,13 +744,14 @@ export default class SpecialVisualizations {
|
|||
required: false,
|
||||
},
|
||||
],
|
||||
constr: (state, tags, args) => {
|
||||
constr: (state, tags, args, feature) => {
|
||||
const targetKey = args[0] === "" ? undefined : args[0]
|
||||
const noBlur = args[3]?.toLowerCase()?.trim()
|
||||
return new SvelteUIElement(UploadImage, {
|
||||
state,
|
||||
tags,
|
||||
targetKey,
|
||||
feature,
|
||||
labelText: args[1],
|
||||
image: args[2],
|
||||
noBlur: noBlur === "true" || noBlur === "yes",
|
||||
|
@ -1093,7 +1094,7 @@ export default class SpecialVisualizations {
|
|||
tags
|
||||
.map((tags) => tags[args[0]])
|
||||
.map((commentsStr) => {
|
||||
const comments: any[] = JSON.parse(commentsStr)
|
||||
const comments: { text: string }[] = JSON.parse(commentsStr)
|
||||
const startLoc = Number(args[1] ?? 0)
|
||||
if (!isNaN(startLoc) && startLoc > 0) {
|
||||
comments.splice(0, startLoc)
|
||||
|
@ -1852,69 +1853,80 @@ export default class SpecialVisualizations {
|
|||
const key = argument[0] ?? "website"
|
||||
const useProxy = argument[1] !== "no"
|
||||
const readonly = argument[3] === "readonly"
|
||||
const isClosed = (arguments[4] ?? "yes") === "yes"
|
||||
const isClosed = (argument[4] ?? "yes") === "yes"
|
||||
|
||||
const url = tags
|
||||
.mapD((tags) => {
|
||||
if (!tags._country || !tags[key] || tags[key] === "undefined") {
|
||||
return null
|
||||
}
|
||||
return JSON.stringify({ url: tags[key], country: tags._country })
|
||||
})
|
||||
.mapD((data) => JSON.parse(data))
|
||||
const sourceUrl: Store<string | undefined> = url.mapD((url) => url.url)
|
||||
const countryStore: Store<string | undefined> = tags.mapD(
|
||||
(tags) => tags._country
|
||||
)
|
||||
const sourceUrl: Store<string | undefined> = tags.mapD((tags) => {
|
||||
if (!tags[key] || tags[key] === "undefined") {
|
||||
return null
|
||||
}
|
||||
return tags[key]
|
||||
})
|
||||
const externalData: Store<{ success: GeoJsonProperties } | { error: any }> =
|
||||
url.bindD(({ url, country }) => {
|
||||
if (url.startsWith("https://data.velopark.be/")) {
|
||||
sourceUrl.bindD(
|
||||
(url) => {
|
||||
const country = countryStore.data
|
||||
if (url.startsWith("https://data.velopark.be/")) {
|
||||
return Stores.FromPromiseWithErr(
|
||||
(async () => {
|
||||
try {
|
||||
const loadAll =
|
||||
layer.id.toLowerCase().indexOf("maproulette") >=
|
||||
0 // Dirty hack
|
||||
const features =
|
||||
await LinkedDataLoader.fetchVeloparkEntry(
|
||||
url,
|
||||
loadAll
|
||||
)
|
||||
const feature =
|
||||
features.find(
|
||||
(f) => f.properties["ref:velopark"] === url
|
||||
) ?? features[0]
|
||||
const properties = feature.properties
|
||||
properties["ref:velopark"] = url
|
||||
console.log(
|
||||
"Got properties from velopark:",
|
||||
properties
|
||||
)
|
||||
return properties
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
})()
|
||||
)
|
||||
}
|
||||
if (country === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return Stores.FromPromiseWithErr(
|
||||
(async () => {
|
||||
try {
|
||||
const loadAll =
|
||||
layer.id.toLowerCase().indexOf("maproulette") >= 0 // Dirty hack
|
||||
const features =
|
||||
await LinkedDataLoader.fetchVeloparkEntry(
|
||||
url,
|
||||
loadAll
|
||||
)
|
||||
const feature =
|
||||
features.find(
|
||||
(f) => f.properties["ref:velopark"] === url
|
||||
) ?? features[0]
|
||||
const properties = feature.properties
|
||||
properties["ref:velopark"] = url
|
||||
console.log("Got properties from velopark:", properties)
|
||||
return properties
|
||||
return await LinkedDataLoader.fetchJsonLd(
|
||||
url,
|
||||
{ country },
|
||||
useProxy ? "proxy" : "fetch-lod"
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
console.log(
|
||||
"Could not get with proxy/download LOD, attempting to download directly. Error for ",
|
||||
url,
|
||||
"is",
|
||||
e
|
||||
)
|
||||
return await LinkedDataLoader.fetchJsonLd(
|
||||
url,
|
||||
{ country },
|
||||
"fetch-raw"
|
||||
)
|
||||
}
|
||||
})()
|
||||
)
|
||||
}
|
||||
return Stores.FromPromiseWithErr(
|
||||
(async () => {
|
||||
try {
|
||||
return await LinkedDataLoader.fetchJsonLd(
|
||||
url,
|
||||
{ country },
|
||||
useProxy ? "proxy" : "fetch-lod"
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"Could not get with proxy/download LOD, attempting to download directly. Error for ",
|
||||
url,
|
||||
"is",
|
||||
e
|
||||
)
|
||||
return await LinkedDataLoader.fetchJsonLd(
|
||||
url,
|
||||
{ country },
|
||||
"fetch-raw"
|
||||
)
|
||||
}
|
||||
})()
|
||||
)
|
||||
})
|
||||
},
|
||||
[countryStore]
|
||||
)
|
||||
|
||||
externalData.addCallbackAndRunD((lod) =>
|
||||
console.log("linked_data_from_website received the following data:", lod)
|
||||
|
@ -1932,7 +1944,7 @@ export default class SpecialVisualizations {
|
|||
collapsed: isClosed,
|
||||
}),
|
||||
undefined,
|
||||
url.map((url) => !!url)
|
||||
sourceUrl.map((url) => !!url)
|
||||
)
|
||||
},
|
||||
},
|
||||
|
@ -1987,13 +1999,7 @@ export default class SpecialVisualizations {
|
|||
funcName: "pending_changes",
|
||||
docs: "A module showing the pending changes, with the option to clear the pending changes",
|
||||
args: [],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
constr(state: SpecialVisualizationState): BaseUIElement {
|
||||
return new SvelteUIElement(PendingChangesIndicator, { state, compact: false })
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import StatusIcon from "./StatusIcon.svelte"
|
||||
import type { MCService } from "./MCService"
|
||||
import ServiceIndicator from "./ServiceIndicator.svelte"
|
||||
|
@ -203,6 +203,30 @@
|
|||
})
|
||||
}
|
||||
|
||||
{
|
||||
const summaryTileServer = Constants.VectorTileServer
|
||||
// "mvt_layer_server": "https://cache.mapcomplete.org/public.{type}_{layer}/{z}/{x}/{y}.pbf",
|
||||
const status = testDownload(
|
||||
Utils.SubstituteKeys(summaryTileServer, {
|
||||
type: "pois",
|
||||
layer: "food",
|
||||
z: 14,
|
||||
x: 8848,
|
||||
y: 5828,
|
||||
})
|
||||
)
|
||||
services.push({
|
||||
name: summaryTileServer,
|
||||
status: status.mapD((s) => {
|
||||
if (s["error"]) {
|
||||
return "offline"
|
||||
}
|
||||
return "online"
|
||||
}),
|
||||
message: new ImmutableStore("See SettingUpPSQL.md to fix"),
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
const s = Constants.countryCoderEndpoint
|
||||
const status = testDownload(s + "/0.0.0.json")
|
||||
|
|
|
@ -34,6 +34,9 @@
|
|||
>tags?.GPSLongitude?.value
|
||||
const exifLat = latD + latM / 60 + latS / (3600 * latSDenom)
|
||||
const exifLon = lonD + lonM / 60 + lonS / (3600 * lonSDenom)
|
||||
const directValueLat = tags?.GPSLatitude?.description
|
||||
const directValueLon = tags?.GPSLongitude?.description
|
||||
|
||||
if (
|
||||
typeof exifLat === "number" &&
|
||||
!isNaN(exifLat) &&
|
||||
|
@ -43,11 +46,29 @@
|
|||
) {
|
||||
lat = exifLat
|
||||
lon = exifLon
|
||||
if (tags?.GPSLatitudeRef?.value?.[0] === "S") {
|
||||
lat *= -1
|
||||
}
|
||||
if (tags?.GPSLongitudeRef?.value?.[0] === "W") {
|
||||
lon *= -1
|
||||
}
|
||||
l("Using EXIFLAT + EXIFLON")
|
||||
} else {
|
||||
l("NOT using exifLat and exifLon: invalid value detected")
|
||||
}
|
||||
l("Lat and lon are", lat, lon)
|
||||
l(
|
||||
"ref lat is",
|
||||
tags?.GPSLatitudeRef?.description,
|
||||
JSON.stringify(tags?.GPSLatitudeRef?.value)
|
||||
)
|
||||
l(
|
||||
"ref lon is",
|
||||
tags?.GPSLongitudeRef?.description,
|
||||
JSON.stringify(tags?.GPSLongitudeRef?.value)
|
||||
)
|
||||
|
||||
l("Direct values are", directValueLat, directValueLon, "corrected:", lat, lon)
|
||||
l("Datetime value is", JSON.stringify(tags.DateTime))
|
||||
const [date, time] = tags.DateTime.value[0].split(" ")
|
||||
datetime = new Date(date.replaceAll(":", "-") + "T" + time).toISOString()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
|
@ -42,11 +42,9 @@
|
|||
import DrawerLeft from "./Base/DrawerLeft.svelte"
|
||||
import DrawerRight from "./Base/DrawerRight.svelte"
|
||||
import SearchResults from "./Search/SearchResults.svelte"
|
||||
import { CloseButton } from "flowbite-svelte"
|
||||
import Hash from "../Logic/Web/Hash"
|
||||
import Searchbar from "./Base/Searchbar.svelte"
|
||||
import ChevronRight from "@babeard/svelte-heroicons/mini/ChevronRight"
|
||||
import ChevronLeft from "@babeard/svelte-heroicons/solid/ChevronLeft"
|
||||
import { Drawer } from "flowbite-svelte"
|
||||
import { linear } from "svelte/easing"
|
||||
|
||||
|
@ -167,6 +165,8 @@
|
|||
const animation = mlmap.keyboard?.keydown(e)
|
||||
animation?.cameraAnimation(mlmap)
|
||||
}
|
||||
|
||||
let apiState = state?.osmConnection?.apiIsOnline ?? new ImmutableStore("online")
|
||||
</script>
|
||||
|
||||
<main>
|
||||
|
@ -175,7 +175,7 @@
|
|||
<MaplibreMap map={maplibremap} mapProperties={mapproperties} autorecovery={true} />
|
||||
</div>
|
||||
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
<LoginToggle ignoreLoading={true} silentFail {state}>
|
||||
{#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $visualFeedback}
|
||||
<!-- Don't use h-full: h-full does _not_ include the area under the URL-bar, which offsets the crosshair a bit -->
|
||||
<div
|
||||
|
@ -201,8 +201,8 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- bottom controls -->
|
||||
<div class="pointer-events-none absolute bottom-0 left-0 mb-4 w-screen">
|
||||
<!-- bottom controls -->
|
||||
<div class="flex w-full items-end justify-between px-4">
|
||||
<div class="flex flex-col">
|
||||
<If condition={featureSwitches.featureSwitchEnableLogin}>
|
||||
|
@ -218,9 +218,10 @@
|
|||
{#if $currentZoom < Constants.minZoomLevelToAddNewPoint}
|
||||
<Tr t={Translations.t.general.add.zoomInFurther} />
|
||||
{:else if state.theme.hasPresets()}
|
||||
✨ <Tr t={Translations.t.general.add.title} />
|
||||
✨
|
||||
<Tr t={Translations.t.general.add.title} />
|
||||
{:else}
|
||||
<Tr t={Translations.t.notes.addAComment} />
|
||||
<Tr t={Translations.t.notes.createNote} />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
@ -419,6 +420,9 @@
|
|||
<If condition={state.featureSwitches.featureSwitchFakeUser}>
|
||||
<div class="alert w-fit">Faking a user (Testmode)</div>
|
||||
</If>
|
||||
{#if $apiState !== "online"}
|
||||
<div class="alert w-fit">API is {$apiState}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col items-center justify-center">
|
||||
|
@ -429,11 +433,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<DrawerLeft shown={state.guistate.pageStates.menu}>
|
||||
<div class="h-screen overflow-y-auto">
|
||||
<MenuDrawer onlyLink={true} {state} />
|
||||
</div>
|
||||
</DrawerLeft>
|
||||
<div class="h-full overflow-hidden">
|
||||
<DrawerLeft shown={state.guistate.pageStates.menu}>
|
||||
<div class="h-screen overflow-y-auto">
|
||||
<MenuDrawer onlyLink={true} {state} />
|
||||
</div>
|
||||
</DrawerLeft>
|
||||
</div>
|
||||
|
||||
{#if $selectedElement !== undefined && $selectedLayer !== undefined && !$selectedLayer.popupInFloatover}
|
||||
<!-- right modal with the selected element view -->
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"contributors": [
|
||||
{
|
||||
"commits": 8650,
|
||||
"commits": 8779,
|
||||
"contributor": "Pieter Vander Vennet"
|
||||
},
|
||||
{
|
||||
"commits": 495,
|
||||
"commits": 505,
|
||||
"contributor": "Robin van der Linde"
|
||||
},
|
||||
{
|
||||
|
@ -80,6 +80,10 @@
|
|||
"commits": 18,
|
||||
"contributor": "Arno Deceuninck"
|
||||
},
|
||||
{
|
||||
"commits": 17,
|
||||
"contributor": "Midgard"
|
||||
},
|
||||
{
|
||||
"commits": 17,
|
||||
"contributor": "pgm-chardelv1"
|
||||
|
@ -128,10 +132,6 @@
|
|||
"commits": 9,
|
||||
"contributor": "Codain"
|
||||
},
|
||||
{
|
||||
"commits": 9,
|
||||
"contributor": "Midgard"
|
||||
},
|
||||
{
|
||||
"commits": 8,
|
||||
"contributor": "Binnette"
|
||||
|
|
|
@ -10653,7 +10653,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=aerialway",
|
||||
"then": "aerialway - Various forms of transport for passengers and goods that use wires, including cable cars, gondolas, chair lifts, drag lifts, and zip lines. "
|
||||
"then": "aerialway - Various forms of transport for passengers and goods that use wires, including cable cars, gondolas, chair lifts, drag lifts, and zip lines."
|
||||
},
|
||||
{
|
||||
"if": "value=ambulancestation",
|
||||
|
@ -10661,7 +10661,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=animal_shelter",
|
||||
"then": "animal_shelter - An animal shelter is a facility where animals in trouble are brought and facility's staff (volunteers or not) feeds them and cares of them, rehabilitating and healing them if necessary. This definition includes kennels for abandoned dogs, catteries for abandoned cats, shelters for other abandoned pets and wildlife recovery centres. "
|
||||
"then": "animal_shelter - An animal shelter is a facility where animals in trouble are brought and facility's staff (volunteers or not) feeds them and cares of them, rehabilitating and healing them if necessary. This definition includes kennels for abandoned dogs, catteries for abandoned cats, shelters for other abandoned pets and wildlife recovery centres."
|
||||
},
|
||||
{
|
||||
"if": "value=artwork",
|
||||
|
@ -10673,7 +10673,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=assisted_repair",
|
||||
"then": "assisted_repair - A self-assisted workshop is a location where people can come and repair their goods with help of volunteers and with the tools available at the given location. A repair café is a type of event organized regularly along the same principles."
|
||||
"then": "assisted_repair - A self-assisted workshop is a location where people can come and repair their goods with help of volunteers and with the tools available at the given location. A repair café is a type of event organized regularly along the same principles."
|
||||
},
|
||||
{
|
||||
"if": "value=atm",
|
||||
|
@ -10691,6 +10691,10 @@
|
|||
"if": "value=bbq",
|
||||
"then": "bbq - A permanently installed barbecue, typically accessible to the public."
|
||||
},
|
||||
{
|
||||
"if": "value=beehive",
|
||||
"then": "beehive - Layer showing beehives"
|
||||
},
|
||||
{
|
||||
"if": "value=bench",
|
||||
"then": "bench - A bench is a wooden, metal, stone, … surface where a human can sit. This layers visualises them and asks a few questions about them."
|
||||
|
@ -10745,7 +10749,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=brothel",
|
||||
"then": "brothel - An establishment specifically dedicated to prostitution. "
|
||||
"then": "brothel - An establishment specifically dedicated to prostitution."
|
||||
},
|
||||
{
|
||||
"if": "value=cafe_pub",
|
||||
|
@ -10765,7 +10769,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=cinema",
|
||||
"then": "cinema - A place showing movies (films), generally open to the public for a fee. Commonly referred to as a movie theater in the US"
|
||||
"then": "cinema - A place showing movies (films), generally open to the public for a fee. Commonly referred to as a movie theater in the US."
|
||||
},
|
||||
{
|
||||
"if": "value=climbing",
|
||||
|
@ -10983,6 +10987,10 @@
|
|||
"if": "value=information_board",
|
||||
"then": "information_board - A layer showing touristical, road side information boards (e.g. giving information about the landscape, a building, a feature, a map, …)"
|
||||
},
|
||||
{
|
||||
"if": "value=insect_hotel",
|
||||
"then": "insect_hotel - Layer showing insect hotels"
|
||||
},
|
||||
{
|
||||
"if": "value=item_with_image",
|
||||
"then": "item_with_image - All items with an image. All alone, not a layer which is relevant for any MapComplete theme, as it is a random collection of items. However, when put into the databank, this allows to quickly fetch (the URL of) pictures nearby a different object, to quickly link this"
|
||||
|
|
|
@ -601,7 +601,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=aerialway",
|
||||
"then": "<b>aerialway</b> (builtin) - Various forms of transport for passengers and goods that use wires, including cable cars, gondolas, chair lifts, drag lifts, and zip lines. "
|
||||
"then": "<b>aerialway</b> (builtin) - Various forms of transport for passengers and goods that use wires, including cable cars, gondolas, chair lifts, drag lifts, and zip lines."
|
||||
},
|
||||
{
|
||||
"if": "value=ambulancestation",
|
||||
|
@ -609,7 +609,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=animal_shelter",
|
||||
"then": "<b>animal_shelter</b> (builtin) - An animal shelter is a facility where animals in trouble are brought and facility's staff (volunteers or not) feeds them and cares of them, rehabilitating and healing them if necessary. This definition includes kennels for abandoned dogs, catteries for abandoned cats, shelters for other abandoned pets and wildlife recovery centres. "
|
||||
"then": "<b>animal_shelter</b> (builtin) - An animal shelter is a facility where animals in trouble are brought and facility's staff (volunteers or not) feeds them and cares of them, rehabilitating and healing them if necessary. This definition includes kennels for abandoned dogs, catteries for abandoned cats, shelters for other abandoned pets and wildlife recovery centres."
|
||||
},
|
||||
{
|
||||
"if": "value=artwork",
|
||||
|
@ -621,7 +621,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=assisted_repair",
|
||||
"then": "<b>assisted_repair</b> (builtin) - A self-assisted workshop is a location where people can come and repair their goods with help of volunteers and with the tools available at the given location. A repair café is a type of event organized regularly along the same principles."
|
||||
"then": "<b>assisted_repair</b> (builtin) - A self-assisted workshop is a location where people can come and repair their goods with help of volunteers and with the tools available at the given location. A repair café is a type of event organized regularly along the same principles."
|
||||
},
|
||||
{
|
||||
"if": "value=atm",
|
||||
|
@ -639,6 +639,10 @@
|
|||
"if": "value=bbq",
|
||||
"then": "<b>bbq</b> (builtin) - A permanently installed barbecue, typically accessible to the public."
|
||||
},
|
||||
{
|
||||
"if": "value=beehive",
|
||||
"then": "<b>beehive</b> (builtin) - Layer showing beehives"
|
||||
},
|
||||
{
|
||||
"if": "value=bench",
|
||||
"then": "<b>bench</b> (builtin) - A bench is a wooden, metal, stone, … surface where a human can sit. This layers visualises them and asks a few questions about them."
|
||||
|
@ -693,7 +697,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=brothel",
|
||||
"then": "<b>brothel</b> (builtin) - An establishment specifically dedicated to prostitution. "
|
||||
"then": "<b>brothel</b> (builtin) - An establishment specifically dedicated to prostitution."
|
||||
},
|
||||
{
|
||||
"if": "value=cafe_pub",
|
||||
|
@ -713,7 +717,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=cinema",
|
||||
"then": "<b>cinema</b> (builtin) - A place showing movies (films), generally open to the public for a fee. Commonly referred to as a movie theater in the US"
|
||||
"then": "<b>cinema</b> (builtin) - A place showing movies (films), generally open to the public for a fee. Commonly referred to as a movie theater in the US."
|
||||
},
|
||||
{
|
||||
"if": "value=climbing",
|
||||
|
@ -931,6 +935,10 @@
|
|||
"if": "value=information_board",
|
||||
"then": "<b>information_board</b> (builtin) - A layer showing touristical, road side information boards (e.g. giving information about the landscape, a building, a feature, a map, …)"
|
||||
},
|
||||
{
|
||||
"if": "value=insect_hotel",
|
||||
"then": "<b>insect_hotel</b> (builtin) - Layer showing insect hotels"
|
||||
},
|
||||
{
|
||||
"if": "value=item_with_image",
|
||||
"then": "<b>item_with_image</b> (builtin) - All items with an image. All alone, not a layer which is relevant for any MapComplete theme, as it is a random collection of items. However, when put into the databank, this allows to quickly fetch (the URL of) pictures nearby a different object, to quickly link this"
|
||||
|
@ -13309,7 +13317,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=aerialway",
|
||||
"then": "aerialway - Various forms of transport for passengers and goods that use wires, including cable cars, gondolas, chair lifts, drag lifts, and zip lines. "
|
||||
"then": "aerialway - Various forms of transport for passengers and goods that use wires, including cable cars, gondolas, chair lifts, drag lifts, and zip lines."
|
||||
},
|
||||
{
|
||||
"if": "value=ambulancestation",
|
||||
|
@ -13317,7 +13325,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=animal_shelter",
|
||||
"then": "animal_shelter - An animal shelter is a facility where animals in trouble are brought and facility's staff (volunteers or not) feeds them and cares of them, rehabilitating and healing them if necessary. This definition includes kennels for abandoned dogs, catteries for abandoned cats, shelters for other abandoned pets and wildlife recovery centres. "
|
||||
"then": "animal_shelter - An animal shelter is a facility where animals in trouble are brought and facility's staff (volunteers or not) feeds them and cares of them, rehabilitating and healing them if necessary. This definition includes kennels for abandoned dogs, catteries for abandoned cats, shelters for other abandoned pets and wildlife recovery centres."
|
||||
},
|
||||
{
|
||||
"if": "value=artwork",
|
||||
|
@ -13329,7 +13337,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=assisted_repair",
|
||||
"then": "assisted_repair - A self-assisted workshop is a location where people can come and repair their goods with help of volunteers and with the tools available at the given location. A repair café is a type of event organized regularly along the same principles."
|
||||
"then": "assisted_repair - A self-assisted workshop is a location where people can come and repair their goods with help of volunteers and with the tools available at the given location. A repair café is a type of event organized regularly along the same principles."
|
||||
},
|
||||
{
|
||||
"if": "value=atm",
|
||||
|
@ -13347,6 +13355,10 @@
|
|||
"if": "value=bbq",
|
||||
"then": "bbq - A permanently installed barbecue, typically accessible to the public."
|
||||
},
|
||||
{
|
||||
"if": "value=beehive",
|
||||
"then": "beehive - Layer showing beehives"
|
||||
},
|
||||
{
|
||||
"if": "value=bench",
|
||||
"then": "bench - A bench is a wooden, metal, stone, … surface where a human can sit. This layers visualises them and asks a few questions about them."
|
||||
|
@ -13401,7 +13413,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=brothel",
|
||||
"then": "brothel - An establishment specifically dedicated to prostitution. "
|
||||
"then": "brothel - An establishment specifically dedicated to prostitution."
|
||||
},
|
||||
{
|
||||
"if": "value=cafe_pub",
|
||||
|
@ -13421,7 +13433,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=cinema",
|
||||
"then": "cinema - A place showing movies (films), generally open to the public for a fee. Commonly referred to as a movie theater in the US"
|
||||
"then": "cinema - A place showing movies (films), generally open to the public for a fee. Commonly referred to as a movie theater in the US."
|
||||
},
|
||||
{
|
||||
"if": "value=climbing",
|
||||
|
@ -13639,6 +13651,10 @@
|
|||
"if": "value=information_board",
|
||||
"then": "information_board - A layer showing touristical, road side information boards (e.g. giving information about the landscape, a building, a feature, a map, …)"
|
||||
},
|
||||
{
|
||||
"if": "value=insect_hotel",
|
||||
"then": "insect_hotel - Layer showing insect hotels"
|
||||
},
|
||||
{
|
||||
"if": "value=item_with_image",
|
||||
"then": "item_with_image - All items with an image. All alone, not a layer which is relevant for any MapComplete theme, as it is a random collection of items. However, when put into the databank, this allows to quickly fetch (the URL of) pictures nearby a different object, to quickly link this"
|
||||
|
@ -35053,7 +35069,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=aerialway",
|
||||
"then": "aerialway - Various forms of transport for passengers and goods that use wires, including cable cars, gondolas, chair lifts, drag lifts, and zip lines. "
|
||||
"then": "aerialway - Various forms of transport for passengers and goods that use wires, including cable cars, gondolas, chair lifts, drag lifts, and zip lines."
|
||||
},
|
||||
{
|
||||
"if": "value=ambulancestation",
|
||||
|
@ -35061,7 +35077,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=animal_shelter",
|
||||
"then": "animal_shelter - An animal shelter is a facility where animals in trouble are brought and facility's staff (volunteers or not) feeds them and cares of them, rehabilitating and healing them if necessary. This definition includes kennels for abandoned dogs, catteries for abandoned cats, shelters for other abandoned pets and wildlife recovery centres. "
|
||||
"then": "animal_shelter - An animal shelter is a facility where animals in trouble are brought and facility's staff (volunteers or not) feeds them and cares of them, rehabilitating and healing them if necessary. This definition includes kennels for abandoned dogs, catteries for abandoned cats, shelters for other abandoned pets and wildlife recovery centres."
|
||||
},
|
||||
{
|
||||
"if": "value=artwork",
|
||||
|
@ -35073,7 +35089,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=assisted_repair",
|
||||
"then": "assisted_repair - A self-assisted workshop is a location where people can come and repair their goods with help of volunteers and with the tools available at the given location. A repair café is a type of event organized regularly along the same principles."
|
||||
"then": "assisted_repair - A self-assisted workshop is a location where people can come and repair their goods with help of volunteers and with the tools available at the given location. A repair café is a type of event organized regularly along the same principles."
|
||||
},
|
||||
{
|
||||
"if": "value=atm",
|
||||
|
@ -35091,6 +35107,10 @@
|
|||
"if": "value=bbq",
|
||||
"then": "bbq - A permanently installed barbecue, typically accessible to the public."
|
||||
},
|
||||
{
|
||||
"if": "value=beehive",
|
||||
"then": "beehive - Layer showing beehives"
|
||||
},
|
||||
{
|
||||
"if": "value=bench",
|
||||
"then": "bench - A bench is a wooden, metal, stone, … surface where a human can sit. This layers visualises them and asks a few questions about them."
|
||||
|
@ -35145,7 +35165,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=brothel",
|
||||
"then": "brothel - An establishment specifically dedicated to prostitution. "
|
||||
"then": "brothel - An establishment specifically dedicated to prostitution."
|
||||
},
|
||||
{
|
||||
"if": "value=cafe_pub",
|
||||
|
@ -35165,7 +35185,7 @@
|
|||
},
|
||||
{
|
||||
"if": "value=cinema",
|
||||
"then": "cinema - A place showing movies (films), generally open to the public for a fee. Commonly referred to as a movie theater in the US"
|
||||
"then": "cinema - A place showing movies (films), generally open to the public for a fee. Commonly referred to as a movie theater in the US."
|
||||
},
|
||||
{
|
||||
"if": "value=climbing",
|
||||
|
@ -35383,6 +35403,10 @@
|
|||
"if": "value=information_board",
|
||||
"then": "information_board - A layer showing touristical, road side information boards (e.g. giving information about the landscape, a building, a feature, a map, …)"
|
||||
},
|
||||
{
|
||||
"if": "value=insect_hotel",
|
||||
"then": "insect_hotel - Layer showing insect hotels"
|
||||
},
|
||||
{
|
||||
"if": "value=item_with_image",
|
||||
"then": "item_with_image - All items with an image. All alone, not a layer which is relevant for any MapComplete theme, as it is a random collection of items. However, when put into the databank, this allows to quickly fetch (the URL of) pictures nearby a different object, to quickly link this"
|
||||
|
|
|
@ -13,11 +13,11 @@
|
|||
"contributor": "paunofu"
|
||||
},
|
||||
{
|
||||
"commits": 127,
|
||||
"commits": 154,
|
||||
"contributor": "Anonymous"
|
||||
},
|
||||
{
|
||||
"commits": 106,
|
||||
"commits": 107,
|
||||
"contributor": "mcliquid"
|
||||
},
|
||||
{
|
||||
|
@ -25,11 +25,11 @@
|
|||
"contributor": "Allan Nordhøy"
|
||||
},
|
||||
{
|
||||
"commits": 83,
|
||||
"commits": 89,
|
||||
"contributor": "Robin van der Linde"
|
||||
},
|
||||
{
|
||||
"commits": 70,
|
||||
"commits": 76,
|
||||
"contributor": "mike140"
|
||||
},
|
||||
{
|
||||
|
@ -37,24 +37,28 @@
|
|||
"contributor": "danieldegroot2"
|
||||
},
|
||||
{
|
||||
"commits": 53,
|
||||
"contributor": "Harry Bond"
|
||||
"commits": 54,
|
||||
"contributor": "Jiří Podhorecký"
|
||||
},
|
||||
{
|
||||
"commits": 52,
|
||||
"contributor": "Jiří Podhorecký"
|
||||
"commits": 53,
|
||||
"contributor": "Harry Bond"
|
||||
},
|
||||
{
|
||||
"commits": 51,
|
||||
"contributor": "gallegonovato"
|
||||
},
|
||||
{
|
||||
"commits": 44,
|
||||
"contributor": "Babos Gábor"
|
||||
"commits": 47,
|
||||
"contributor": "Supaplex"
|
||||
},
|
||||
{
|
||||
"commits": 44,
|
||||
"contributor": "Supaplex"
|
||||
"commits": 46,
|
||||
"contributor": "Midgard"
|
||||
},
|
||||
{
|
||||
"commits": 45,
|
||||
"contributor": "Babos Gábor"
|
||||
},
|
||||
{
|
||||
"commits": 38,
|
||||
|
@ -116,6 +120,10 @@
|
|||
"commits": 14,
|
||||
"contributor": "J. Lavoie"
|
||||
},
|
||||
{
|
||||
"commits": 13,
|
||||
"contributor": "small"
|
||||
},
|
||||
{
|
||||
"commits": 13,
|
||||
"contributor": "Olivier"
|
||||
|
@ -148,10 +156,6 @@
|
|||
"commits": 11,
|
||||
"contributor": "Túllio Franca"
|
||||
},
|
||||
{
|
||||
"commits": 10,
|
||||
"contributor": "small"
|
||||
},
|
||||
{
|
||||
"commits": 10,
|
||||
"contributor": "Jeff Huang"
|
||||
|
@ -208,6 +212,10 @@
|
|||
"commits": 9,
|
||||
"contributor": "Jacque Fresco"
|
||||
},
|
||||
{
|
||||
"commits": 8,
|
||||
"contributor": "Joost Schouppe"
|
||||
},
|
||||
{
|
||||
"commits": 8,
|
||||
"contributor": "nilocram"
|
||||
|
@ -216,6 +224,10 @@
|
|||
"commits": 8,
|
||||
"contributor": "Vinicius"
|
||||
},
|
||||
{
|
||||
"commits": 7,
|
||||
"contributor": "Weblate Admin"
|
||||
},
|
||||
{
|
||||
"commits": 7,
|
||||
"contributor": "Franco"
|
||||
|
@ -224,10 +236,6 @@
|
|||
"commits": 7,
|
||||
"contributor": "NetworkedPoncho"
|
||||
},
|
||||
{
|
||||
"commits": 7,
|
||||
"contributor": "Joost Schouppe"
|
||||
},
|
||||
{
|
||||
"commits": 7,
|
||||
"contributor": "Andrews Leruth"
|
||||
|
@ -276,6 +284,10 @@
|
|||
"commits": 6,
|
||||
"contributor": "lvgx"
|
||||
},
|
||||
{
|
||||
"commits": 5,
|
||||
"contributor": "Eric Armijo"
|
||||
},
|
||||
{
|
||||
"commits": 5,
|
||||
"contributor": "foxandpotatoes"
|
||||
|
@ -328,10 +340,6 @@
|
|||
"commits": 5,
|
||||
"contributor": "Alexey Shabanov"
|
||||
},
|
||||
{
|
||||
"commits": 4,
|
||||
"contributor": "Weblate Admin"
|
||||
},
|
||||
{
|
||||
"commits": 4,
|
||||
"contributor": "André Marcelo Alvarenga"
|
||||
|
@ -358,7 +366,11 @@
|
|||
},
|
||||
{
|
||||
"commits": 3,
|
||||
"contributor": "Eric Armijo"
|
||||
"contributor": "Gábor"
|
||||
},
|
||||
{
|
||||
"commits": 3,
|
||||
"contributor": "Michal Čermák"
|
||||
},
|
||||
{
|
||||
"commits": 3,
|
||||
|
@ -440,6 +452,10 @@
|
|||
"commits": 3,
|
||||
"contributor": "SiegbjornSitumeang"
|
||||
},
|
||||
{
|
||||
"commits": 2,
|
||||
"contributor": "Héctor Ochoa Ortiz"
|
||||
},
|
||||
{
|
||||
"commits": 2,
|
||||
"contributor": "SmallSoap"
|
||||
|
@ -524,10 +540,6 @@
|
|||
"commits": 2,
|
||||
"contributor": "Marc Marc"
|
||||
},
|
||||
{
|
||||
"commits": 2,
|
||||
"contributor": "Midgard"
|
||||
},
|
||||
{
|
||||
"commits": 2,
|
||||
"contributor": "M. Rey"
|
||||
|
@ -572,14 +584,6 @@
|
|||
"commits": 2,
|
||||
"contributor": "Leo Alcaraz"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "Héctor Ochoa Ortiz"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "Gábor"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "Roger"
|
||||
|
@ -636,10 +640,6 @@
|
|||
"commits": 1,
|
||||
"contributor": "Julio Salas"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "Michal Čermák"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "Juan"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue