Merge develop

This commit is contained in:
Pieter Vander Vennet 2023-12-03 04:44:59 +01:00
commit d959b6b40b
290 changed files with 37178 additions and 2200 deletions

View file

@ -137,7 +137,6 @@ export default class GeoLocationHandler {
}
}
console.trace("Moving the map to the GPS-location")
mapLocation.setData({
lon: newLocation.longitude,
lat: newLocation.latitude,
@ -152,7 +151,6 @@ export default class GeoLocationHandler {
private CopyGeolocationIntoMapstate() {
const features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
this.currentUserLocation = new StaticFeatureSource(features)
const keysToCopy = ["speed", "accuracy", "altitude", "altitudeAccuracy", "heading"]
let i = 0
this.geolocationState.currentGPSLocation.addCallbackAndRun((location) => {
if (location === undefined) {
@ -163,18 +161,15 @@ export default class GeoLocationHandler {
id: "gps-" + i,
"user:location": "yes",
date: new Date().toISOString(),
// GeolocationObject behaves really weird when indexing, so copying it one by one is the most stable
accuracy: location.accuracy,
speed: location.speed,
altitude: location.altitude,
altitudeAccuracy: location.altitudeAccuracy,
heading: location.heading,
}
i++
for (const k in keysToCopy) {
// For some weird reason, the 'Object.keys' method doesn't work for the 'location: GeolocationCoordinates'-object and will thus not copy all the properties when using {...location}
// As such, they are copied here
if (location[k]) {
properties[k] = location[k]
}
}
properties["_all"] = JSON.stringify(location)
const feature = <Feature>{
type: "Feature",
properties,
@ -183,7 +178,6 @@ export default class GeoLocationHandler {
coordinates: [location.longitude, location.latitude],
},
}
features.setData([feature])
})
}

View file

@ -48,6 +48,7 @@ export class PreferredRasterLayerSelector {
this._preferredBackgroundLayer.addCallbackD((_) => self.updateLayer())
this._availableLayers.addCallbackD((_) => self.updateLayer())
self.updateLayer()
}
/**
@ -63,6 +64,12 @@ export class PreferredRasterLayerSelector {
const foundLayer = isCategory
? available.find((l) => l.properties.category === targetLayerId)
: available.find((l) => l.properties.id === targetLayerId)
console.debug("Updating background layer to", foundLayer?.id, {
targetLayerId,
queryParam: this._queryParameter?.data,
preferred: this._preferredBackgroundLayer?.data,
isCategory,
})
if (foundLayer) {
this._rasterLayerSetting.setData(foundLayer)
return true

View file

@ -31,11 +31,12 @@ export default class GenericImageProvider extends ImageProvider {
key: key,
url: value,
provider: this,
id: value
}),
]
}
SourceIcon(backlinkSource?: string) {
SourceIcon() {
return undefined
}

View file

@ -6,13 +6,14 @@ import { Utils } from "../../Utils"
export interface ProvidedImage {
url: string
key: string
provider: ImageProvider
provider: ImageProvider,
id: string
}
export default abstract class ImageProvider {
public abstract readonly defaultKeyPrefixes: string[]
public abstract SourceIcon(backlinkSource?: string): BaseUIElement
public abstract SourceIcon(id?: string, location?: {lon: number, lat: number}): BaseUIElement
/**
* Given a properies object, maps it onto _all_ the available pictures for this imageProvider
@ -28,7 +29,7 @@ export default abstract class ImageProvider {
throw "No `defaultKeyPrefixes` defined by this image provider"
}
const relevantUrls = new UIEventSource<
{ url: string; key: string; provider: ImageProvider }[]
{ id: string, url: string; key: string; provider: ImageProvider }[]
>([])
const seenValues = new Set<string>()
allTags.addCallbackAndRunD((tags) => {
@ -67,4 +68,10 @@ export default abstract class ImageProvider {
public abstract DownloadAttribution(url: string): Promise<LicenseInfo>
public abstract apiUrls(): string[]
public backlink(): string | undefined {
return undefined
}
}

View file

@ -66,6 +66,7 @@ export class Imgur extends ImageProvider implements ImageUploader {
url: value,
key: key,
provider: this,
id: value
}),
]
}

View file

@ -4,6 +4,7 @@ import Svg from "../../Svg"
import { Utils } from "../../Utils"
import { LicenseInfo } from "./LicenseInfo"
import Constants from "../../Models/Constants"
import Link from "../../UI/Base/Link"
export class Mapillary extends ImageProvider {
public static readonly singleton = new Mapillary()
@ -17,10 +18,6 @@ export class Mapillary extends ImageProvider {
]
defaultKeyPrefixes = ["mapillary", "image"]
apiUrls(): string[] {
return ["https://mapillary.com", "https://www.mapillary.com", "https://graph.mapillary.com"]
}
/**
* Indicates that this is the same URL
* Ignores 'stp' parameter
@ -57,6 +54,22 @@ export class Mapillary extends ImageProvider {
return false
}
static createLink(location: {
lon: number,
lat: number
} = undefined, zoom: number = 17, pKey?: string) {
const params = {
focus: pKey === undefined ? "map" : "photo",
lat: location.lat,
lng: location.lon,
z: location === undefined ? undefined : Math.max((zoom ?? 2) - 1, 1),
pKey,
}
const baselink = `https://www.mapillary.com/app/?`
const paramsStr = Utils.NoNull(Object.keys(params).map(k => params[k] === undefined ? undefined : k + "=" + params[k]))
return baselink + paramsStr.join("&")
}
/**
* Returns the correct key for API v4.0
*/
@ -80,8 +93,19 @@ export class Mapillary extends ImageProvider {
return undefined
}
SourceIcon(backlinkSource?: string): BaseUIElement {
return Svg.mapillary_svg()
apiUrls(): string[] {
return ["https://mapillary.com", "https://www.mapillary.com", "https://graph.mapillary.com"]
}
SourceIcon(id: string, location?: {
lon: number,
lat: number
}): BaseUIElement {
const icon = Svg.mapillary_svg()
if (!id) {
return icon
}
return new Link(icon, Mapillary.createLink(location, 16, "" + id), true)
}
async ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]> {
@ -111,6 +135,7 @@ export class Mapillary extends ImageProvider {
const response = await Utils.downloadJsonCached(metadataUrl, 60 * 60)
const url = <string>response["thumb_1024_url"]
return {
id: "" + mapillaryId,
url: url,
provider: this,
key: key,

View file

@ -15,7 +15,7 @@ export class WikidataImageProvider extends ImageProvider {
super()
}
public SourceIcon(_?: string): BaseUIElement {
public SourceIcon(): BaseUIElement {
return Svg.wikidata_svg()
}

View file

@ -1,7 +1,6 @@
import ImageProvider, { ProvidedImage } from "./ImageProvider"
import BaseUIElement from "../../UI/BaseUIElement"
import Svg from "../../Svg"
import Link from "../../UI/Base/Link"
import { Utils } from "../../Utils"
import { LicenseInfo } from "./LicenseInfo"
import Wikimedia from "../Web/Wikimedia"
@ -70,17 +69,8 @@ export class WikimediaImageProvider extends ImageProvider {
return WikimediaImageProvider.apiUrls
}
SourceIcon(backlink: string): BaseUIElement {
const img = Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em")
if (backlink === undefined) {
return img
}
return new Link(
Svg.wikimedia_commons_white_svg(),
`https://commons.wikimedia.org/wiki/${backlink}`,
true
)
SourceIcon(): BaseUIElement {
return Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em")
}
public PrepUrl(value: string): ProvidedImage {
@ -173,6 +163,6 @@ export class WikimediaImageProvider extends ImageProvider {
if (!image.startsWith("File:")) {
image = "File:" + image
}
return { url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this }
return { url: WikimediaImageProvider.PrepareUrl(image), key: undefined, provider: this , id: image}
}
}

View file

@ -31,7 +31,7 @@ export class Stores {
* @param promise
* @constructor
*/
public static FromPromise<T>(promise: Promise<T>): Store<T> {
public static FromPromise<T>(promise: Promise<T>): Store<T | undefined> {
const src = new UIEventSource<T>(undefined)
promise?.then((d) => src.setData(d))
promise?.catch((err) => console.warn("Promise failed:", err))
@ -97,7 +97,7 @@ export abstract class Store<T> implements Readable<T> {
abstract map<J>(f: (t: T) => J): Store<J>
abstract map<J>(f: (t: T) => J, extraStoresToWatch: Store<any>[]): Store<J>
public mapD<J>(f: (t: T) => J, extraStoresToWatch?: Store<any>[]): Store<J> {
public mapD<J>(f: (t: Exclude<T, undefined | null>) => J, extraStoresToWatch?: Store<any>[]): Store<J> {
return this.map((t) => {
if (t === undefined) {
return undefined
@ -105,7 +105,7 @@ export abstract class Store<T> implements Readable<T> {
if (t === null) {
return null
}
return f(t)
return f(<Exclude<T, undefined | null>> t)
}, extraStoresToWatch)
}
@ -603,7 +603,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
*/
public static FromPromiseWithErr<T>(
promise: Promise<T>
): UIEventSource<{ success: T } | { error: any }> {
): UIEventSource<{ success: T } | { error: any } | undefined> {
const src = new UIEventSource<{ success: T } | { error: any }>(undefined)
promise?.then((d) => src.setData({ success: d }))
promise?.catch((err) => src.setData({ error: err }))
@ -771,18 +771,21 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
* Monoidal map which results in a read-only store. 'undefined' is passed 'as is'
* Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)'
*/
public mapD<J>(f: (t: T) => J, extraSources: Store<any>[] = []): Store<J | undefined> {
public mapD<J>(f: (t: Exclude<T, undefined | null>) => J, extraSources: Store<any>[] = []): Store<J | undefined> {
return new MappedStore(
this,
(t) => {
if (t === undefined) {
return undefined
}
return f(t)
if (t === null) {
return null
}
return f(<Exclude<T, undefined | null>> t)
},
extraSources,
this._callbacks,
this.data === undefined ? undefined : f(this.data)
(this.data === undefined || this.data === null) ?(<undefined | null> this.data) : f(<any> this.data)
)
}

View file

@ -40,15 +40,26 @@ export interface P4CPicture {
export default class NearbyImagesSearch {
public static readonly services = ["mapillary", "flickr", "kartaview", "wikicommons"] as const
public static readonly apiUrls = ["https://api.flickr.com"]
private readonly individualStores: Store<{ images: P4CPicture[]; beforeFilter: number }>[]
private readonly individualStores: Store<{ images: P4CPicture[]; beforeFilter: number } | undefined>[]
private readonly _store: UIEventSource<P4CPicture[]> = new UIEventSource<P4CPicture[]>([])
public readonly store: Store<P4CPicture[]> = this._store
public readonly allDone: Store<boolean>
private readonly _options: NearbyImageOptions
constructor(options: NearbyImageOptions, features: IndexedFeatureSource) {
this.individualStores = NearbyImagesSearch.services.map((s) =>
NearbyImagesSearch.buildPictureFetcher(options, s)
)
const allDone = new UIEventSource(false)
this.allDone = allDone
const self = this
function updateAllDone(){
const stillRunning = self.individualStores.some(store => store.data === undefined)
allDone.setData(!stillRunning)
}
self.individualStores.forEach(s => s.addCallback(_ => updateAllDone()))
this._options = options
if (features !== undefined) {
const osmImages = new ImagesInLoadedDataFetcher(features).fetchAround({
@ -93,13 +104,17 @@ export default class NearbyImagesSearch {
private static buildPictureFetcher(
options: NearbyImageOptions,
fetcher: P4CService
): Store<{ images: P4CPicture[]; beforeFilter: number }> {
const p4cStore = Stores.FromPromise<P4CPicture[]>(
): Store<{ images: P4CPicture[]; beforeFilter: number } | null | undefined> {
const p4cStore = Stores.FromPromiseWithErr<P4CPicture[]>(
NearbyImagesSearch.fetchImages(options, fetcher)
)
const searchRadius = options.searchRadius ?? 100
return p4cStore.map(
(images) => {
return p4cStore.mapD(
(imagesState) => {
if(imagesState["error"]){
return null
}
let images = imagesState["success"]
if (images === undefined) {
return undefined
}