forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
00c233a2eb
188 changed files with 4982 additions and 1745 deletions
|
|
@ -8,10 +8,13 @@ import { FeatureSource, WritableFeatureSource } from "../FeatureSource/FeatureSo
|
|||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import StaticFeatureSource, {
|
||||
WritableStaticFeatureSource,
|
||||
} from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import { MapProperties } from "../../Models/MapProperties"
|
||||
import { Orientation } from "../../Sensors/Orientation"
|
||||
|
||||
;("use strict")
|
||||
/**
|
||||
* The geolocation-handler takes a map-location and a geolocation state.
|
||||
* It'll move the map as appropriate given the state of the geolocation-API
|
||||
|
|
@ -43,13 +46,13 @@ export default class GeoLocationHandler {
|
|||
public readonly mapHasMoved: UIEventSource<Date | undefined> = new UIEventSource<
|
||||
Date | undefined
|
||||
>(undefined)
|
||||
private readonly selectedElement: UIEventSource<any>
|
||||
private readonly selectedElement: UIEventSource<Feature>
|
||||
private readonly mapProperties?: MapProperties
|
||||
private readonly gpsLocationHistoryRetentionTime?: UIEventSource<number>
|
||||
|
||||
constructor(
|
||||
geolocationState: GeoLocationState,
|
||||
selectedElement: UIEventSource<any>,
|
||||
selectedElement: UIEventSource<Feature>,
|
||||
mapProperties?: MapProperties,
|
||||
gpsLocationHistoryRetentionTime?: UIEventSource<number>
|
||||
) {
|
||||
|
|
@ -59,13 +62,12 @@ export default class GeoLocationHandler {
|
|||
this.mapProperties = mapProperties
|
||||
this.gpsLocationHistoryRetentionTime = gpsLocationHistoryRetentionTime
|
||||
// Did an interaction move the map?
|
||||
let self = this
|
||||
let initTime = new Date()
|
||||
mapLocation.addCallbackD((_) => {
|
||||
const initTime = new Date()
|
||||
mapLocation.addCallbackD(() => {
|
||||
if (new Date().getTime() - initTime.getTime() < 250) {
|
||||
return
|
||||
}
|
||||
self.mapHasMoved.setData(new Date())
|
||||
this.mapHasMoved.setData(new Date())
|
||||
return true // Unsubscribe
|
||||
})
|
||||
|
||||
|
|
@ -76,12 +78,12 @@ export default class GeoLocationHandler {
|
|||
this.mapHasMoved.setData(new Date())
|
||||
}
|
||||
|
||||
this.geolocationState.currentGPSLocation.addCallbackAndRunD((_) => {
|
||||
this.geolocationState.currentGPSLocation.addCallbackAndRunD(() => {
|
||||
const timeSinceLastRequest =
|
||||
(new Date().getTime() - geolocationState.requestMoment.data?.getTime() ?? 0) / 1000
|
||||
if (!this.mapHasMoved.data) {
|
||||
// The map hasn't moved yet; we received our first coordinates, so let's move there!
|
||||
self.MoveMapToCurrentLocation()
|
||||
this.MoveMapToCurrentLocation()
|
||||
}
|
||||
if (
|
||||
timeSinceLastRequest < Constants.zoomToLocationTimeout &&
|
||||
|
|
@ -90,12 +92,12 @@ export default class GeoLocationHandler {
|
|||
geolocationState.requestMoment.data?.getTime())
|
||||
) {
|
||||
// still within request time and the map hasn't moved since requesting to jump to the current location
|
||||
self.MoveMapToCurrentLocation()
|
||||
this.MoveMapToCurrentLocation()
|
||||
}
|
||||
|
||||
if (!this.geolocationState.allowMoving.data) {
|
||||
// Jup, the map is locked to the bound location: move automatically
|
||||
self.MoveMapToCurrentLocation(0)
|
||||
this.MoveMapToCurrentLocation(0)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
|
@ -183,7 +185,7 @@ export default class GeoLocationHandler {
|
|||
}
|
||||
|
||||
private initUserLocationTrail() {
|
||||
const features = LocalStorageSource.getParsed<Feature[]>("gps_location_history", [])
|
||||
const features = LocalStorageSource.getParsed<Feature<Point>[]>("gps_location_history", [])
|
||||
const now = new Date().getTime()
|
||||
features.data = features.data.filter((ff) => {
|
||||
if (ff.properties === undefined) {
|
||||
|
|
@ -230,7 +232,7 @@ export default class GeoLocationHandler {
|
|||
features.ping()
|
||||
})
|
||||
|
||||
this.historicalUserLocations = <any>new StaticFeatureSource(features)
|
||||
this.historicalUserLocations = new WritableStaticFeatureSource<Feature<Point>>(features)
|
||||
|
||||
const asLine = features.map((allPoints) => {
|
||||
if (allPoints === undefined || allPoints.length < 2) {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import { LocalStorageSource } from "../Web/LocalStorageSource"
|
|||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import Hash from "../Web/Hash"
|
||||
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
|
||||
import { OsmObject } from "../Osm/OsmObject"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { Utils } from "../../Utils"
|
||||
import { GeoLocationState } from "../State/GeoLocationState"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
|
||||
;("use strict")
|
||||
|
||||
/**
|
||||
* This actor is responsible to set the map location.
|
||||
|
|
@ -25,7 +27,11 @@ export default class InitialMapPositioning {
|
|||
public location: UIEventSource<{ lon: number; lat: number }>
|
||||
public useTerrain: Store<boolean>
|
||||
|
||||
constructor(layoutToUse: ThemeConfig, geolocationState: GeoLocationState) {
|
||||
constructor(
|
||||
layoutToUse: ThemeConfig,
|
||||
geolocationState: GeoLocationState,
|
||||
osmConnection: OsmConnection
|
||||
) {
|
||||
function localStorageSynced(
|
||||
key: string,
|
||||
deflt: number,
|
||||
|
|
@ -47,8 +53,6 @@ export default class InitialMapPositioning {
|
|||
return src
|
||||
}
|
||||
|
||||
const initialHash = Hash.hash.data
|
||||
|
||||
// -- Location control initialization
|
||||
this.zoom = localStorageSynced(
|
||||
"z",
|
||||
|
|
@ -72,6 +76,7 @@ export default class InitialMapPositioning {
|
|||
})
|
||||
this.useTerrain = new ImmutableStore<boolean>(layoutToUse.enableTerrain)
|
||||
|
||||
const initialHash = Hash.hash.data
|
||||
if (initialHash?.match(/^(node|way|relation)\/[0-9]+$/)) {
|
||||
// We pan to the selected element
|
||||
const [type, id] = initialHash.split("/")
|
||||
|
|
@ -88,6 +93,16 @@ export default class InitialMapPositioning {
|
|||
const [lat, lon] = osmObject.centerpoint()
|
||||
this.location.setData({ lon, lat })
|
||||
})
|
||||
} else if (layoutToUse.id === "notes" && initialHash?.match(/[0-9]+/)) {
|
||||
console.log("Loading note", initialHash)
|
||||
const noteId = Number(initialHash)
|
||||
if (osmConnection.isLoggedIn.data) {
|
||||
osmConnection.getNote(noteId).then((note) => {
|
||||
const [lon, lat] = note.geometry.coordinates
|
||||
console.log("Got note:", note)
|
||||
this.location.set({ lon, lat })
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
Constants.GeoIpServer &&
|
||||
lat.data === defaultLat &&
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Feature } from "geojson"
|
||||
import { Feature, Geometry } from "geojson"
|
||||
import { UpdatableFeatureSource } from "../FeatureSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
|
|
@ -7,6 +7,9 @@ import { Overpass } from "../../Osm/Overpass"
|
|||
import { Utils } from "../../../Utils"
|
||||
import { TagsFilter } from "../../Tags/TagsFilter"
|
||||
import { BBox } from "../../BBox"
|
||||
import { FeatureCollection } from "@turf/turf"
|
||||
import { OsmTags } from "../../../Models/OsmFeature"
|
||||
;("use strict")
|
||||
|
||||
/**
|
||||
* A wrapper around the 'Overpass'-object.
|
||||
|
|
@ -56,17 +59,13 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
|
|||
this.state = state
|
||||
this._isActive = options?.isActive ?? new ImmutableStore(true)
|
||||
this.padToZoomLevel = options?.padToTiles
|
||||
const self = this
|
||||
this._layersToDownload = options?.ignoreZoom
|
||||
? new ImmutableStore(state.layers)
|
||||
: state.zoom.map((zoom) => this.layersToDownload(zoom))
|
||||
|
||||
state.bounds.mapD(
|
||||
(_) => {
|
||||
self.updateAsyncIfNeeded()
|
||||
},
|
||||
[this._layersToDownload]
|
||||
)
|
||||
state.bounds.mapD(() => {
|
||||
this.updateAsyncIfNeeded()
|
||||
}, [this._layersToDownload])
|
||||
}
|
||||
|
||||
private layersToDownload(zoom: number): LayerConfig[] {
|
||||
|
|
@ -104,10 +103,11 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
|
|||
|
||||
/**
|
||||
* Download the relevant data from overpass. Attempt to use a different server if one fails; only downloads the relevant layers
|
||||
* Will always attempt to download, even is 'options.isActive.data' is 'false', the zoom level is incorrect, ...
|
||||
* @private
|
||||
*/
|
||||
public async updateAsync(overrideBounds?: BBox): Promise<void> {
|
||||
let data: any = undefined
|
||||
let data: FeatureCollection<Geometry, OsmTags> = undefined
|
||||
let lastUsed = 0
|
||||
const start = new Date()
|
||||
const layersToDownload = this._layersToDownload.data
|
||||
|
|
@ -116,8 +116,7 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
|
|||
return
|
||||
}
|
||||
|
||||
const self = this
|
||||
const overpassUrls = self.state.overpassUrl.data
|
||||
const overpassUrls = this.state.overpassUrl.data
|
||||
if (overpassUrls === undefined || overpassUrls.length === 0) {
|
||||
throw "Panic: overpassFeatureSource didn't receive any overpassUrls"
|
||||
}
|
||||
|
|
@ -140,10 +139,11 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
|
|||
return undefined
|
||||
}
|
||||
this.runningQuery.setData(true)
|
||||
console.trace("Overpass feature source: querying geojson")
|
||||
data = (await overpass.queryGeoJson(bounds))[0]
|
||||
} catch (e) {
|
||||
self.retries.data++
|
||||
self.retries.ping()
|
||||
this.retries.data++
|
||||
this.retries.ping()
|
||||
console.error(`QUERY FAILED due to`, e)
|
||||
|
||||
await Utils.waitFor(1000)
|
||||
|
|
@ -153,12 +153,12 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
|
|||
console.log("Trying next time with", overpassUrls[lastUsed])
|
||||
} else {
|
||||
lastUsed = 0
|
||||
self.timeout.setData(self.retries.data * 5)
|
||||
this.timeout.setData(this.retries.data * 5)
|
||||
|
||||
while (self.timeout.data > 0) {
|
||||
while (this.timeout.data > 0) {
|
||||
await Utils.waitFor(1000)
|
||||
self.timeout.data--
|
||||
self.timeout.ping()
|
||||
this.timeout.data--
|
||||
this.timeout.ping()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -180,14 +180,14 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
|
|||
timeNeeded,
|
||||
"seconds"
|
||||
)
|
||||
self.features.setData(data.features)
|
||||
this.features.setData(data.features)
|
||||
this._lastQueryBBox = bounds
|
||||
this._lastRequestedLayers = layersToDownload
|
||||
} catch (e) {
|
||||
console.error("Got the overpass response, but could not process it: ", e, e.stack)
|
||||
} finally {
|
||||
self.retries.setData(0)
|
||||
self.runningQuery.setData(false)
|
||||
this.retries.setData(0)
|
||||
this.runningQuery.setData(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { FeatureSource } from "../FeatureSource"
|
||||
import { ImmutableStore, Store } from "../../UIEventSource"
|
||||
import { FeatureSource, WritableFeatureSource } from "../FeatureSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
|
||||
import { Feature } from "geojson"
|
||||
|
||||
;("use strict")
|
||||
/**
|
||||
* A simple, read only feature store.
|
||||
*/
|
||||
|
|
@ -30,3 +31,29 @@ export default class StaticFeatureSource<T extends Feature = Feature> implements
|
|||
return new StaticFeatureSource(geojson)
|
||||
}
|
||||
}
|
||||
|
||||
export class WritableStaticFeatureSource<T extends Feature = Feature>
|
||||
implements WritableFeatureSource<T>
|
||||
{
|
||||
public readonly features: UIEventSource<T[]> = undefined
|
||||
|
||||
constructor(features: UIEventSource<T[]> | T[] | { features: T[] } | { features: Store<T[]> }) {
|
||||
if (features === undefined) {
|
||||
throw "Static feature source received undefined as source"
|
||||
}
|
||||
|
||||
let feats: T[] | UIEventSource<T[]>
|
||||
|
||||
if (features["features"]) {
|
||||
feats = features["features"]
|
||||
} else {
|
||||
feats = <T[] | UIEventSource<T[]>>features
|
||||
}
|
||||
|
||||
if (Array.isArray(feats)) {
|
||||
this.features = new UIEventSource<T[]>(feats)
|
||||
} else {
|
||||
this.features = feats
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ export default class ThemeSource extends FeatureSourceMerger {
|
|||
*/
|
||||
public readonly isLoading: Store<boolean>
|
||||
|
||||
private readonly supportsForceDownload: UpdatableFeatureSource[]
|
||||
|
||||
public static readonly fromCacheZoomLevel = 15
|
||||
|
||||
/**
|
||||
|
|
@ -44,8 +42,6 @@ export default class ThemeSource extends FeatureSourceMerger {
|
|||
mvtAvailableLayers: Set<string>,
|
||||
fullNodeDatabaseSource?: FullNodeDatabaseSource
|
||||
) {
|
||||
const supportsForceDownload: UpdatableFeatureSource[] = []
|
||||
|
||||
const { bounds, zoom } = mapProperties
|
||||
// remove all 'special' layers
|
||||
layers = layers.filter((layer) => layer.source !== null && layer.source !== undefined)
|
||||
|
|
@ -95,7 +91,6 @@ export default class ThemeSource extends FeatureSourceMerger {
|
|||
)
|
||||
overpassSource = ThemeSource.setupOverpass(osmLayers, bounds, zoom, featureSwitches)
|
||||
nonMvtSources.push(overpassSource)
|
||||
supportsForceDownload.push(overpassSource)
|
||||
}
|
||||
|
||||
function setIsLoading() {
|
||||
|
|
@ -110,7 +105,6 @@ export default class ThemeSource extends FeatureSourceMerger {
|
|||
ThemeSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
|
||||
)
|
||||
|
||||
const downloadAllBounds: UIEventSource<BBox> = new UIEventSource<BBox>(undefined)
|
||||
const downloadAll = new OverpassFeatureSource(
|
||||
{
|
||||
layers: layers.filter((l) => l.isNormal()),
|
||||
|
|
@ -123,6 +117,7 @@ export default class ThemeSource extends FeatureSourceMerger {
|
|||
},
|
||||
{
|
||||
ignoreZoom: true,
|
||||
isActive: new ImmutableStore(false),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -135,13 +130,8 @@ export default class ThemeSource extends FeatureSourceMerger {
|
|||
)
|
||||
|
||||
this.isLoading = isLoading
|
||||
supportsForceDownload.push(...geojsonSources)
|
||||
supportsForceDownload.push(...mvtSources) // Non-mvt sources are handled by overpass
|
||||
|
||||
this._mapBounds = mapProperties.bounds
|
||||
this._downloadAll = downloadAll
|
||||
|
||||
this.supportsForceDownload = supportsForceDownload
|
||||
this._mapBounds = mapProperties.bounds
|
||||
}
|
||||
|
||||
private static setupMvtSource(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { BBox } from "./BBox"
|
||||
import * as turf from "@turf/turf"
|
||||
import { AllGeoJSON, booleanWithin, Coord, Lines } from "@turf/turf"
|
||||
import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf"
|
||||
import {
|
||||
Feature,
|
||||
FeatureCollection,
|
||||
GeoJSON,
|
||||
Geometry,
|
||||
LineString,
|
||||
MultiLineString,
|
||||
MultiPolygon,
|
||||
|
|
@ -15,6 +14,9 @@ import {
|
|||
} from "geojson"
|
||||
import { Tiles } from "../Models/TileRange"
|
||||
import { Utils } from "../Utils"
|
||||
import { NearestPointOnLine } from "@turf/nearest-point-on-line"
|
||||
|
||||
;("use strict")
|
||||
|
||||
export class GeoOperations {
|
||||
private static readonly _earthRadius = 6378137
|
||||
|
|
@ -52,15 +54,21 @@ export class GeoOperations {
|
|||
/**
|
||||
* Create a union between two features
|
||||
*/
|
||||
public static union(f0: Feature, f1: Feature): Feature<Polygon | MultiPolygon> | null {
|
||||
return turf.union(<any>f0, <any>f1)
|
||||
public static union(
|
||||
f0: Feature<Polygon | MultiPolygon>,
|
||||
f1: Feature<Polygon | MultiPolygon>
|
||||
): Feature<Polygon | MultiPolygon> | null {
|
||||
return turf.union(f0, f1)
|
||||
}
|
||||
|
||||
public static intersect(f0: Feature, f1: Feature): Feature<Polygon | MultiPolygon> | null {
|
||||
return turf.intersect(<any>f0, <any>f1)
|
||||
public static intersect(
|
||||
f0: Feature<Polygon | MultiPolygon>,
|
||||
f1: Feature<Polygon | MultiPolygon>
|
||||
): Feature<Polygon | MultiPolygon> | null {
|
||||
return turf.intersect(f0, f1)
|
||||
}
|
||||
|
||||
static surfaceAreaInSqMeters(feature: any) {
|
||||
static surfaceAreaInSqMeters(feature: Feature<Polygon | MultiPolygon>): number {
|
||||
return turf.area(feature)
|
||||
}
|
||||
|
||||
|
|
@ -68,19 +76,31 @@ export class GeoOperations {
|
|||
* Converts a GeoJson feature to a point GeoJson feature
|
||||
* @param feature
|
||||
*/
|
||||
static centerpoint(feature: any): Feature<Point> {
|
||||
const newFeature: Feature<Point> = turf.center(feature)
|
||||
static centerpoint(feature: Feature): Feature<Point> {
|
||||
const newFeature: Feature<Point> = turf.center(<turf.Feature>feature)
|
||||
newFeature.properties = feature.properties
|
||||
newFeature.id = feature.id
|
||||
return newFeature
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [lon,lat] coordinates
|
||||
* Returns [lon,lat] coordinates.
|
||||
* @param feature
|
||||
*
|
||||
* GeoOperations.centerpointCoordinates(undefined) // => undefined
|
||||
*/
|
||||
static centerpointCoordinates(feature: undefined | null): undefined
|
||||
static centerpointCoordinates(
|
||||
feature: AllGeoJSON | GeoJSON | undefined
|
||||
): [number, number] | undefined
|
||||
static centerpointCoordinates(
|
||||
feature: NonNullable<AllGeoJSON> | NonNullable<GeoJSON>
|
||||
): NonNullable<[number, number]>
|
||||
static centerpointCoordinates(feature: AllGeoJSON | GeoJSON): [number, number] {
|
||||
return <[number, number]>turf.center(<any>feature).geometry.coordinates
|
||||
if (feature === undefined || feature === null) {
|
||||
return undefined
|
||||
}
|
||||
return <[number, number]>turf.center(<turf.Feature>feature).geometry.coordinates
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -132,11 +152,14 @@ export class GeoOperations {
|
|||
* const overlap0 = GeoOperations.calculateOverlap(line0, [polygon]);
|
||||
* overlap.length // => 1
|
||||
*/
|
||||
static calculateOverlap(feature: any, otherFeatures: any[]): { feat: any; overlap: number }[] {
|
||||
static calculateOverlap(
|
||||
feature: Feature,
|
||||
otherFeatures: Feature[]
|
||||
): { feat: Feature; overlap: number }[] {
|
||||
const featureBBox = BBox.get(feature)
|
||||
const result: { feat: any; overlap: number }[] = []
|
||||
const result: { feat: Feature; overlap: number }[] = []
|
||||
if (feature.geometry.type === "Point") {
|
||||
const coor = feature.geometry.coordinates
|
||||
const coor = <[number, number]>feature.geometry.coordinates
|
||||
for (const otherFeature of otherFeatures) {
|
||||
if (
|
||||
feature.properties.id !== undefined &&
|
||||
|
|
@ -189,7 +212,7 @@ export class GeoOperations {
|
|||
}
|
||||
|
||||
if (otherFeature.geometry.type === "Point") {
|
||||
if (this.inside(otherFeature, feature)) {
|
||||
if (this.inside(<Feature<Point>>otherFeature, feature)) {
|
||||
result.push({ feat: otherFeature, overlap: undefined })
|
||||
}
|
||||
continue
|
||||
|
|
@ -255,9 +278,10 @@ export class GeoOperations {
|
|||
const y: number = pointCoordinate[1]
|
||||
|
||||
if (feature.geometry.type === "MultiPolygon") {
|
||||
const coordinatess = feature.geometry.coordinates
|
||||
const coordinatess: [number, number][][][] = <[number, number][][][]>(
|
||||
feature.geometry.coordinates
|
||||
)
|
||||
for (const coordinates of coordinatess) {
|
||||
// @ts-ignore
|
||||
const inThisPolygon = GeoOperations.pointInPolygonCoordinates(x, y, coordinates)
|
||||
if (inThisPolygon) {
|
||||
return true
|
||||
|
|
@ -267,24 +291,30 @@ export class GeoOperations {
|
|||
}
|
||||
|
||||
if (feature.geometry.type === "Polygon") {
|
||||
// @ts-ignore
|
||||
return GeoOperations.pointInPolygonCoordinates(x, y, feature.geometry.coordinates)
|
||||
return GeoOperations.pointInPolygonCoordinates(
|
||||
x,
|
||||
y,
|
||||
<[number, number][][]>feature.geometry.coordinates
|
||||
)
|
||||
}
|
||||
|
||||
throw "GeoOperations.inside: unsupported geometry type " + feature.geometry.type
|
||||
}
|
||||
|
||||
static lengthInMeters(feature: any) {
|
||||
static lengthInMeters(feature: Feature): number {
|
||||
return turf.length(feature) * 1000
|
||||
}
|
||||
|
||||
static buffer(feature: any, bufferSizeInMeter: number) {
|
||||
static buffer(
|
||||
feature: Feature,
|
||||
bufferSizeInMeter: number
|
||||
): Feature<Polygon | MultiPolygon> | FeatureCollection<Polygon | MultiPolygon> {
|
||||
return turf.buffer(feature, bufferSizeInMeter / 1000, {
|
||||
units: "kilometers",
|
||||
})
|
||||
}
|
||||
|
||||
static bbox(feature: Feature | FeatureCollection): Feature<LineString, {}> {
|
||||
static bbox(feature: Feature | FeatureCollection): Feature<LineString> {
|
||||
const [lon, lat, lon0, lat0] = turf.bbox(feature)
|
||||
return {
|
||||
type: "Feature",
|
||||
|
|
@ -316,17 +346,8 @@ export class GeoOperations {
|
|||
public static nearestPoint(
|
||||
way: Feature<LineString>,
|
||||
point: [number, number]
|
||||
): Feature<
|
||||
Point,
|
||||
{
|
||||
index: number
|
||||
dist: number
|
||||
location: number
|
||||
}
|
||||
> {
|
||||
return <any>(
|
||||
turf.nearestPointOnLine(<Feature<LineString>>way, point, { units: "kilometers" })
|
||||
)
|
||||
): NearestPointOnLine {
|
||||
return turf.nearestPointOnLine(<Feature<LineString>>way, point, { units: "kilometers" })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -344,18 +365,32 @@ export class GeoOperations {
|
|||
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
|
||||
): Feature<LineString | MultiLineString> {
|
||||
if (way.geometry.type === "Polygon") {
|
||||
way = { ...way }
|
||||
way.geometry = { ...way.geometry }
|
||||
way.geometry.type = "LineString"
|
||||
way.geometry.coordinates = (<Polygon>way.geometry).coordinates[0]
|
||||
} else if (way.geometry.type === "MultiPolygon") {
|
||||
way = { ...way }
|
||||
way.geometry = { ...way.geometry }
|
||||
way.geometry.type = "MultiLineString"
|
||||
way.geometry.coordinates = (<MultiPolygon>way.geometry).coordinates[0]
|
||||
return <Feature<LineString>>{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: way.geometry.coordinates[0],
|
||||
},
|
||||
properties: way.properties,
|
||||
}
|
||||
}
|
||||
|
||||
return <any>way
|
||||
if (way.geometry.type === "MultiPolygon") {
|
||||
return <Feature<MultiLineString>>{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "MultiLineString",
|
||||
coordinates: way.geometry.coordinates[0],
|
||||
},
|
||||
properties: way.properties,
|
||||
}
|
||||
}
|
||||
if (way.geometry.type === "LineString") {
|
||||
return <Feature<LineString>>way
|
||||
}
|
||||
if (way.geometry.type === "MultiLineString") {
|
||||
return <Feature<MultiLineString>>way
|
||||
}
|
||||
throw "Invalid geometry to create a way from this"
|
||||
}
|
||||
|
||||
public static toCSV(
|
||||
|
|
@ -394,9 +429,6 @@ export class GeoOperations {
|
|||
for (const feature of _features) {
|
||||
const properties = feature.properties
|
||||
for (const key in properties) {
|
||||
if (!properties.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
addH(key)
|
||||
}
|
||||
}
|
||||
|
|
@ -602,7 +634,7 @@ export class GeoOperations {
|
|||
* const copy = GeoOperations.removeOvernoding(feature)
|
||||
* expect(copy.geometry.coordinates[0]).deep.equal([[4.477944199999975,51.02783550000022],[4.477987899999996,51.027818800000034],[4.478004500000021,51.02783399999988],[4.478025499999962,51.02782489999994],[4.478079099999993,51.027873899999896],[4.47801040000006,51.027903799999955],[4.477944199999975,51.02783550000022]])
|
||||
*/
|
||||
static removeOvernoding(feature: any) {
|
||||
static removeOvernoding(feature: Feature<LineString | Polygon>) {
|
||||
if (feature.geometry.type !== "LineString" && feature.geometry.type !== "Polygon") {
|
||||
throw "Overnode removal is only supported on linestrings and polygons"
|
||||
}
|
||||
|
|
@ -613,10 +645,10 @@ export class GeoOperations {
|
|||
}
|
||||
let coordinates: [number, number][]
|
||||
if (feature.geometry.type === "LineString") {
|
||||
coordinates = [...feature.geometry.coordinates]
|
||||
coordinates = <[number, number][]>[...feature.geometry.coordinates]
|
||||
copy.geometry.coordinates = coordinates
|
||||
} else {
|
||||
coordinates = [...feature.geometry.coordinates[0]]
|
||||
coordinates = <[number, number][]>[...feature.geometry.coordinates[0]]
|
||||
copy.geometry.coordinates[0] = coordinates
|
||||
}
|
||||
|
||||
|
|
@ -663,7 +695,7 @@ export class GeoOperations {
|
|||
|
||||
public static along(a: Coord, b: Coord, distanceMeter: number): Coord {
|
||||
return turf.along(
|
||||
<any>{
|
||||
<Feature<LineString>>{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
|
|
@ -705,8 +737,8 @@ export class GeoOperations {
|
|||
* GeoOperations.completelyWithin(park, pond) // => false
|
||||
*/
|
||||
static completelyWithin(
|
||||
feature: Feature<Geometry, any>,
|
||||
possiblyEnclosingFeature: Feature<Polygon | MultiPolygon, any>
|
||||
feature: Feature,
|
||||
possiblyEnclosingFeature: Feature<Polygon | MultiPolygon>
|
||||
): boolean {
|
||||
return booleanWithin(feature, possiblyEnclosingFeature)
|
||||
}
|
||||
|
|
@ -1169,7 +1201,7 @@ export class GeoOperations {
|
|||
|
||||
// Calculate the length of the intersection
|
||||
|
||||
let intersectionPoints = turf.lineIntersect(feature, otherFeature)
|
||||
const intersectionPoints = turf.lineIntersect(feature, otherFeature)
|
||||
if (intersectionPoints.features.length == 0) {
|
||||
// No intersections.
|
||||
// If one point is inside of the polygon, all points are
|
||||
|
|
@ -1182,7 +1214,7 @@ export class GeoOperations {
|
|||
|
||||
return null
|
||||
}
|
||||
let intersectionPointsArray = intersectionPoints.features.map((d) => {
|
||||
const intersectionPointsArray = intersectionPoints.features.map((d) => {
|
||||
return d.geometry.coordinates
|
||||
})
|
||||
|
||||
|
|
@ -1204,7 +1236,7 @@ export class GeoOperations {
|
|||
}
|
||||
}
|
||||
|
||||
let intersection = turf.lineSlice(
|
||||
const intersection = turf.lineSlice(
|
||||
turf.point(intersectionPointsArray[0]),
|
||||
turf.point(intersectionPointsArray[1]),
|
||||
feature
|
||||
|
|
|
|||
|
|
@ -107,12 +107,15 @@ export class ImageUploadManager {
|
|||
* @param file a jpg file to upload
|
||||
* @param tagsStore The tags of the feature
|
||||
* @param targetKey Use this key to save the attribute under. Default: 'image'
|
||||
* @param noblur if true, then the api call will indicate that the image is already blurred. The server won't apply blurring in this case
|
||||
* @param feature the feature this image is about. Will be used as fallback to get the GPS-coordinates
|
||||
*/
|
||||
public async uploadImageAndApply(
|
||||
file: File,
|
||||
tagsStore: UIEventSource<OsmTags>,
|
||||
targetKey: string,
|
||||
noblur: boolean
|
||||
noblur: boolean,
|
||||
feature: Feature
|
||||
): Promise<void> {
|
||||
const canBeUploaded = this.canBeUploaded(file)
|
||||
if (canBeUploaded !== true) {
|
||||
|
|
@ -130,7 +133,8 @@ export class ImageUploadManager {
|
|||
author,
|
||||
file,
|
||||
targetKey,
|
||||
noblur
|
||||
noblur,
|
||||
feature
|
||||
)
|
||||
if (!uploadResult) {
|
||||
return
|
||||
|
|
@ -157,7 +161,7 @@ export class ImageUploadManager {
|
|||
blob: File,
|
||||
targetKey: string | undefined,
|
||||
noblur: boolean,
|
||||
feature?: Feature,
|
||||
feature: Feature,
|
||||
ignoreGps: boolean = false
|
||||
): Promise<UploadResult> {
|
||||
this.increaseCountFor(this._uploadStarted, featureId)
|
||||
|
|
@ -168,9 +172,22 @@ export class ImageUploadManager {
|
|||
if (this._gps.data && !ignoreGps) {
|
||||
location = [this._gps.data.longitude, this._gps.data.latitude]
|
||||
}
|
||||
if (location === undefined || location?.some((l) => l === undefined)) {
|
||||
{
|
||||
feature ??= this._indexedFeatures.featuresById.data.get(featureId)
|
||||
location = GeoOperations.centerpointCoordinates(feature)
|
||||
if (feature === undefined) {
|
||||
throw "ImageUploadManager: no feature given and no feature found in the indexedFeature. Cannot upload this image"
|
||||
}
|
||||
const featureCenterpoint = GeoOperations.centerpointCoordinates(feature)
|
||||
if (
|
||||
location === undefined ||
|
||||
location?.some((l) => l === undefined) ||
|
||||
GeoOperations.distanceBetween(location, featureCenterpoint) > 150
|
||||
) {
|
||||
/* GPS location is either unknown or very far away from the photographed location.
|
||||
* Default to the centerpoint
|
||||
*/
|
||||
location = featureCenterpoint
|
||||
}
|
||||
}
|
||||
try {
|
||||
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
}
|
||||
|
||||
public async getInfo(hash: string): Promise<ProvidedImage> {
|
||||
return await this.getInfoFor(hash).then((r) => this.featureToImage(<any>r))
|
||||
return await this.getInfoFor(hash).then((r) => this.featureToImage(<any>r))
|
||||
}
|
||||
|
||||
getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
|
||||
|
|
@ -234,8 +234,19 @@ export class PanoramaxUploader implements ImageUploader {
|
|||
) {
|
||||
lat = exifLat
|
||||
lon = exifLon
|
||||
if (tags?.GPSLatitudeRef?.value?.[0] === "S") {
|
||||
lat *= -1
|
||||
}
|
||||
if (tags?.GPSLongitudeRef?.value?.[0] === "W") {
|
||||
lon *= -1
|
||||
}
|
||||
}
|
||||
const [date, time] =( tags.DateTime.value[0] ?? tags.DateTimeOriginal.value[0] ?? tags.GPSDateStamp ?? tags["Date Created"]).split(" ")
|
||||
const [date, time] = (
|
||||
tags.DateTime.value[0] ??
|
||||
tags.DateTimeOriginal.value[0] ??
|
||||
tags.GPSDateStamp ??
|
||||
tags["Date Created"]
|
||||
).split(" ")
|
||||
const exifDatetime = new Date(date.replaceAll(":", "-") + "T" + time)
|
||||
if (exifDatetime.getFullYear() === 1970) {
|
||||
// The data probably got reset to the epoch
|
||||
|
|
|
|||
|
|
@ -1,9 +1,24 @@
|
|||
import Constants from "../Models/Constants"
|
||||
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
|
||||
|
||||
export interface MaprouletteTask {
|
||||
name: string
|
||||
description: string
|
||||
instruction: string
|
||||
}
|
||||
export const maprouletteStatus = [
|
||||
"Open",
|
||||
"Fixed",
|
||||
"False_positive",
|
||||
"Skipped",
|
||||
"Deleted",
|
||||
"Already fixed",
|
||||
"Too_Hard",
|
||||
"Disabled",
|
||||
] as const
|
||||
|
||||
export type MaprouletteStatus = (typeof maprouletteStatus)[number]
|
||||
|
||||
export default class Maproulette {
|
||||
public static readonly defaultEndpoint = "https://maproulette.org/api/v2"
|
||||
|
||||
|
|
@ -16,16 +31,6 @@ export default class Maproulette {
|
|||
public static readonly STATUS_TOO_HARD = 6
|
||||
public static readonly STATUS_DISABLED = 9
|
||||
|
||||
public static readonly STATUS_MEANING = {
|
||||
0: "Open",
|
||||
1: "Fixed",
|
||||
2: "False_positive",
|
||||
3: "Skipped",
|
||||
4: "Deleted",
|
||||
5: "Already fixed",
|
||||
6: "Too_Hard",
|
||||
9: "Disabled",
|
||||
}
|
||||
public static singleton = new Maproulette()
|
||||
/*
|
||||
* The API endpoint to use
|
||||
|
|
@ -59,12 +64,11 @@ export default class Maproulette {
|
|||
if (code === "Created") {
|
||||
return Maproulette.STATUS_OPEN
|
||||
}
|
||||
for (let i = 0; i < 9; i++) {
|
||||
if (Maproulette.STATUS_MEANING["" + i] === code) {
|
||||
return i
|
||||
}
|
||||
const i = maprouletteStatus.indexOf(<any>code)
|
||||
if (i < 0) {
|
||||
return undefined
|
||||
}
|
||||
return undefined
|
||||
return i
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -78,6 +82,7 @@ export default class Maproulette {
|
|||
async closeTask(
|
||||
taskId: number,
|
||||
status = Maproulette.STATUS_FIXED,
|
||||
state: SpecialVisualizationState,
|
||||
options?: {
|
||||
comment?: string
|
||||
tags?: string
|
||||
|
|
@ -86,13 +91,16 @@ export default class Maproulette {
|
|||
}
|
||||
): Promise<void> {
|
||||
console.log("Maproulette: setting", `${this.endpoint}/task/${taskId}/${status}`, options)
|
||||
options ??= {}
|
||||
const userdetails = state.osmConnection.userDetails.data
|
||||
options.tags = `MapComplete MapComplete:${state.theme.id}; userid: ${userdetails?.uid}; username: ${userdetails?.name}`
|
||||
const response = await fetch(`${this.endpoint}/task/${taskId}/${status}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
apiKey: this.apiKey,
|
||||
},
|
||||
body: options !== undefined ? JSON.stringify(options) : undefined,
|
||||
body: JSON.stringify(options),
|
||||
})
|
||||
if (response.status !== 204) {
|
||||
console.log(`Failed to close task: ${response.status}`)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import ChangeTagAction from "./ChangeTagAction"
|
|||
import { Tag } from "../../Tags/Tag"
|
||||
import OsmChangeAction from "./OsmChangeAction"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import { Store, UIEventSource } from "../../UIEventSource"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
|
||||
export default class LinkImageAction extends OsmChangeAction {
|
||||
private readonly _proposedKey: "image" | "mapillary" | "wiki_commons" | string
|
||||
|
|
|
|||
|
|
@ -600,7 +600,7 @@ export class Changes {
|
|||
" trying again before dropping it from the changes (" +
|
||||
e +
|
||||
")"
|
||||
this._reportError(msg)
|
||||
// this._reportError(msg) // We don't report this yet, might be a temporary fluke
|
||||
const osmObj = await downloader.DownloadObjectAsync(id, 0)
|
||||
return { id, osmObj }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { LocalStorageSource } from "../Web/LocalStorageSource"
|
|||
import { AuthConfig } from "./AuthConfig"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { AndroidPolyfill } from "../Web/AndroidPolyfill"
|
||||
import { Feature, Point } from "geojson"
|
||||
|
||||
interface OsmUserInfo {
|
||||
id: number
|
||||
|
|
@ -41,6 +42,39 @@ export default class UserDetails {
|
|||
|
||||
export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable"
|
||||
|
||||
interface CapabilityResult {
|
||||
version: "0.6" | string
|
||||
generator: "OpenStreetMap server" | string
|
||||
copyright: "OpenStreetMap and contributors" | string
|
||||
attribution: "http://www.openstreetmap.org/copyright" | string
|
||||
license: "http://opendatacommons.org/licenses/odbl/1-0/" | string
|
||||
api: {
|
||||
version: { minimum: "0.6"; maximum: "0.6" }
|
||||
area: { maximum: 0.25 | number }
|
||||
note_area: { maximum: 25 | number }
|
||||
tracepoints: { per_page: 5000 | number }
|
||||
waynodes: { maximum: 2000 | number }
|
||||
relationmembers: { maximum: 32000 | number }
|
||||
changesets: {
|
||||
maximum_elements: 10000 | number
|
||||
default_query_limit: 100 | number
|
||||
maximum_query_limit: 100 | number
|
||||
}
|
||||
notes: { default_query_limit: 100 | number; maximum_query_limit: 10000 | number }
|
||||
timeout: { seconds: 300 | number }
|
||||
status: {
|
||||
database: OsmServiceState
|
||||
api: OsmServiceState
|
||||
gpx: OsmServiceState
|
||||
}
|
||||
}
|
||||
policy: {
|
||||
imagery: {
|
||||
blacklist: { regex: string }[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OsmConnection {
|
||||
public auth: osmAuth
|
||||
public userDetails: UIEventSource<UserDetails>
|
||||
|
|
@ -424,6 +458,10 @@ export class OsmConnection {
|
|||
return id
|
||||
}
|
||||
|
||||
public async getNote(id: number): Promise<Feature<Point>> {
|
||||
return JSON.parse(await this.get("notes/" + id + ".json"))
|
||||
}
|
||||
|
||||
public static GpxTrackVisibility = ["private", "public", "trackable", "identifiable"] as const
|
||||
|
||||
public async uploadGpxTrack(
|
||||
|
|
@ -466,7 +504,7 @@ export class OsmConnection {
|
|||
(options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
|
||||
"\"\r\nContent-Type: application/gpx+xml",
|
||||
}
|
||||
|
||||
user
|
||||
const boundary = "987654"
|
||||
|
||||
let body = ""
|
||||
|
|
@ -608,20 +646,26 @@ export class OsmConnection {
|
|||
return parsed
|
||||
}
|
||||
|
||||
private async FetchCapabilities(): Promise<{ api: OsmServiceState; gpx: OsmServiceState }> {
|
||||
private async FetchCapabilities(): Promise<{
|
||||
api: OsmServiceState
|
||||
gpx: OsmServiceState
|
||||
database: OsmServiceState
|
||||
}> {
|
||||
if (Utils.runningFromConsole) {
|
||||
return { api: "online", gpx: "online" }
|
||||
return { api: "online", gpx: "online", database: "online" }
|
||||
}
|
||||
const result = await Utils.downloadAdvanced(this.Backend() + "/api/0.6/capabilities")
|
||||
if (result["content"] === undefined) {
|
||||
console.log("Something went wrong:", result)
|
||||
return { api: "unreachable", gpx: "unreachable" }
|
||||
try {
|
||||
const result = await Utils.downloadJson<CapabilityResult>(
|
||||
this.Backend() + "/api/0.6/capabilities.json"
|
||||
)
|
||||
if (result?.api?.status === undefined) {
|
||||
console.log("Something went wrong:", result)
|
||||
return { api: "unreachable", gpx: "unreachable", database: "unreachable" }
|
||||
}
|
||||
return result.api.status
|
||||
} catch (e) {
|
||||
console.error("Could not fetch capabilities")
|
||||
return { api: "offline", gpx: "offline", database: "online" }
|
||||
}
|
||||
const xmlRaw = result["content"]
|
||||
const parsed = new DOMParser().parseFromString(xmlRaw, "text/xml")
|
||||
const statusEl = parsed.getElementsByTagName("status")[0]
|
||||
const api = <OsmServiceState>statusEl.getAttribute("api")
|
||||
const gpx = <OsmServiceState>statusEl.getAttribute("gpx")
|
||||
return { api, gpx }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import osmtogeojson from "osmtogeojson"
|
|||
import { FeatureCollection } from "@turf/turf"
|
||||
import { Geometry } from "geojson"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
|
||||
;("use strict")
|
||||
/**
|
||||
* Interfaces overpass to get all the latest data
|
||||
*/
|
||||
|
|
@ -74,9 +74,9 @@ export class Overpass {
|
|||
console.warn("No features for", json)
|
||||
}
|
||||
|
||||
const geojson = osmtogeojson(json)
|
||||
const geojson = <FeatureCollection<Geometry, OsmTags>>osmtogeojson(json)
|
||||
const osmTime = new Date(json.osm3s.timestamp_osm_base)
|
||||
return [<any>geojson, osmTime]
|
||||
return [geojson, osmTime]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,14 +1,42 @@
|
|||
import { Utils } from "../../Utils"
|
||||
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
|
||||
export class ThemeMetaTagging {
|
||||
public static readonly themeName = "usersettings"
|
||||
public static readonly themeName = "usersettings"
|
||||
|
||||
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
|
||||
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
|
||||
feat.properties['__current_backgroun'] = 'initial_value'
|
||||
}
|
||||
}
|
||||
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
|
||||
feat.properties._description
|
||||
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
|
||||
?.at(1)
|
||||
)
|
||||
Utils.AddLazyProperty(
|
||||
feat.properties,
|
||||
"_d",
|
||||
() => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? ""
|
||||
)
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
|
||||
((feat) => {
|
||||
const e = document.createElement("div")
|
||||
e.innerHTML = feat.properties._d
|
||||
return Array.from(e.getElementsByTagName("a")).filter(
|
||||
(a) => a.href.match(/mastodon|en.osm.town/) !== null
|
||||
)[0]?.href
|
||||
})(feat)
|
||||
)
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
|
||||
((feat) => {
|
||||
const e = document.createElement("div")
|
||||
e.innerHTML = feat.properties._d
|
||||
return Array.from(e.getElementsByTagName("a")).filter(
|
||||
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
|
||||
)[0]?.href
|
||||
})(feat)
|
||||
)
|
||||
Utils.AddLazyProperty(
|
||||
feat.properties,
|
||||
"_mastodon_candidate",
|
||||
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
|
||||
)
|
||||
feat.properties["__current_backgroun"] = "initial_value"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Utils } from "../Utils"
|
||||
import { Readable, Subscriber, Unsubscriber, Updater, Writable } from "svelte/store"
|
||||
|
||||
/**
|
||||
* Various static utils
|
||||
*/
|
||||
|
|
@ -276,7 +275,7 @@ export abstract class Store<T> implements Readable<T> {
|
|||
|
||||
public bindD<X>(
|
||||
f: (t: Exclude<T, undefined | null>) => Store<X>,
|
||||
extraSources: UIEventSource<object>[] = []
|
||||
extraSources: Store<any>[] = []
|
||||
): Store<X> {
|
||||
return this.bind((t) => {
|
||||
if (t === null) {
|
||||
|
|
@ -952,15 +951,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
g: (j: J, t: T) => T,
|
||||
allowUnregister = false
|
||||
): UIEventSource<J> {
|
||||
const self = this
|
||||
|
||||
const stack = new Error().stack.split("\n")
|
||||
const callee = stack[1]
|
||||
|
||||
const newSource = new UIEventSource<J>(f(this.data), "map(" + this.tag + ")@" + callee)
|
||||
|
||||
const update = function () {
|
||||
newSource.setData(f(self.data))
|
||||
const update = () => {
|
||||
newSource.setData(f(this.data))
|
||||
return allowUnregister && newSource._callbacks.length() === 0
|
||||
}
|
||||
|
||||
|
|
@ -971,7 +968,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
|
||||
if (g !== undefined) {
|
||||
newSource.addCallback((latest) => {
|
||||
self.setData(g(latest, self.data))
|
||||
this.setData(g(latest, this.data))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -980,8 +977,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
|
||||
public syncWith(otherSource: UIEventSource<T>, reverseOverride = false): UIEventSource<T> {
|
||||
this.addCallback((latest) => otherSource.setData(latest))
|
||||
const self = this
|
||||
otherSource.addCallback((latest) => self.setData(latest))
|
||||
otherSource.addCallback((latest) => this.setData(latest))
|
||||
if (reverseOverride) {
|
||||
if (otherSource.data !== undefined) {
|
||||
this.setData(otherSource.data)
|
||||
|
|
|
|||
|
|
@ -421,6 +421,7 @@ export default class LinkedDataLoader {
|
|||
delete output["chargeEnd"]
|
||||
delete output["chargeStart"]
|
||||
delete output["timeUnit"]
|
||||
delete output["id"]
|
||||
|
||||
asBoolean("covered")
|
||||
asBoolean("fee", true)
|
||||
|
|
@ -518,6 +519,9 @@ export default class LinkedDataLoader {
|
|||
property: string,
|
||||
variable?: string
|
||||
): Promise<SparqlResult<T, G>> {
|
||||
if (property === "schema:photos") {
|
||||
console.log(">> Getting photos")
|
||||
}
|
||||
const results = await new TypedSparql().typedSparql<T, G>(
|
||||
{
|
||||
schema: "http://schema.org/",
|
||||
|
|
@ -531,15 +535,23 @@ export default class LinkedDataLoader {
|
|||
" ?parking a <http://schema.mobivoc.org/BicycleParkingStation>",
|
||||
"?parking " + property + " " + (variable ?? "")
|
||||
)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param url
|
||||
* @param property
|
||||
* @param subExpr
|
||||
* @private
|
||||
*/
|
||||
private static async fetchVeloparkGraphProperty<T extends string>(
|
||||
url: string,
|
||||
property: string,
|
||||
subExpr?: string
|
||||
): Promise<SparqlResult<T, "g">> {
|
||||
return await new TypedSparql().typedSparql<T, "g">(
|
||||
const result = await new TypedSparql().typedSparql<T, "g">(
|
||||
{
|
||||
schema: "http://schema.org/",
|
||||
mv: "http://schema.mobivoc.org/",
|
||||
|
|
@ -551,8 +563,15 @@ export default class LinkedDataLoader {
|
|||
"g",
|
||||
" ?parking a <http://schema.mobivoc.org/BicycleParkingStation>",
|
||||
|
||||
S.graph("g", "?section " + property + " " + (subExpr ?? ""), "?section a ?type")
|
||||
S.graph(
|
||||
"g",
|
||||
"?section " + property + " " + (subExpr ?? ""),
|
||||
"?section a ?type",
|
||||
"BIND(STR(?section) AS ?id)"
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -569,26 +588,69 @@ export default class LinkedDataLoader {
|
|||
continue
|
||||
}
|
||||
for (const sectionKey in subResult) {
|
||||
if (!r[sectionKey]) {
|
||||
r[sectionKey] = {}
|
||||
}
|
||||
const section = subResult[sectionKey]
|
||||
for (const key in section) {
|
||||
r[sectionKey][key] ??= section[key]
|
||||
if (sectionKey === "default") {
|
||||
r["default"] ??= {}
|
||||
const section = subResult["default"]
|
||||
for (const key in section) {
|
||||
r["default"][key] ??= section[key]
|
||||
}
|
||||
} else {
|
||||
const section = subResult[sectionKey]
|
||||
const actualId = Array.from(section["id"] ?? [])[0] ?? sectionKey
|
||||
r[actualId] ??= {}
|
||||
for (const key in section) {
|
||||
r[actualId][key] ??= section[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (r["default"] !== undefined && Object.keys(r).length > 1) {
|
||||
/**
|
||||
* Copy all values from the section with name "key" into the other sections,
|
||||
* remove section "key" afterwards
|
||||
* @param key
|
||||
*/
|
||||
function spreadSection(key: string) {
|
||||
for (const section in r) {
|
||||
if (section === "default") {
|
||||
if (section === key) {
|
||||
continue
|
||||
}
|
||||
for (const k in r.default) {
|
||||
r[section][k] ??= r.default[k]
|
||||
for (const k in r[key]) {
|
||||
r[section][k] ??= r[key][k]
|
||||
}
|
||||
}
|
||||
delete r.default
|
||||
delete r[key]
|
||||
}
|
||||
|
||||
// The "default" part of the result contains all general info
|
||||
// The other 'sections' need to get those copied! Then, we delete the "default"-section
|
||||
if (r["default"] !== undefined && Object.keys(r).length > 1) {
|
||||
spreadSection("default")
|
||||
}
|
||||
if (Object.keys(r).length > 1) {
|
||||
// This result has multiple sections
|
||||
// We should check that the naked URL got distributed and scrapped
|
||||
const keys = Object.keys(r)
|
||||
if (Object.keys(r).length > 2) {
|
||||
console.log("Multiple sections detected: ", JSON.stringify(keys))
|
||||
}
|
||||
const shortestKeyLength: number = Math.min(...keys.map((k) => k.length))
|
||||
const key = keys.find((k) => k.length === shortestKeyLength)
|
||||
if (keys.some((k) => !k.startsWith(key))) {
|
||||
throw (
|
||||
"Invalid multi-object: the shortest key is not the start of all the others: " +
|
||||
JSON.stringify(keys)
|
||||
)
|
||||
}
|
||||
spreadSection(key)
|
||||
}
|
||||
if (Object.keys(r).length == 1) {
|
||||
const key = Object.keys(r)[0]
|
||||
if (key.indexOf("#") > 0) {
|
||||
const newKey = key.split("#")[0]
|
||||
r[newKey] = r[key]
|
||||
delete r[key]
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
|
@ -675,6 +737,7 @@ export default class LinkedDataLoader {
|
|||
/**
|
||||
* Fetches all data relevant to velopark.
|
||||
* The id will be saved as `ref:velopark`
|
||||
* If the entry has multiple sections, this will return multiple items
|
||||
* @param url
|
||||
*/
|
||||
public static async fetchVeloparkEntry(
|
||||
|
|
@ -685,6 +748,7 @@ export default class LinkedDataLoader {
|
|||
if (this.veloparkCache[cacheKey]) {
|
||||
return this.veloparkCache[cacheKey]
|
||||
}
|
||||
// Note: the proxy doesn't make any changes in this case
|
||||
const withProxyUrl = Constants.linkedDataProxy.replace("{url}", encodeURIComponent(url))
|
||||
const optionalPaths: Record<string, string | Record<string, string>> = {
|
||||
"schema:interactionService": {
|
||||
|
|
@ -697,6 +761,7 @@ export default class LinkedDataLoader {
|
|||
"schema:email": "email",
|
||||
"schema:telephone": "phone",
|
||||
},
|
||||
// "schema:photos": "images",
|
||||
"schema:dateModified": "_last_edit_timestamp",
|
||||
}
|
||||
if (includeExtras) {
|
||||
|
|
@ -740,9 +805,22 @@ export default class LinkedDataLoader {
|
|||
graphOptionalPaths,
|
||||
extra
|
||||
)
|
||||
for (const unpatchedKey in unpatched) {
|
||||
// Dirty hack
|
||||
const rawData = await Utils.downloadJsonCached<object>(url, 1000 * 60 * 60)
|
||||
const images = rawData["photos"]?.map((ph) => <string>ph.image)
|
||||
if (images) {
|
||||
unpatched[unpatchedKey].images = new Set<string>(images)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Got unpatched:", unpatched)
|
||||
const patched: Feature[] = []
|
||||
for (const section in unpatched) {
|
||||
for (let section in unpatched) {
|
||||
const p = LinkedDataLoader.patchVeloparkProperties(unpatched[section])
|
||||
if (Object.keys(unpatched).length === 1 && section.endsWith("#section1")) {
|
||||
section = section.split("#")[0]
|
||||
}
|
||||
p["ref:velopark"] = [section]
|
||||
patched.push(LinkedDataLoader.asGeojson(p))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,13 +67,14 @@ export default class TypedSparql {
|
|||
bindings.forEach((item) => {
|
||||
const result = <Record<VARS | G, Set<string>>>{}
|
||||
item.forEach((value, key) => {
|
||||
if (!result[key.value]) {
|
||||
result[key.value] = new Set()
|
||||
}
|
||||
result[key.value] ??= new Set()
|
||||
result[key.value].add(value.value)
|
||||
})
|
||||
if (graphVariable && result[graphVariable]?.size > 0) {
|
||||
const id = Array.from(result[graphVariable])?.[0] ?? "default"
|
||||
const id: string =
|
||||
<string>Array.from(result["id"] ?? [])?.[0] ??
|
||||
Array.from(result[graphVariable] ?? [])?.[0] ??
|
||||
"default"
|
||||
resultAllGraphs[id] = result
|
||||
} else {
|
||||
resultAllGraphs["default"] = result
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue