Merge master

This commit is contained in:
Pieter Vander Vennet 2024-08-09 17:02:59 +02:00
commit 51fa48a01f
151 changed files with 16260 additions and 1872 deletions

View file

@ -165,9 +165,12 @@ class MvtFeatureBuilder {
i += commandCount * 2
}
if (commandId === 7) {
if(currentRing.length === 0){
console.error("Invalid MVT file: got a 'closePath', but the currentRing is empty. Full command:", commandInteger)
}else{
if (currentRing.length === 0) {
console.error(
"Invalid MVT file: got a 'closePath', but the currentRing is empty. Full command:",
commandInteger
)
} else {
currentRing.push([...currentRing[0]])
}
i++
@ -438,7 +441,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
}
this._features.setData(features)
} catch (e) {
console.error("Could not download MVT "+this._url+" tile due to", e)
console.error("Could not download MVT " + this._url + " tile due to", e)
}
}

View file

@ -28,7 +28,7 @@ export default class AllImageProviders {
Mapillary.singleton,
WikidataImageProvider.singleton,
WikimediaImageProvider.singleton,
AllImageProviders.genericImageProvider
AllImageProviders.genericImageProvider,
]
public static apiUrls: string[] = [].concat(
...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls())
@ -52,15 +52,13 @@ export default class AllImageProviders {
}
public static async selectBestProvider(key: string, value: string): Promise<ImageProvider> {
for (const imageProvider of AllImageProviders.ImageAttributionSource) {
try{
const extracted = await Promise.all(await imageProvider.ExtractUrls(key, value))
if(extracted?.length > 0){
return imageProvider
}
}catch (e) {
try {
const extracted = await Promise.all(await imageProvider.ExtractUrls(key, value))
if (extracted?.length > 0) {
return imageProvider
}
} catch (e) {
console.warn("Provider gave an error while trying to determine a match:", e)
}
}

View file

@ -134,7 +134,9 @@ export class WikimediaImageProvider extends ImageProvider {
"titles=" +
filename +
"&format=json&origin=*"
const data = await Utils.downloadJsonCached<{query: {pages: {title: string, imageinfo: { extmetadata} []}[]}}>(url, 365 * 24 * 60 * 60)
const data = await Utils.downloadJsonCached<{
query: { pages: { title: string; imageinfo: { extmetadata }[] }[] }
}>(url, 365 * 24 * 60 * 60)
const licenseInfo = new LicenseInfo()
const pageInfo = data.query.pages.at(-1)
if (pageInfo === undefined) {

View file

@ -576,7 +576,7 @@ export class Changes {
if (createdIds.has(c.id)) {
toUpload.push(c)
} else {
(this._reportError)(
this._reportError(
`Got an orphaned change. The 'creation'-change description for ${c.type}/${c.id} got lost. Permanently dropping this change:` +
JSON.stringify(c)
)

View file

@ -24,7 +24,9 @@ export class Geocoding {
const b = bbox ?? BBox.global
const url = `${
Geocoding.host
}search?format=json&limit=1&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${Locale.language.data}&q=${query}`
}search?format=json&limit=1&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${
Locale.language.data
}&q=${query}`
return Utils.downloadJson(url)
}

View file

@ -24,7 +24,7 @@ export abstract class OsmObject {
// @ts-ignore
this.type = type
this.tags = {
id: `${this.type}/${id}`
id: `${this.type}/${id}`,
}
}
@ -55,7 +55,7 @@ export abstract class OsmObject {
const allGeojsons = OsmToGeoJson(
{ elements },
{
flatProperties: true
flatProperties: true,
}
)
const feature = allGeojsons.features.find(
@ -120,7 +120,7 @@ export abstract class OsmObject {
const blacklist = polygonFeature.polygon === "blacklist"
result.set(key, {
values: new Set<string>(polygonFeature.values),
blacklist: blacklist
blacklist: blacklist,
})
}
@ -151,7 +151,9 @@ export abstract class OsmObject {
}
const v = this.tags[key]
if (v !== "" && v !== undefined) {
tags += ` <tag k="${Utils.EncodeXmlValue(key)}" v="${Utils.EncodeXmlValue(this.tags[key])}"/>
tags += ` <tag k="${Utils.EncodeXmlValue(key)}" v="${Utils.EncodeXmlValue(
this.tags[key]
)}"/>
`
}
}
@ -212,7 +214,7 @@ export class OsmNode extends OsmObject {
ChangesetXML(changesetId: string, header?: string): string {
const tags = this.TagsXML()
return ` <node id="${this.id}" ${header ?? ""} ${
changesetId ? " changeset=\"" + changesetId + "\" " : ""
changesetId ? ' changeset="' + changesetId + '" ' : ""
}${this.VersionXML()} lat="${this.lat}" lon="${this.lon}">
${tags} </node>
`
@ -233,8 +235,8 @@ ${tags} </node>
properties: this.tags,
geometry: {
type: "Point",
coordinates: [this.lon, this.lat]
}
coordinates: [this.lon, this.lat],
},
}
}
}
@ -264,11 +266,11 @@ export class OsmWay extends OsmObject {
const tags = this.TagsXML()
let nds = ""
for (const node in this.nodes) {
nds += " <nd ref=\"" + this.nodes[node] + "\"/>\n"
nds += ' <nd ref="' + this.nodes[node] + '"/>\n'
}
return ` <way id="${this.id}" ${header ?? ""} ${
changesetId ? "changeset=\"" + changesetId + "\" " : ""
changesetId ? 'changeset="' + changesetId + '" ' : ""
} ${this.VersionXML()}>
${nds}${tags} </way>
`
@ -313,18 +315,18 @@ ${nds}${tags} </way>
if (this.isPolygon()) {
geometry = {
type: "Polygon",
coordinates: [coordinates]
coordinates: [coordinates],
}
} else {
geometry = {
type: "LineString",
coordinates: coordinates
coordinates: coordinates,
}
}
return {
type: "Feature",
properties: <any>this.tags,
geometry
geometry,
}
}

View file

@ -54,8 +54,14 @@ export class Overpass {
return `${this._interpreterUrl}?data=${encodeURIComponent(query)}`
}
private async ExecuteQuery(query: string): Promise<[FeatureCollection<Geometry, OsmTags>, Date]> {
const json = await Utils.downloadJson<{elements: [], remark, osm3s: {timestamp_osm_base: string}}>(this.buildUrl(query))
private async ExecuteQuery(
query: string
): Promise<[FeatureCollection<Geometry, OsmTags>, Date]> {
const json = await Utils.downloadJson<{
elements: []
remark
osm3s: { timestamp_osm_base: string }
}>(this.buildUrl(query))
if (json.elements.length === 0 && json.remark !== undefined) {
console.warn("Timeout or other runtime error while querying overpass", json.remark)

View file

@ -194,11 +194,17 @@ export class And extends TagsFilter {
* const expr = <And> TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} )
* expr.removePhraseConsideredKnown(new Tag("club","climbing"), false) // => expr
*/
removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): (TagsFilterClosed & OptimizedTag) | boolean {
removePhraseConsideredKnown(
knownExpression: TagsFilter,
value: boolean
): (TagsFilterClosed & OptimizedTag) | boolean {
const newAnds: TagsFilter[] = []
for (const tag of this.and) {
if (tag instanceof And) {
throw "Optimize expressions before using removePhraseConsideredKnown. Found an AND in an AND: " + this.asHumanString()
throw (
"Optimize expressions before using removePhraseConsideredKnown. Found an AND in an AND: " +
this.asHumanString()
)
}
if (tag instanceof Or) {
// Second try
@ -449,9 +455,9 @@ export class And extends TagsFilter {
} else {
const newOrs: TagsFilterClosed[] = []
for (const containedOr of containedOrs) {
const elements: (FlatTag | (And & OptimizedTag))[] = TagTypes.safeOr( containedOr).filter(
(candidate) => !commonValues.some((cv) => cv.shadows(candidate))
)
const elements: (FlatTag | (And & OptimizedTag))[] = TagTypes.safeOr(
containedOr
).filter((candidate) => !commonValues.some((cv) => cv.shadows(candidate)))
if (elements.length > 0) {
newOrs.push(Or.construct(elements))
}
@ -464,7 +470,7 @@ export class And extends TagsFilter {
return false
} else if (result === true) {
// neutral element: skip
}else if(result instanceof And) {
} else if (result instanceof And) {
newAnds.push(...TagTypes.safeAnd(result))
} else {
newAnds.push(result)

View file

@ -123,7 +123,7 @@ export default class ComparingTag extends TagsFilter {
}
optimize(): (ComparingTag & OptimizedTag) | boolean {
return <any> this
return <any>this
}
isNegative(): boolean {

View file

@ -19,7 +19,7 @@ export class Or extends TagsFilter {
public static construct(or: TagsFilter[]): TagsFilter
public static construct<T extends TagsFilter>(or: [T]): T
public static construct(or: ((And & OptimizedTag) | FlatTag)[]): (TagsFilterClosed & OptimizedTag)
public static construct(or: ((And & OptimizedTag) | FlatTag)[]): TagsFilterClosed & OptimizedTag
public static construct(or: TagsFilter[]): TagsFilter {
if (or.length === 1) {
return or[0]
@ -63,7 +63,11 @@ export class Or extends TagsFilter {
return choices
}
asHumanString(linkToWiki: boolean = false, shorten: boolean = false, properties: Record<string, string> = {}) {
asHumanString(
linkToWiki: boolean = false,
shorten: boolean = false,
properties: Record<string, string> = {}
) {
return this.or
.map((t) => {
let e = t.asHumanString(linkToWiki, shorten, properties)
@ -125,11 +129,17 @@ export class Or extends TagsFilter {
* new Or([ new Tag("key","value") ]).removePhraseConsideredKnown(new Tag("key","value"), false) // => false
* new Or([new RegexTag("x", "y", true),new RegexTag("c", "d")]).removePhraseConsideredKnown(new Tag("foo","bar"), false) // => new Or([new RegexTag("c", "d"), new RegexTag("x", "y", true)])
*/
removePhraseConsideredKnown(knownExpression: TagsFilter, value: boolean): (TagsFilterClosed & OptimizedTag) | boolean {
removePhraseConsideredKnown(
knownExpression: TagsFilter,
value: boolean
): (TagsFilterClosed & OptimizedTag) | boolean {
const newOrs: TagsFilter[] = []
for (const tag of this.or) {
if (tag instanceof Or) {
throw "Optimize expressions before using removePhraseConsideredKnown. Found an OR in an OR: " + this.asHumanString()
throw (
"Optimize expressions before using removePhraseConsideredKnown. Found an OR in an OR: " +
this.asHumanString()
)
}
if (tag instanceof And) {
const r = tag.removePhraseConsideredKnown(knownExpression, value)
@ -203,7 +213,9 @@ export class Or extends TagsFilter {
// partition of all the ands
containedAnds.push(tf)
} else {
newOrs.push(<(Tag | (And & OptimizedTag) | RegexTag | SubstitutingTag | ComparingTag)>tf)
newOrs.push(
<Tag | (And & OptimizedTag) | RegexTag | SubstitutingTag | ComparingTag>tf
)
}
}
@ -230,13 +242,14 @@ export class Or extends TagsFilter {
continue // clean up with the other known values
}
if(cleaned instanceof Or){
if (cleaned instanceof Or) {
// An optimized 'or' should not contain 'ors', we can safely cast
newOrs.push(...TagTypes.safeOr(cleaned))
continue
}
const noAnd: OptimizedTag & (Tag | RegexTag | SubstitutingTag | ComparingTag) = cleaned
const noAnd: OptimizedTag &
(Tag | RegexTag | SubstitutingTag | ComparingTag) = cleaned
// the 'and' dissolved into a normal tag -> it has to be added to the newOrs
newOrs.push(noAnd)
dirty = true // rerun this algo later on
@ -263,9 +276,9 @@ export class Or extends TagsFilter {
} else {
const newAnds: TagsFilterClosed[] = []
for (const containedAnd of containedAnds) {
const elements: (FlatTag | (Or & OptimizedTag))[] = TagTypes.safeAnd(containedAnd).filter(
(candidate) => !commonValues.some((cv) => cv.shadows(candidate))
)
const elements: (FlatTag | (Or & OptimizedTag))[] = TagTypes.safeAnd(
containedAnd
).filter((candidate) => !commonValues.some((cv) => cv.shadows(candidate)))
if (elements.length > 0) {
newAnds.push(And.construct(elements))
}
@ -280,10 +293,9 @@ export class Or extends TagsFilter {
return true
} else if (result === false) {
// neutral element: skip
} else if(result instanceof Or){
} else if (result instanceof Or) {
newOrs.push(...TagTypes.safeOr(result))
}else
newOrs.push(result)
} else newOrs.push(result)
}
}

View file

@ -356,7 +356,7 @@ export class RegexTag extends TagsFilter {
}
optimize(): (RegexTag & OptimizedTag) | boolean {
return <any> this
return <any>this
}
isNegative(): boolean {

View file

@ -113,7 +113,7 @@ export default class SubstitutingTag extends TagsFilter {
}
optimize(): (SubstitutingTag & OptimizedTag) | boolean {
return <any> this
return <any>this
}
isNegative(): boolean {

View file

@ -67,7 +67,7 @@ export class Tag extends TagsFilter {
asOverpass(): string[] {
if (this.value === "") {
// NOT having this key
return ["[!\"" + this.key + "\"]"]
return ['[!"' + this.key + '"]']
}
return [`["${this.key}"="${this.value}"]`]
}
@ -141,7 +141,7 @@ export class Tag extends TagsFilter {
return other.key === this.key && other.value === this.value
}
if (other instanceof RegexTag) {
if(other.key === this.key || !other.invert){
if (other.key === this.key || !other.invert) {
return other.matchesProperties({ [this.key]: this.value })
}
}

View file

@ -13,7 +13,6 @@ type Brand<B> = { [__is_optimized]: B }
*/
export type OptimizedTag = Brand<TagsFilter>
export type UploadableTag = Tag | SubstitutingTag | And
/**
* Not nested
@ -21,15 +20,12 @@ export type UploadableTag = Tag | SubstitutingTag | And
export type FlatTag = Tag | RegexTag | SubstitutingTag | ComparingTag
export type TagsFilterClosed = FlatTag | And | Or
export class TagTypes {
static safeAnd(and: And & OptimizedTag): ((FlatTag | (Or & OptimizedTag)) & OptimizedTag)[]{
return <any> and.and
static safeAnd(and: And & OptimizedTag): ((FlatTag | (Or & OptimizedTag)) & OptimizedTag)[] {
return <any>and.and
}
static safeOr(or: Or & OptimizedTag): ((FlatTag | (And & OptimizedTag)) & OptimizedTag)[]{
return <any> or.or
static safeOr(or: Or & OptimizedTag): ((FlatTag | (And & OptimizedTag)) & OptimizedTag)[] {
return <any>or.or
}
}

View file

@ -484,7 +484,10 @@ export class TagUtils {
* regex.matchesProperties({maxspeed: "50 mph"}) // => true
*/
public static Tag(json: TagConfigJson, context: string | ConversionContext = ""): TagsFilterClosed {
public static Tag(
json: TagConfigJson,
context: string | ConversionContext = ""
): TagsFilterClosed {
try {
const ctx = typeof context === "string" ? context : context.path.join(".")
return this.ParseTagUnsafe(json, ctx)

View file

@ -151,9 +151,9 @@ class ImagesInLoadedDataFetcher implements ImageFetcher {
coordinates: { lng: centerpoint[0], lat: centerpoint[1] },
provider: "OpenStreetMap",
details: {
isSpherical: false
isSpherical: false,
},
osmTags: { image }
osmTags: { image },
})
}
})
@ -173,46 +173,64 @@ class ImagesFromCacheServerFetcher implements ImageFetcher {
}
async fetchImages(lat: number, lon: number): Promise<P4CPicture[]> {
return (await Promise.all([
this.fetchImagesForType(lat, lon, "lines"),
this.fetchImagesForType(lat, lon, "pois"),
this.fetchImagesForType(lat, lon, "polygons")
])).flatMap(x => x)
return (
await Promise.all([
this.fetchImagesForType(lat, lon, "lines"),
this.fetchImagesForType(lat, lon, "pois"),
this.fetchImagesForType(lat, lon, "polygons"),
])
).flatMap((x) => x)
}
async fetchImagesForType(targetlat: number, targetlon: number, type: "lines" | "pois" | "polygons"): Promise<P4CPicture[]> {
async fetchImagesForType(
targetlat: number,
targetlon: number,
type: "lines" | "pois" | "polygons"
): Promise<P4CPicture[]> {
const { x, y, z } = Tiles.embedded_tile(targetlat, targetlon, 14)
const url = this._serverUrl
async function getFeatures(x: number, y: number) {
const src = new MvtSource(Utils.SubstituteKeys(url, {
type, x, y, z, layer: "item_with_image"
}), x, y, z)
const src = new MvtSource(
Utils.SubstituteKeys(url, {
type,
x,
y,
z,
layer: "item_with_image",
}),
x,
y,
z
)
await src.updateAsync()
return src.features.data
}
const features = (await Promise.all([
getFeatures(x, y),
getFeatures(x, y + 1),
getFeatures(x, y - 1),
const features = (
await Promise.all([
getFeatures(x, y),
getFeatures(x, y + 1),
getFeatures(x, y - 1),
getFeatures(x + 1, y + 1),
getFeatures(x + 1, y),
getFeatures(x + 1, y - 1),
getFeatures(x + 1, y + 1),
getFeatures(x + 1, y),
getFeatures(x + 1, y - 1),
getFeatures(x - 1, y - 1),
getFeatures(x - 1, y),
getFeatures(x - 1, y + 1)
])).flatMap(x => x)
getFeatures(x - 1, y - 1),
getFeatures(x - 1, y),
getFeatures(x - 1, y + 1),
])
).flatMap((x) => x)
const pics: P4CPicture[] = []
for (const f of features) {
const [lng, lat] = GeoOperations.centerpointCoordinates(f)
if (GeoOperations.distanceBetween([targetlon, targetlat], [lng, lat]) > this._searchRadius) {
if (
GeoOperations.distanceBetween([targetlon, targetlat], [lng, lat]) >
this._searchRadius
) {
return []
}
for (let i = -1; i < 50; i++) {
@ -235,13 +253,13 @@ class ImagesFromCacheServerFetcher implements ImageFetcher {
pictureUrl: v,
coordinates: { lat, lng },
details: {
isSpherical: false
isSpherical: false,
},
osmTags: {
image: v
image: v,
},
thumbUrl: v,
provider
provider,
})
}
}
@ -299,12 +317,12 @@ class MapillaryFetcher implements ImageFetcher {
const response = await Utils.downloadJson<{
data: {
id: string,
creator: string,
computed_geometry: Point,
is_pano: boolean,
thumb_256_url: string,
thumb_original_url: string,
id: string
creator: string
computed_geometry: Point
is_pano: boolean
thumb_256_url: string
thumb_original_url: string
compass_angle: number
}[]
}>(url)
@ -323,8 +341,8 @@ class MapillaryFetcher implements ImageFetcher {
mapillary: img.id,
},
details: {
isSpherical: img.is_pano
}
isSpherical: img.is_pano,
},
})
}
return pics
@ -342,24 +360,28 @@ export class CombinedFetcher {
new ImagesInLoadedDataFetcher(indexedFeatures, radius),
new ImagesFromCacheServerFetcher(radius),
new MapillaryFetcher({
panoramas: "no",
max_images: 25,
start_captured_at : maxage
}),
new P4CImageFetcher("mapillary"),
new P4CImageFetcher("wikicommons"),
].map(f => new CachedFetcher(f))
panoramas: "no",
max_images: 25,
start_captured_at: maxage,
}),
new P4CImageFetcher("mapillary"),
new P4CImageFetcher("wikicommons"),
].map((f) => new CachedFetcher(f))
}
private async fetchImage(source: CachedFetcher, lat: number, lon: number, state: UIEventSource<Record<string, "loading" | "done" | "error">>, sink: UIEventSource<P4CPicture[]>): Promise<void> {
private async fetchImage(
source: CachedFetcher,
lat: number,
lon: number,
state: UIEventSource<Record<string, "loading" | "done" | "error">>,
sink: UIEventSource<P4CPicture[]>
): Promise<void> {
try {
const pics = await source.fetchImages(lat, lon)
console.log(source.name, "==>>", pics)
state.data[source.name] = "done"
state.ping()
if (sink.data === undefined) {
sink.setData(pics)
} else {
@ -383,8 +405,11 @@ export class CombinedFetcher {
}
}
public getImagesAround(lon: number, lat: number): {
images: Store<P4CPicture[]>,
public getImagesAround(
lon: number,
lat: number
): {
images: Store<P4CPicture[]>
state: Store<Record<string, "loading" | "done" | "error">>
} {
const sink = new UIEventSource<P4CPicture[]>([])