Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-12-31 16:31:55 +01:00
commit 00c233a2eb
188 changed files with 4982 additions and 1745 deletions

View file

@ -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) {

View file

@ -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 &&

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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(

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -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}`)

View file

@ -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

View file

@ -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 }
}

View file

@ -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 }
}
}

View file

@ -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]
}
/**

View file

@ -1,14 +1,42 @@
import { Utils } from "../../Utils"
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
export class ThemeMetaTagging {
public static readonly themeName = "usersettings"
public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? '' )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
feat.properties['__current_backgroun'] = 'initial_value'
}
}
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
feat.properties._description
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
?.at(1)
)
Utils.AddLazyProperty(
feat.properties,
"_d",
() => feat.properties._description?.replace(/&lt;/g, "<")?.replace(/&gt;/g, ">") ?? ""
)
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.href.match(/mastodon|en.osm.town/) !== null
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(
feat.properties,
"_mastodon_candidate",
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
)
feat.properties["__current_backgroun"] = "initial_value"
}
}

View file

@ -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)

View file

@ -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))
}

View file

@ -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

View file

@ -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

View file

@ -232,7 +232,7 @@ export default class FilteredLayer {
}
}
{
if(!this.isDisplayed.data){
if (!this.isDisplayed.data) {
return false
}
}

View file

@ -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) {

View file

@ -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,

View file

@ -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) {

View file

@ -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"}

View file

@ -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}

View file

@ -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 />

View file

@ -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)}

View file

@ -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}

View file

@ -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}

View file

@ -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}

View file

@ -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}

View file

@ -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
}
}

View file

@ -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>

View file

@ -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={() => {

View file

@ -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">

View file

@ -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)

View file

@ -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

View file

@ -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">

View file

@ -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} />

View file

@ -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>

View file

@ -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) {

View file

@ -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(

View file

@ -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>

View file

@ -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({})

View file

@ -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)

View file

@ -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()
}

View file

@ -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"

View file

@ -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()
}

View file

@ -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}

View file

@ -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}>

View file

@ -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"
/**

View file

@ -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 })
},
},

View file

@ -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")

View file

@ -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()

View file

@ -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 -->

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"