refactoring: more state splitting, basic layoutFeatureSource

This commit is contained in:
Pieter Vander Vennet 2023-03-26 05:58:28 +02:00
parent 8e2f04c0d0
commit b94a8f5745
54 changed files with 1067 additions and 1969 deletions

View file

@ -6,19 +6,18 @@ import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescr
import { Utils } from "../../Utils"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import SimpleMetaTagger from "../SimpleMetaTagger"
import FeatureSource from "../FeatureSource/FeatureSource"
import { ElementStorage } from "../ElementStorage"
import FeatureSource, { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
import { GeoLocationPointProperties } from "../State/GeoLocationState"
import { GeoOperations } from "../GeoOperations"
import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler"
import { OsmConnection } from "./OsmConnection"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
/**
* Handles all changes made to OSM.
* Needs an authenticator via OsmConnection
*/
export class Changes {
public readonly name = "Newly added features"
/**
* All the newly created features as featureSource + all the modified features
*/
@ -26,7 +25,7 @@ export class Changes {
public readonly pendingChanges: UIEventSource<ChangeDescription[]> =
LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
public readonly state: { allElements: ElementStorage; osmConnection: OsmConnection }
public readonly state: { allElements: IndexedFeatureSource; osmConnection: OsmConnection }
public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined)
private readonly historicalUserLocations: FeatureSource
@ -38,7 +37,9 @@ export class Changes {
constructor(
state?: {
allElements: ElementStorage
dryRun: UIEventSource<boolean>
allElements: IndexedFeatureSource
featurePropertiesStore: FeaturePropertiesStore
osmConnection: OsmConnection
historicalUserLocations: FeatureSource
},
@ -50,8 +51,10 @@ export class Changes {
// If a pending change contains a negative ID, we save that
this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id) ?? []))
this.state = state
this._changesetHandler = state?.osmConnection?.CreateChangesetHandler(
state.allElements,
this._changesetHandler = new ChangesetHandler(
state.dryRun,
state.osmConnection,
state.featurePropertiesStore,
this
)
this.historicalUserLocations = state.historicalUserLocations
@ -187,7 +190,7 @@ export class Changes {
const changedObjectCoordinates: [number, number][] = []
const feature = this.state.allElements.ContainingFeatures.get(change.mainObjectId)
const feature = this.state.allElements.featuresById.data.get(change.mainObjectId)
if (feature !== undefined) {
changedObjectCoordinates.push(GeoOperations.centerpointCoordinates(feature))
}

View file

@ -1,7 +1,6 @@
import escapeHtml from "escape-html"
import UserDetails, { OsmConnection } from "./OsmConnection"
import { UIEventSource } from "../UIEventSource"
import { ElementStorage } from "../ElementStorage"
import Locale from "../../UI/i18n/Locale"
import Constants from "../../Models/Constants"
import { Changes } from "./Changes"
@ -14,12 +13,11 @@ export interface ChangesetTag {
}
export class ChangesetHandler {
private readonly allElements: ElementStorage
private readonly allElements: { addAlias: (id0: String, id1: string) => void }
private osmConnection: OsmConnection
private readonly changes: Changes
private readonly _dryRun: UIEventSource<boolean>
private readonly userDetails: UIEventSource<UserDetails>
private readonly auth: any
private readonly backend: string
/**
@ -28,20 +26,11 @@ export class ChangesetHandler {
*/
private readonly _remappings = new Map<string, string>()
/**
* Use 'osmConnection.CreateChangesetHandler' instead
* @param dryRun
* @param osmConnection
* @param allElements
* @param changes
* @param auth
*/
constructor(
dryRun: UIEventSource<boolean>,
osmConnection: OsmConnection,
allElements: ElementStorage,
changes: Changes,
auth
allElements: { addAlias: (id0: String, id1: string) => void },
changes: Changes
) {
this.osmConnection = osmConnection
this.allElements = allElements
@ -49,7 +38,6 @@ export class ChangesetHandler {
this._dryRun = dryRun
this.userDetails = osmConnection.userDetails
this.backend = osmConnection._oauth_config.url
this.auth = auth
if (dryRun) {
console.log("DRYRUN ENABLED")
@ -61,7 +49,7 @@ export class ChangesetHandler {
*
* ChangesetHandler.removeDuplicateMetaTags([{key: "k", value: "v"}, {key: "k0", value: "v0"}, {key: "k", value:"v"}] // => [{key: "k", value: "v"}, {key: "k0", value: "v0"}]
*/
public static removeDuplicateMetaTags(extraMetaTags: ChangesetTag[]): ChangesetTag[] {
private static removeDuplicateMetaTags(extraMetaTags: ChangesetTag[]): ChangesetTag[] {
const r: ChangesetTag[] = []
const seen = new Set<string>()
for (const extraMetaTag of extraMetaTags) {
@ -82,7 +70,7 @@ export class ChangesetHandler {
* @param rewriteIds
* @private
*/
static rewriteMetaTags(extraMetaTags: ChangesetTag[], rewriteIds: Map<string, string>) {
private static rewriteMetaTags(extraMetaTags: ChangesetTag[], rewriteIds: Map<string, string>) {
let hasChange = false
for (const tag of extraMetaTags) {
const match = tag.key.match(/^([a-zA-Z0-9_]+):(node\/-[0-9])$/)
@ -198,7 +186,7 @@ export class ChangesetHandler {
* @param rewriteIds: the mapping of ids
* @param oldChangesetMeta: the metadata-object of the already existing changeset
*/
public RewriteTagsOf(
private RewriteTagsOf(
extraMetaTags: ChangesetTag[],
rewriteIds: Map<string, string>,
oldChangesetMeta: {
@ -318,28 +306,14 @@ export class ChangesetHandler {
}
private async CloseChangeset(changesetId: number = undefined): Promise<void> {
const self = this
return new Promise<void>(function (resolve, reject) {
if (changesetId === undefined) {
return
}
self.auth.xhr(
{
method: "PUT",
path: "/api/0.6/changeset/" + changesetId + "/close",
},
function (err, response) {
if (response == null) {
console.log("err", err)
}
console.log("Closed changeset ", changesetId)
resolve()
}
)
})
if (changesetId === undefined) {
return
}
await this.osmConnection.put("changeset/" + changesetId + "/close")
console.log("Closed changeset ", changesetId)
}
async GetChangesetMeta(csId: number): Promise<{
private async GetChangesetMeta(csId: number): Promise<{
id: number
open: boolean
uid: number
@ -358,34 +332,16 @@ export class ChangesetHandler {
private async UpdateTags(csId: number, tags: ChangesetTag[]) {
tags = ChangesetHandler.removeDuplicateMetaTags(tags)
const self = this
return new Promise<string>(function (resolve, reject) {
tags = Utils.NoNull(tags).filter(
(tag) =>
tag.key !== undefined &&
tag.value !== undefined &&
tag.key !== "" &&
tag.value !== ""
)
const metadata = tags.map((kv) => `<tag k="${kv.key}" v="${escapeHtml(kv.value)}"/>`)
self.auth.xhr(
{
method: "PUT",
path: "/api/0.6/changeset/" + csId,
options: { header: { "Content-Type": "text/xml" } },
content: [`<osm><changeset>`, metadata, `</changeset></osm>`].join(""),
},
function (err, response) {
if (response === undefined) {
console.error("Updating the tags of changeset " + csId + " failed:", err)
reject(err)
} else {
resolve(response)
}
}
)
})
tags = Utils.NoNull(tags).filter(
(tag) =>
tag.key !== undefined &&
tag.value !== undefined &&
tag.key !== "" &&
tag.value !== ""
)
const metadata = tags.map((kv) => `<tag k="${kv.key}" v="${escapeHtml(kv.value)}"/>`)
const content = [`<osm><changeset>`, metadata, `</changeset></osm>`].join("")
return this.osmConnection.put("changeset/" + csId, content, { "Content-Type": "text/xml" })
}
private defaultChangesetTags(): ChangesetTag[] {
@ -413,57 +369,35 @@ export class ChangesetHandler {
* @constructor
* @private
*/
private OpenChangeset(changesetTags: ChangesetTag[]): Promise<number> {
const self = this
return new Promise<number>(function (resolve, reject) {
const metadata = changesetTags
.map((cstag) => [cstag.key, cstag.value])
.filter((kv) => (kv[1] ?? "") !== "")
.map((kv) => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`)
.join("\n")
private async OpenChangeset(changesetTags: ChangesetTag[]): Promise<number> {
const metadata = changesetTags
.map((cstag) => [cstag.key, cstag.value])
.filter((kv) => (kv[1] ?? "") !== "")
.map((kv) => `<tag k="${kv[0]}" v="${escapeHtml(kv[1])}"/>`)
.join("\n")
self.auth.xhr(
{
method: "PUT",
path: "/api/0.6/changeset/create",
options: { header: { "Content-Type": "text/xml" } },
content: [`<osm><changeset>`, metadata, `</changeset></osm>`].join(""),
},
function (err, response) {
if (response === undefined) {
console.error("Opening a changeset failed:", err)
reject(err)
} else {
resolve(Number(response))
}
}
)
})
const csId = await this.osmConnection.put(
"changeset/create",
[`<osm><changeset>`, metadata, `</changeset></osm>`].join(""),
{ "Content-Type": "text/xml" }
)
return Number(csId)
}
/**
* Upload a changesetXML
*/
private UploadChange(changesetId: number, changesetXML: string): Promise<Map<string, string>> {
const self = this
return new Promise(function (resolve, reject) {
self.auth.xhr(
{
method: "POST",
options: { header: { "Content-Type": "text/xml" } },
path: "/api/0.6/changeset/" + changesetId + "/upload",
content: changesetXML,
},
function (err, response) {
if (response == null) {
console.error("Uploading an actual change failed", err)
reject(err)
}
const changes = self.parseUploadChangesetResponse(response)
console.log("Uploaded changeset ", changesetId)
resolve(changes)
}
)
})
private async UploadChange(
changesetId: number,
changesetXML: string
): Promise<Map<string, string>> {
const response = await this.osmConnection.post(
"changeset/" + changesetId + "/upload",
changesetXML,
{ "Content-Type": "text/xml" }
)
const changes = this.parseUploadChangesetResponse(response)
console.log("Uploaded changeset ", changesetId)
return changes
}
}

View file

@ -1,13 +1,8 @@
import osmAuth from "osm-auth"
import { Store, Stores, UIEventSource } from "../UIEventSource"
import { OsmPreferences } from "./OsmPreferences"
import { ChangesetHandler } from "./ChangesetHandler"
import { ElementStorage } from "../ElementStorage"
import Svg from "../../Svg"
import Img from "../../UI/Base/Img"
import { Utils } from "../../Utils"
import { OsmObject } from "./OsmObject"
import { Changes } from "./Changes"
export default class UserDetails {
public loggedIn = false
@ -148,16 +143,6 @@ export class OsmConnection {
}
}
public CreateChangesetHandler(allElements: ElementStorage, changes: Changes) {
return new ChangesetHandler(
this._dryRun,
<any>/*casting is needed to make the tests work*/ this,
allElements,
changes,
this.auth
)
}
public GetPreference(
key: string,
defaultValue: string = undefined,
@ -288,6 +273,57 @@ export class OsmConnection {
)
}
/**
* Interact with the API.
*
* @param path: the path to query, without host and without '/api/0.6'. Example 'notes/1234/close'
*/
public async interact(
path: string,
method: "GET" | "POST" | "PUT" | "DELETE",
header?: Record<string, string | number>,
content?: string
): Promise<any> {
return new Promise((ok, error) => {
this.auth.xhr(
{
method,
options: {
header,
},
content,
path: `/api/0.6/${path}`,
},
function (err, response) {
if (err !== null) {
error(err)
} else {
ok(response)
}
}
)
})
}
public async post(
path: string,
content?: string,
header?: Record<string, string | number>
): Promise<any> {
return await this.interact(path, "POST", header, content)
}
public async put(
path: string,
content?: string,
header?: Record<string, string | number>
): Promise<any> {
return await this.interact(path, "PUT", header, content)
}
public async get(path: string, header?: Record<string, string | number>): Promise<any> {
return await this.interact(path, "GET", header)
}
public closeNote(id: number | string, text?: string): Promise<void> {
let textSuffix = ""
if ((text ?? "") !== "") {
@ -299,21 +335,7 @@ export class OsmConnection {
ok()
})
}
return new Promise((ok, error) => {
this.auth.xhr(
{
method: "POST",
path: `/api/0.6/notes/${id}/close${textSuffix}`,
},
function (err, _) {
if (err !== null) {
error(err)
} else {
ok()
}
}
)
})
return this.post(`notes/${id}/close${textSuffix}`)
}
public reopenNote(id: number | string, text?: string): Promise<void> {
@ -327,24 +349,10 @@ export class OsmConnection {
if ((text ?? "") !== "") {
textSuffix = "?text=" + encodeURIComponent(text)
}
return new Promise((ok, error) => {
this.auth.xhr(
{
method: "POST",
path: `/api/0.6/notes/${id}/reopen${textSuffix}`,
},
function (err, _) {
if (err !== null) {
error(err)
} else {
ok()
}
}
)
})
return this.post(`notes/${id}/reopen${textSuffix}`)
}
public openNote(lat: number, lon: number, text: string): Promise<{ id: number }> {
public async openNote(lat: number, lon: number, text: string): Promise<{ id: number }> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually opening note with text ", text)
return new Promise<{ id: number }>((ok) => {
@ -356,29 +364,13 @@ export class OsmConnection {
}
const auth = this.auth
const content = { lat, lon, text }
return new Promise((ok, error) => {
auth.xhr(
{
method: "POST",
path: `/api/0.6/notes.json`,
options: {
header: { "Content-Type": "application/json" },
},
content: JSON.stringify(content),
},
function (err, response: string) {
console.log("RESPONSE IS", response)
if (err !== null) {
error(err)
} else {
const parsed = JSON.parse(response)
const id = parsed.properties.id
console.log("OPENED NOTE", id)
ok({ id })
}
}
)
const response = await this.post("notes.json", JSON.stringify(content), {
"Content-Type": "application/json",
})
const parsed = JSON.parse(response)
const id = parsed.properties.id
console.log("OPENED NOTE", id)
return id
}
public async uploadGpxTrack(
@ -434,31 +426,13 @@ export class OsmConnection {
}
body += "--" + boundary + "--\r\n"
return new Promise((ok, error) => {
auth.xhr(
{
method: "POST",
path: `/api/0.6/gpx/create`,
options: {
header: {
"Content-Type": "multipart/form-data; boundary=" + boundary,
"Content-Length": body.length,
},
},
content: body,
},
function (err, response: string) {
console.log("RESPONSE IS", response)
if (err !== null) {
error(err)
} else {
const parsed = JSON.parse(response)
console.log("Uploaded GPX track", parsed)
ok({ id: parsed })
}
}
)
const response = await this.post("gpx/create", body, {
"Content-Type": "multipart/form-data; boundary=" + boundary,
"Content-Length": body.length,
})
const parsed = JSON.parse(response)
console.log("Uploaded GPX track", parsed)
return { id: parsed }
}
public addCommentToNote(id: number | string, text: string): Promise<void> {

View file

@ -1,5 +1,4 @@
import { TagsFilter } from "../Tags/TagsFilter"
import RelationsTracker from "./RelationsTracker"
import { Utils } from "../../Utils"
import { ImmutableStore, Store } from "../UIEventSource"
import { BBox } from "../BBox"
@ -15,14 +14,12 @@ export class Overpass {
private readonly _timeout: Store<number>
private readonly _extraScripts: string[]
private readonly _includeMeta: boolean
private _relationTracker: RelationsTracker
constructor(
filter: TagsFilter,
extraScripts: string[],
interpreterUrl: string,
timeout?: Store<number>,
relationTracker?: RelationsTracker,
includeMeta = true
) {
this._timeout = timeout ?? new ImmutableStore<number>(90)
@ -34,7 +31,6 @@ export class Overpass {
this._filter = optimized
this._extraScripts = extraScripts
this._includeMeta = includeMeta
this._relationTracker = relationTracker
}
public async queryGeoJson(bounds: BBox): Promise<[FeatureCollection, Date]> {
@ -57,7 +53,6 @@ export class Overpass {
}
public async ExecuteQuery(query: string): Promise<[FeatureCollection, Date]> {
const self = this
const json = await Utils.downloadJson(this.buildUrl(query))
if (json.elements.length === 0 && json.remark !== undefined) {
@ -68,7 +63,6 @@ export class Overpass {
console.warn("No features for", json)
}
self._relationTracker?.RegisterRelations(json)
const geojson = osmtogeojson(json)
const osmTime = new Date(json.osm3s.timestamp_osm_base)
return [<any>geojson, osmTime]
@ -104,7 +98,6 @@ export class Overpass {
/**
* Constructs the actual script to execute on Overpass with geocoding
* 'PostCall' can be used to set an extra range, see 'AsOverpassTurboLink'
*
*/
public buildScriptInArea(
area: { osm_type: "way" | "relation"; osm_id: number },
@ -142,7 +135,7 @@ export class Overpass {
* Little helper method to quickly open overpass-turbo in the browser
*/
public static AsOverpassTurboLink(tags: TagsFilter) {
const overpass = new Overpass(tags, [], "", undefined, undefined, false)
const overpass = new Overpass(tags, [], "", undefined, false)
const script = overpass.buildScript("", "({{bbox}})", true)
const url = "http://overpass-turbo.eu/?Q="
return url + encodeURIComponent(script)

View file

@ -1,76 +0,0 @@
import { UIEventSource } from "../UIEventSource"
export interface Relation {
id: number
type: "relation"
members: {
type: "way" | "node" | "relation"
ref: number
role: string
}[]
tags: any
// Alias for tags; tags == properties
properties: any
}
export default class RelationsTracker {
public knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>(
new Map(),
"Relation memberships"
)
constructor() {}
/**
* Gets an overview of the relations - except for multipolygons. We don't care about those
* @param overpassJson
* @constructor
*/
private static GetRelationElements(overpassJson: any): Relation[] {
const relations = overpassJson.elements.filter(
(element) => element.type === "relation" && element.tags.type !== "multipolygon"
)
for (const relation of relations) {
relation.properties = relation.tags
}
return relations
}
public RegisterRelations(overpassJson: any): void {
this.UpdateMembershipTable(RelationsTracker.GetRelationElements(overpassJson))
}
/**
* Build a mapping of {memberId --> {role in relation, id of relation} }
* @param relations
* @constructor
*/
private UpdateMembershipTable(relations: Relation[]): void {
const memberships = this.knownRelations.data
let changed = false
for (const relation of relations) {
for (const member of relation.members) {
const role = {
role: member.role,
relation: relation,
}
const key = member.type + "/" + member.ref
if (!memberships.has(key)) {
memberships.set(key, [])
}
const knownRelations = memberships.get(key)
const alreadyExists = knownRelations.some((knownRole) => {
return knownRole.role === role.role && knownRole.relation === role.relation
})
if (!alreadyExists) {
knownRelations.push(role)
changed = true
}
}
}
if (changed) {
this.knownRelations.ping()
}
}
}