forked from MapComplete/MapComplete
Feature: stabilize saved history, add code to cleanup old preferences, make loading preferences faster (which prevents a 'hang') when just logged in
This commit is contained in:
parent
dc1e582664
commit
c1d3f35d30
10 changed files with 249 additions and 146 deletions
|
@ -1,19 +1,18 @@
|
||||||
import { QueryParameters } from "../Web/QueryParameters"
|
import { QueryParameters } from "../Web/QueryParameters"
|
||||||
import { BBox } from "../BBox"
|
import { BBox } from "../BBox"
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
import { GeoLocationState } from "../State/GeoLocationState"
|
import { GeoLocationPointProperties, GeoLocationState } from "../State/GeoLocationState"
|
||||||
import { Store, UIEventSource } from "../UIEventSource"
|
import { Store, UIEventSource } from "../UIEventSource"
|
||||||
import { Feature, LineString, Point } from "geojson"
|
import { Feature, LineString, Point } from "geojson"
|
||||||
import { FeatureSource, WritableFeatureSource } from "../FeatureSource/FeatureSource"
|
import { FeatureSource, WritableFeatureSource } from "../FeatureSource/FeatureSource"
|
||||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||||
import { GeoOperations } from "../GeoOperations"
|
import { GeoOperations } from "../GeoOperations"
|
||||||
import { OsmTags } from "../../Models/OsmFeature"
|
import { OsmTags } from "../../Models/OsmFeature"
|
||||||
import StaticFeatureSource, {
|
import StaticFeatureSource, { WritableStaticFeatureSource } from "../FeatureSource/Sources/StaticFeatureSource"
|
||||||
WritableStaticFeatureSource,
|
|
||||||
} from "../FeatureSource/Sources/StaticFeatureSource"
|
|
||||||
import { MapProperties } from "../../Models/MapProperties"
|
import { MapProperties } from "../../Models/MapProperties"
|
||||||
import { Orientation } from "../../Sensors/Orientation"
|
import { Orientation } from "../../Sensors/Orientation"
|
||||||
;("use strict")
|
|
||||||
|
("use strict")
|
||||||
/**
|
/**
|
||||||
* The geolocation-handler takes a map-location and a geolocation state.
|
* 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
|
* It'll move the map as appropriate given the state of the geolocation-API
|
||||||
|
@ -25,12 +24,12 @@ export default class GeoLocationHandler {
|
||||||
/**
|
/**
|
||||||
* The location as delivered by the GPS, wrapped as FeatureSource
|
* The location as delivered by the GPS, wrapped as FeatureSource
|
||||||
*/
|
*/
|
||||||
public currentUserLocation: FeatureSource
|
public currentUserLocation: FeatureSource<Feature<Point, GeoLocationPointProperties>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All previously visited points (as 'Point'-objects), with their metadata
|
* All previously visited points (as 'Point'-objects), with their metadata
|
||||||
*/
|
*/
|
||||||
public historicalUserLocations: WritableFeatureSource<Feature<Point>>
|
public historicalUserLocations: WritableFeatureSource<Feature<Point, GeoLocationPointProperties>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A featureSource containing a single linestring which has the GPS-history of the user.
|
* A featureSource containing a single linestring which has the GPS-history of the user.
|
||||||
|
@ -151,27 +150,29 @@ export default class GeoLocationHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private CopyGeolocationIntoMapstate() {
|
private CopyGeolocationIntoMapstate() {
|
||||||
const features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([])
|
const features: UIEventSource<Feature<Point, GeoLocationPointProperties>[]> = new UIEventSource<Feature<Point, GeoLocationPointProperties>[]>([])
|
||||||
this.currentUserLocation = new StaticFeatureSource(features)
|
this.currentUserLocation = new StaticFeatureSource(features)
|
||||||
let i = 0
|
let i = 0
|
||||||
this.geolocationState.currentGPSLocation.addCallbackAndRunD((location) => {
|
this.geolocationState.currentGPSLocation.addCallbackAndRunD((location) => {
|
||||||
const properties = {
|
const properties: GeoLocationPointProperties = {
|
||||||
id: "gps-" + i,
|
id: "gps-" + i,
|
||||||
"user:location": "yes",
|
"user:location": "yes",
|
||||||
date: new Date().toISOString(),
|
date: new Date().toISOString(),
|
||||||
// GeolocationObject behaves really weird when indexing, so copying it one by one is the most stable
|
// GeolocationObject behaves really weird when indexing, so copying it one by one is the most stable
|
||||||
accuracy: location.accuracy,
|
accuracy: location.accuracy,
|
||||||
speed: location.speed,
|
speed: location.speed,
|
||||||
|
latitude: location.latitude,
|
||||||
|
longitude: location.longitude,
|
||||||
altitude: location.altitude,
|
altitude: location.altitude,
|
||||||
altitudeAccuracy: location.altitudeAccuracy,
|
altitudeAccuracy: location.altitudeAccuracy,
|
||||||
heading: location.heading,
|
heading: location.heading,
|
||||||
alpha: Orientation.singleton.gotMeasurement.data
|
alpha: Orientation.singleton.gotMeasurement.data
|
||||||
? "" + Orientation.singleton.alpha.data
|
? ("" + Orientation.singleton.alpha.data)
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
|
|
||||||
const feature = <Feature>{
|
const feature = <Feature<Point, GeoLocationPointProperties>>{
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties,
|
properties,
|
||||||
geometry: {
|
geometry: {
|
||||||
|
@ -184,7 +185,7 @@ export default class GeoLocationHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private initUserLocationTrail() {
|
private initUserLocationTrail() {
|
||||||
const features = LocalStorageSource.getParsed<Feature<Point>[]>("gps_location_history", [])
|
const features = LocalStorageSource.getParsed<Feature<Point, GeoLocationPointProperties>[]>("gps_location_history", [])
|
||||||
const now = new Date().getTime()
|
const now = new Date().getTime()
|
||||||
features.data = features.data.filter((ff) => {
|
features.data = features.data.filter((ff) => {
|
||||||
if (ff.properties === undefined) {
|
if (ff.properties === undefined) {
|
||||||
|
@ -197,7 +198,7 @@ export default class GeoLocationHandler {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
features.ping()
|
features.ping()
|
||||||
this.currentUserLocation?.features?.addCallbackAndRunD(([location]: [Feature<Point>]) => {
|
this.currentUserLocation?.features?.addCallbackAndRunD(([location]: [Feature<Point, GeoLocationPointProperties>]) => {
|
||||||
if (location === undefined) {
|
if (location === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -231,7 +232,7 @@ export default class GeoLocationHandler {
|
||||||
features.ping()
|
features.ping()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.historicalUserLocations = new WritableStaticFeatureSource<Feature<Point>>(features)
|
this.historicalUserLocations = new WritableStaticFeatureSource<Feature<Point, GeoLocationPointProperties>>(features)
|
||||||
|
|
||||||
const asLine = features.map((allPoints) => {
|
const asLine = features.map((allPoints) => {
|
||||||
if (allPoints === undefined || allPoints.length < 2) {
|
if (allPoints === undefined || allPoints.length < 2) {
|
||||||
|
|
|
@ -27,8 +27,7 @@ export default class PendingChangesUploader {
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("mouseout", (e) => {
|
document.addEventListener("mouseout", (e) => {
|
||||||
// @ts-ignore
|
if (!e["toElement"] && !e.relatedTarget) {
|
||||||
if (!e.toElement && !e.relatedTarget) {
|
|
||||||
changes.flushChanges("Flushing changes due to focus lost")
|
changes.flushChanges("Flushing changes due to focus lost")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,6 +17,7 @@ import ChangeTagAction from "./Actions/ChangeTagAction"
|
||||||
import DeleteAction from "./Actions/DeleteAction"
|
import DeleteAction from "./Actions/DeleteAction"
|
||||||
import MarkdownUtils from "../../Utils/MarkdownUtils"
|
import MarkdownUtils from "../../Utils/MarkdownUtils"
|
||||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
||||||
|
import { Feature, Point } from "geojson"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles all changes made to OSM.
|
* Handles all changes made to OSM.
|
||||||
|
@ -37,7 +38,7 @@ export class Changes {
|
||||||
public readonly backend: string
|
public readonly backend: string
|
||||||
public readonly isUploading = new UIEventSource(false)
|
public readonly isUploading = new UIEventSource(false)
|
||||||
public readonly errors = new UIEventSource<string[]>([], "upload-errors")
|
public readonly errors = new UIEventSource<string[]>([], "upload-errors")
|
||||||
private readonly historicalUserLocations?: FeatureSource
|
private readonly historicalUserLocations?: FeatureSource<Feature<Point, GeoLocationPointProperties>>
|
||||||
private _nextId: number = 0 // Newly assigned ID's are negative
|
private _nextId: number = 0 // Newly assigned ID's are negative
|
||||||
private readonly previouslyCreated: OsmObject[] = []
|
private readonly previouslyCreated: OsmObject[] = []
|
||||||
private readonly _leftRightSensitive: boolean
|
private readonly _leftRightSensitive: boolean
|
||||||
|
@ -53,7 +54,7 @@ export class Changes {
|
||||||
osmConnection: OsmConnection
|
osmConnection: OsmConnection
|
||||||
reportError?: (error: string) => void
|
reportError?: (error: string) => void
|
||||||
featureProperties?: FeaturePropertiesStore
|
featureProperties?: FeaturePropertiesStore
|
||||||
historicalUserLocations?: FeatureSource
|
historicalUserLocations?: FeatureSource<Feature<Point, GeoLocationPointProperties>>
|
||||||
allElements?: IndexedFeatureSource
|
allElements?: IndexedFeatureSource
|
||||||
},
|
},
|
||||||
leftRightSensitive: boolean = false
|
leftRightSensitive: boolean = false
|
||||||
|
@ -66,7 +67,7 @@ export class Changes {
|
||||||
if (isNaN(this._nextId) && state.reportError !== undefined) {
|
if (isNaN(this._nextId) && state.reportError !== undefined) {
|
||||||
state.reportError(
|
state.reportError(
|
||||||
"Got a NaN as nextID. Pending changes IDs are:" +
|
"Got a NaN as nextID. Pending changes IDs are:" +
|
||||||
this.pendingChanges.data?.map((pch) => pch?.id).join(".")
|
this.pendingChanges.data?.map((pch) => pch?.id).join(".")
|
||||||
)
|
)
|
||||||
this._nextId = -100
|
this._nextId = -100
|
||||||
}
|
}
|
||||||
|
@ -90,8 +91,8 @@ export class Changes {
|
||||||
return new Changes({
|
return new Changes({
|
||||||
osmConnection: new OsmConnection(),
|
osmConnection: new OsmConnection(),
|
||||||
featureSwitches: {
|
featureSwitches: {
|
||||||
featureSwitchIsTesting: new ImmutableStore(true),
|
featureSwitchIsTesting: new ImmutableStore(true)
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,50 +179,50 @@ export class Changes {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: "comment",
|
key: "comment",
|
||||||
docs: "The changeset comment. Will be a fixed string, mentioning the theme",
|
docs: "The changeset comment. Will be a fixed string, mentioning the theme"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "theme",
|
key: "theme",
|
||||||
docs: "The name of the theme that was used to create this change. ",
|
docs: "The name of the theme that was used to create this change. "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "source",
|
key: "source",
|
||||||
value: "survey",
|
value: "survey",
|
||||||
docs: "The contributor had their geolocation enabled while making changes",
|
docs: "The contributor had their geolocation enabled while making changes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "change_within_{distance}",
|
key: "change_within_{distance}",
|
||||||
docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. This gives an indication of proximity and if they truly surveyed or were armchair-mapping",
|
docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. This gives an indication of proximity and if they truly surveyed or were armchair-mapping"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "change_over_{distance}",
|
key: "change_over_{distance}",
|
||||||
docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. If they were over 5000m away, the might have been armchair-mapping",
|
docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. If they were over 5000m away, the might have been armchair-mapping"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "created_by",
|
key: "created_by",
|
||||||
value: "MapComplete <version>",
|
value: "MapComplete <version>",
|
||||||
docs: "The piece of software used to create this changeset; will always start with MapComplete, followed by the version number",
|
docs: "The piece of software used to create this changeset; will always start with MapComplete, followed by the version number"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "locale",
|
key: "locale",
|
||||||
value: "en|nl|de|...",
|
value: "en|nl|de|...",
|
||||||
docs: "The code of the language that the contributor used MapComplete in. Hints what language the user speaks.",
|
docs: "The code of the language that the contributor used MapComplete in. Hints what language the user speaks."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "host",
|
key: "host",
|
||||||
value: "https://mapcomplete.org/<theme>",
|
value: "https://mapcomplete.org/<theme>",
|
||||||
docs: "The URL that the contributor used to make changes. One can see the used instance with this",
|
docs: "The URL that the contributor used to make changes. One can see the used instance with this"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "imagery",
|
key: "imagery",
|
||||||
docs: "The identifier of the used background layer, this will probably be an identifier from the [editor layer index](https://github.com/osmlab/editor-layer-index)",
|
docs: "The identifier of the used background layer, this will probably be an identifier from the [editor layer index](https://github.com/osmlab/editor-layer-index)"
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
"default"
|
"default"
|
||||||
),
|
),
|
||||||
...addSource(ChangeTagAction.metatags, "ChangeTag"),
|
...addSource(ChangeTagAction.metatags, "ChangeTag"),
|
||||||
...addSource(ChangeLocationAction.metatags, "ChangeLocation"),
|
...addSource(ChangeLocationAction.metatags, "ChangeLocation"),
|
||||||
...addSource(DeleteAction.metatags, "DeleteAction"),
|
...addSource(DeleteAction.metatags, "DeleteAction")
|
||||||
// TODO
|
// TODO
|
||||||
/*
|
/*
|
||||||
...DeleteAction.metatags,
|
...DeleteAction.metatags,
|
||||||
|
@ -243,11 +244,11 @@ export class Changes {
|
||||||
docs,
|
docs,
|
||||||
specialMotivation
|
specialMotivation
|
||||||
? "This might give a reason per modified node or way"
|
? "This might give a reason per modified node or way"
|
||||||
: "",
|
: ""
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
source,
|
source
|
||||||
])
|
])
|
||||||
),
|
)
|
||||||
].join("\n\n")
|
].join("\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +267,7 @@ export class Changes {
|
||||||
this._changesetHandler._remappings.has("node/" + this._nextId) ||
|
this._changesetHandler._remappings.has("node/" + this._nextId) ||
|
||||||
this._changesetHandler._remappings.has("way/" + this._nextId) ||
|
this._changesetHandler._remappings.has("way/" + this._nextId) ||
|
||||||
this._changesetHandler._remappings.has("relation/" + this._nextId)
|
this._changesetHandler._remappings.has("relation/" + this._nextId)
|
||||||
)
|
)
|
||||||
return this._nextId
|
return this._nextId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,6 +334,7 @@ export class Changes {
|
||||||
this.previouslyCreated
|
this.previouslyCreated
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createChangesetObjectsStatic(
|
public static createChangesetObjectsStatic(
|
||||||
changes: ChangeDescription[],
|
changes: ChangeDescription[],
|
||||||
downloadedOsmObjects: OsmObject[],
|
downloadedOsmObjects: OsmObject[],
|
||||||
|
@ -502,7 +504,7 @@ export class Changes {
|
||||||
const result = {
|
const result = {
|
||||||
newObjects: [],
|
newObjects: [],
|
||||||
modifiedObjects: [],
|
modifiedObjects: [],
|
||||||
deletedObjects: [],
|
deletedObjects: []
|
||||||
}
|
}
|
||||||
|
|
||||||
objects.forEach((v, id) => {
|
objects.forEach((v, id) => {
|
||||||
|
@ -559,9 +561,7 @@ export class Changes {
|
||||||
const recentLocationPoints = locations
|
const recentLocationPoints = locations
|
||||||
.filter((feat) => feat.geometry.type === "Point")
|
.filter((feat) => feat.geometry.type === "Point")
|
||||||
.filter((feat) => {
|
.filter((feat) => {
|
||||||
const visitTime = new Date(
|
const visitTime = new Date(feat.properties.date)
|
||||||
(<GeoLocationPointProperties>(<any>feat.properties)).date
|
|
||||||
)
|
|
||||||
// In seconds
|
// In seconds
|
||||||
const diff = (now.getTime() - visitTime.getTime()) / 1000
|
const diff = (now.getTime() - visitTime.getTime()) / 1000
|
||||||
return diff < Constants.nearbyVisitTime
|
return diff < Constants.nearbyVisitTime
|
||||||
|
@ -665,7 +665,7 @@ export class Changes {
|
||||||
} else {
|
} else {
|
||||||
this._reportError(
|
this._reportError(
|
||||||
`Got an orphaned change. The 'creation'-change description for ${c.type}/${c.id} got lost. Permanently dropping this change:` +
|
`Got an orphaned change. The 'creation'-change description for ${c.type}/${c.id} got lost. Permanently dropping this change:` +
|
||||||
JSON.stringify(c)
|
JSON.stringify(c)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -676,10 +676,10 @@ export class Changes {
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
"Refusing change about " +
|
"Refusing change about " +
|
||||||
c.type +
|
c.type +
|
||||||
"/" +
|
"/" +
|
||||||
c.id +
|
c.id +
|
||||||
" as not in the objects. No internet?"
|
" as not in the objects. No internet?"
|
||||||
)
|
)
|
||||||
refused.push(c)
|
refused.push(c)
|
||||||
}
|
}
|
||||||
|
@ -694,7 +694,7 @@ export class Changes {
|
||||||
*/
|
*/
|
||||||
private async flushSelectChanges(
|
private async flushSelectChanges(
|
||||||
pending: ChangeDescription[],
|
pending: ChangeDescription[],
|
||||||
openChangeset: UIEventSource<number>
|
openChangeset: UIEventSource<{ id: number, opened: number }>
|
||||||
): Promise<ChangeDescription[]> {
|
): Promise<ChangeDescription[]> {
|
||||||
const neededIds = Changes.GetNeededIds(pending)
|
const neededIds = Changes.GetNeededIds(pending)
|
||||||
/* Download the latest version of the OSM-objects
|
/* Download the latest version of the OSM-objects
|
||||||
|
@ -775,14 +775,14 @@ export class Changes {
|
||||||
([key, count]) => ({
|
([key, count]) => ({
|
||||||
key: key,
|
key: key,
|
||||||
value: count,
|
value: count,
|
||||||
aggregate: true,
|
aggregate: true
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const motivations = pending
|
const motivations = pending
|
||||||
.filter((descr) => descr.meta.specialMotivation !== undefined)
|
.filter((descr) => descr.meta.specialMotivation !== undefined)
|
||||||
.map((descr) => ({
|
.map((descr) => ({
|
||||||
key: descr.meta.changeType + ":" + descr.type + "/" + descr.id,
|
key: descr.meta.changeType + ":" + descr.type + "/" + descr.id,
|
||||||
value: descr.meta.specialMotivation,
|
value: descr.meta.specialMotivation
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const distances = Utils.NoNull(pending.map((descr) => descr.meta.distanceToObject))
|
const distances = Utils.NoNull(pending.map((descr) => descr.meta.distanceToObject))
|
||||||
|
@ -813,7 +813,7 @@ export class Changes {
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
value: count,
|
value: count,
|
||||||
aggregate: true,
|
aggregate: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -828,19 +828,20 @@ export class Changes {
|
||||||
const metatags: ChangesetTag[] = [
|
const metatags: ChangesetTag[] = [
|
||||||
{
|
{
|
||||||
key: "comment",
|
key: "comment",
|
||||||
value: comment,
|
value: comment
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "theme",
|
key: "theme",
|
||||||
value: theme,
|
value: theme
|
||||||
},
|
},
|
||||||
...perType,
|
...perType,
|
||||||
...motivations,
|
...motivations,
|
||||||
...perBinMessage,
|
...perBinMessage
|
||||||
]
|
]
|
||||||
return metatags
|
return metatags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async flushChangesAsync(): Promise<void> {
|
private async flushChangesAsync(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// At last, we build the changeset and upload
|
// At last, we build the changeset and upload
|
||||||
|
@ -858,16 +859,12 @@ export class Changes {
|
||||||
const refusedChanges: ChangeDescription[][] = await Promise.all(
|
const refusedChanges: ChangeDescription[][] = await Promise.all(
|
||||||
Array.from(pendingPerTheme, async ([theme, pendingChanges]) => {
|
Array.from(pendingPerTheme, async ([theme, pendingChanges]) => {
|
||||||
try {
|
try {
|
||||||
const openChangeset = UIEventSource.asInt(
|
const openChangeset = this.state.osmConnection.getCurrentChangesetFor(theme)
|
||||||
this.state.osmConnection.GetPreference(
|
|
||||||
"current-open-changeset-" + theme
|
|
||||||
)
|
|
||||||
)
|
|
||||||
console.log(
|
console.log(
|
||||||
"Using current-open-changeset-" +
|
"Using current-open-changeset-" +
|
||||||
theme +
|
theme +
|
||||||
" from the preferences, got " +
|
" from the preferences, got " +
|
||||||
openChangeset.data
|
openChangeset.data
|
||||||
)
|
)
|
||||||
|
|
||||||
const refused = await this.flushSelectChanges(pendingChanges, openChangeset)
|
const refused = await this.flushSelectChanges(pendingChanges, openChangeset)
|
||||||
|
|
|
@ -113,11 +113,11 @@ export class ChangesetHandler {
|
||||||
|
|
||||||
private async UploadWithNew(
|
private async UploadWithNew(
|
||||||
generateChangeXML: (csid: number, remappings: Map<string, string>) => string,
|
generateChangeXML: (csid: number, remappings: Map<string, string>) => string,
|
||||||
openChangeset: UIEventSource<number>,
|
openChangeset: UIEventSource<{ id: number, opened: number }>,
|
||||||
extraMetaTags: ChangesetTag[]
|
extraMetaTags: ChangesetTag[]
|
||||||
) {
|
) {
|
||||||
const csId = await this.OpenChangeset(extraMetaTags)
|
const csId = await this.OpenChangeset(extraMetaTags)
|
||||||
openChangeset.setData(csId)
|
openChangeset.setData({ id: csId, opened: new Date().getTime() })
|
||||||
const changeset = generateChangeXML(csId, this._remappings)
|
const changeset = generateChangeXML(csId, this._remappings)
|
||||||
console.log(
|
console.log(
|
||||||
"Opened a new changeset (openChangeset.data is undefined):",
|
"Opened a new changeset (openChangeset.data is undefined):",
|
||||||
|
@ -145,7 +145,7 @@ export class ChangesetHandler {
|
||||||
public async UploadChangeset(
|
public async UploadChangeset(
|
||||||
generateChangeXML: (csid: number, remappings: Map<string, string>) => string,
|
generateChangeXML: (csid: number, remappings: Map<string, string>) => string,
|
||||||
extraMetaTags: ChangesetTag[],
|
extraMetaTags: ChangesetTag[],
|
||||||
openChangeset: UIEventSource<number>
|
openChangeset: UIEventSource<{ id: number, opened: number }>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (
|
if (
|
||||||
!extraMetaTags.some((tag) => tag.key === "comment") ||
|
!extraMetaTags.some((tag) => tag.key === "comment") ||
|
||||||
|
@ -169,18 +169,21 @@ export class ChangesetHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Trying to reuse changeset", openChangeset.data)
|
console.log("Trying to reuse changeset", openChangeset.data)
|
||||||
if (openChangeset.data) {
|
const now = new Date()
|
||||||
|
const changesetIsUsable = openChangeset.data !== undefined &&
|
||||||
|
(now.getTime() - openChangeset.data.opened < 24 * 60 * 60 * 1000)
|
||||||
|
if (changesetIsUsable) {
|
||||||
try {
|
try {
|
||||||
const csId = openChangeset.data
|
const csId = openChangeset.data
|
||||||
const oldChangesetMeta = await this.GetChangesetMeta(csId)
|
const oldChangesetMeta = await this.GetChangesetMeta(csId.id)
|
||||||
console.log("Got metadata:", oldChangesetMeta, "isopen", oldChangesetMeta?.open)
|
console.log("Got metadata:", oldChangesetMeta, "isopen", oldChangesetMeta?.open)
|
||||||
if (oldChangesetMeta.open) {
|
if (oldChangesetMeta.open) {
|
||||||
// We can hopefully reuse the changeset
|
// We can hopefully reuse the changeset
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rewritings = await this.UploadChange(
|
const rewritings = await this.UploadChange(
|
||||||
csId,
|
csId.id,
|
||||||
generateChangeXML(csId, this._remappings)
|
generateChangeXML(csId.id, this._remappings)
|
||||||
)
|
)
|
||||||
|
|
||||||
const rewrittenTags = this.RewriteTagsOf(
|
const rewrittenTags = this.RewriteTagsOf(
|
||||||
|
@ -188,7 +191,7 @@ export class ChangesetHandler {
|
||||||
rewritings,
|
rewritings,
|
||||||
oldChangesetMeta
|
oldChangesetMeta
|
||||||
)
|
)
|
||||||
await this.UpdateTags(csId, rewrittenTags)
|
await this.UpdateTags(csId.id, rewrittenTags)
|
||||||
return // We are done!
|
return // We are done!
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._reportError(e, "While reusing a changeset " + openChangeset.data)
|
this._reportError(e, "While reusing a changeset " + openChangeset.data)
|
||||||
|
@ -236,9 +239,9 @@ export class ChangesetHandler {
|
||||||
/**
|
/**
|
||||||
* Given an existing changeset with metadata and extraMetaTags to add, will fuse them to a new set of metatags
|
* Given an existing changeset with metadata and extraMetaTags to add, will fuse them to a new set of metatags
|
||||||
* Does not yet send data
|
* Does not yet send data
|
||||||
* @param extraMetaTags: new changeset tags to add/fuse with this changeset
|
* @param extraMetaTags new changeset tags to add/fuse with this changeset
|
||||||
* @param rewriteIds: the mapping of ids
|
* @param rewriteIds the mapping of ids
|
||||||
* @param oldChangesetMeta: the metadata-object of the already existing changeset
|
* @param oldChangesetMeta the metadata-object of the already existing changeset
|
||||||
*
|
*
|
||||||
* @public for testing purposes
|
* @public for testing purposes
|
||||||
*/
|
*/
|
||||||
|
@ -250,7 +253,7 @@ export class ChangesetHandler {
|
||||||
id: number
|
id: number
|
||||||
uid: number // User ID
|
uid: number // User ID
|
||||||
changes_count: number
|
changes_count: number
|
||||||
tags: any
|
tags: Record<string, string>
|
||||||
}
|
}
|
||||||
): ChangesetTag[] {
|
): ChangesetTag[] {
|
||||||
// Note: extraMetaTags is where all the tags are collected into
|
// Note: extraMetaTags is where all the tags are collected into
|
||||||
|
@ -300,11 +303,11 @@ export class ChangesetHandler {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the id in the AllElements store, returns the new ID
|
* Updates the id in the AllElements store, returns the new ID
|
||||||
* @param node: the XML-element, e.g. <node old_id="-1" new_id="9650458521" new_version="1"/>
|
* @param node the XML-element, e.g. <node old_id="-1" new_id="9650458521" new_version="1"/>
|
||||||
* @param type
|
* @param type
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private static parseIdRewrite(node: any, type: string): [string, string] {
|
private static parseIdRewrite(node: any, type: "node" | "way" | "relation"): [string, string] {
|
||||||
const oldId = parseInt(node.attributes.old_id.value)
|
const oldId = parseInt(node.attributes.old_id.value)
|
||||||
if (node.attributes.new_id === undefined) {
|
if (node.attributes.new_id === undefined) {
|
||||||
return [type + "/" + oldId, undefined]
|
return [type + "/" + oldId, undefined]
|
||||||
|
|
|
@ -246,11 +246,13 @@ export class OsmConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPreference<T extends string = string>(
|
public getPreference<T extends string = string>(
|
||||||
key: string,
|
key: string, options?: {
|
||||||
defaultValue: string = undefined,
|
defaultValue?: string,
|
||||||
prefix: string = "mapcomplete-"
|
prefix?: "mapcomplete-" | string,
|
||||||
|
saveToLocalStorage?: true | boolean
|
||||||
|
}
|
||||||
): UIEventSource<T | undefined> {
|
): UIEventSource<T | undefined> {
|
||||||
return <UIEventSource<T>>this.preferencesHandler.getPreference(key, defaultValue, prefix)
|
return <UIEventSource<T>>this.preferencesHandler.getPreference(key, options?.defaultValue, options?.prefix ?? "mapcomplete-")
|
||||||
}
|
}
|
||||||
|
|
||||||
public LogOut() {
|
public LogOut() {
|
||||||
|
@ -731,4 +733,24 @@ export class OsmConnection {
|
||||||
return { api: "offline", gpx: "offline", database: "online" }
|
return { api: "offline", gpx: "offline", database: "online" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCurrentChangesetFor(theme: string) {
|
||||||
|
return UIEventSource.asObject<{ id: number, opened: number }>(
|
||||||
|
this.GetPreference(
|
||||||
|
"current-changeset-" + theme
|
||||||
|
),
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the names of the themes that have an open changeset
|
||||||
|
*/
|
||||||
|
public getAllOpenChangesetsPreferences(): Store<string[]> {
|
||||||
|
const prefix = "current-changeset-"
|
||||||
|
return this.preferencesHandler.allPreferences.map(dict =>
|
||||||
|
Object.keys(dict)
|
||||||
|
.filter(k => k.startsWith(prefix))
|
||||||
|
.map(k => k.substring(prefix.length)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ export class OsmPreferences {
|
||||||
private localStorageInited: Set<string> = new Set()
|
private localStorageInited: Set<string> = new Set()
|
||||||
/**
|
/**
|
||||||
* Contains all the keys as returned by the OSM-preferences.
|
* Contains all the keys as returned by the OSM-preferences.
|
||||||
|
* This includes combined preferences, such as: pref, pref:0, pref:1
|
||||||
* Used to clean up old preferences
|
* Used to clean up old preferences
|
||||||
*/
|
*/
|
||||||
private seenKeys: string[] = []
|
private seenKeys: string[] = []
|
||||||
|
@ -59,18 +60,18 @@ export class OsmPreferences {
|
||||||
value: string = undefined,
|
value: string = undefined,
|
||||||
deferPing = false
|
deferPing = false
|
||||||
): UIEventSource<string> {
|
): UIEventSource<string> {
|
||||||
if (this.preferences[key] !== undefined) {
|
const cached = this.preferences[key]
|
||||||
|
if (cached !== undefined) {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
this.preferences[key].set(value)
|
cached.set(value)
|
||||||
}
|
}
|
||||||
return this.preferences[key]
|
return cached
|
||||||
}
|
}
|
||||||
const pref = (this.preferences[key] = new UIEventSource(value, "preference: " + key))
|
const pref = (this.preferences[key] = new UIEventSource(value, "preference: " + key))
|
||||||
if (value) {
|
if (value) {
|
||||||
this.setPreferencesAll(key, value, deferPing)
|
this.setPreferencesAll(key, value, deferPing)
|
||||||
}
|
}
|
||||||
pref.addCallback((v) => {
|
pref.addCallback((v) => {
|
||||||
console.log("Got an update:", key, "--->", v)
|
|
||||||
this.uploadKvSplit(key, v)
|
this.uploadKvSplit(key, v)
|
||||||
this.setPreferencesAll(key, v, deferPing)
|
this.setPreferencesAll(key, v, deferPing)
|
||||||
})
|
})
|
||||||
|
@ -82,13 +83,16 @@ export class OsmPreferences {
|
||||||
this.seenKeys = Object.keys(prefs)
|
this.seenKeys = Object.keys(prefs)
|
||||||
const merged = OsmPreferences.mergeDict(prefs)
|
const merged = OsmPreferences.mergeDict(prefs)
|
||||||
for (const key in merged) {
|
for (const key in merged) {
|
||||||
this.initPreference(key, prefs[key], true)
|
this.initPreference(key, merged[key], true)
|
||||||
}
|
}
|
||||||
this._allPreferences.ping()
|
this._allPreferences.ping()
|
||||||
|
if (this.osmConnection.isLoggedIn.data) {
|
||||||
|
await this.cleanup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPreference(key: string, defaultValue: string = undefined, prefix?: string) {
|
public getPreference(key: string, defaultValue: string = undefined, prefix?: string, saveLocally = true) {
|
||||||
return this.getPreferenceSeedFromlocal(key, defaultValue, { prefix })
|
return this.getPreferenceSeedFromlocal(key, defaultValue, { prefix, saveToLocalStorage: saveLocally })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,23 +143,52 @@ export class OsmPreferences {
|
||||||
* OsmPreferences.mergeDict({abc: "123", def: "123", "def:0": "456", "def:1":"789"}) // => {abc: "123", def: "123456789"}
|
* OsmPreferences.mergeDict({abc: "123", def: "123", "def:0": "456", "def:1":"789"}) // => {abc: "123", def: "123456789"}
|
||||||
*/
|
*/
|
||||||
private static mergeDict(dict: Record<string, string>): Record<string, string> {
|
private static mergeDict(dict: Record<string, string>): Record<string, string> {
|
||||||
const newDict = {}
|
|
||||||
|
|
||||||
const allKeys: string[] = Object.keys(dict)
|
const keyParts: Record<string, Record<number, string>> = {}
|
||||||
const normalKeys = allKeys.filter((k) => !k.match(/[a-z-_0-9A-Z]*:[0-9]+/))
|
const endsWithNumber = /:[0-9]+$/
|
||||||
for (const normalKey of normalKeys) {
|
for (const key of Object.keys(dict)) {
|
||||||
if (normalKey.match(/-combined-[0-9]*$/) || normalKey.match(/-combined-length$/)) {
|
if (key.match(/-combined-[0-9]*$/) || key.match(/-combined-length$/)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const partKeys = OsmPreferences.keysStartingWith(allKeys, normalKey)
|
const nr = key.match(endsWithNumber)
|
||||||
const parts = partKeys.map((k) => dict[k])
|
if (nr) {
|
||||||
newDict[normalKey] = parts.join("")
|
const i = Number(nr[0].substring(1))
|
||||||
|
const k = key.substring(0, key.length - nr[0].length)
|
||||||
|
let subparts = keyParts[k]
|
||||||
|
if (!subparts) {
|
||||||
|
subparts = {}
|
||||||
|
keyParts[k] = subparts
|
||||||
|
}
|
||||||
|
subparts[i] = dict[key]
|
||||||
|
} else {
|
||||||
|
let subparts = keyParts[key]
|
||||||
|
if (!subparts) {
|
||||||
|
subparts = keyParts[key] = {}
|
||||||
|
}
|
||||||
|
subparts[""] = dict[key]
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newDict = {}
|
||||||
|
|
||||||
|
for (const key in keyParts) {
|
||||||
|
const subparts = keyParts[key]
|
||||||
|
let i = 0
|
||||||
|
let v = subparts[""] ?? ""
|
||||||
|
while (subparts[i]) {
|
||||||
|
v += subparts[i]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
newDict[key] = v
|
||||||
|
}
|
||||||
|
|
||||||
return newDict
|
return newDict
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bulk-downloads all preferences, creates a simple record from all
|
* Bulk-downloads all preferences, creates a simple record from all preferences.
|
||||||
|
* This should still be merged!
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async getPreferencesDictDirectly(): Promise<Record<string, string>> {
|
private async getPreferencesDictDirectly(): Promise<Record<string, string>> {
|
||||||
|
@ -166,7 +199,7 @@ export class OsmPreferences {
|
||||||
this.auth.xhr(
|
this.auth.xhr(
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/api/0.6/user/preferences",
|
path: "/api/0.6/user/preferences"
|
||||||
},
|
},
|
||||||
(error, value: XMLDocument) => {
|
(error, value: XMLDocument) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -187,6 +220,9 @@ export class OsmPreferences {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static readonly endsWithNumber = /:[0-9]+$/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all keys matching `k:[number]`
|
* Returns all keys matching `k:[number]`
|
||||||
* Split separately for test
|
* Split separately for test
|
||||||
|
@ -198,7 +234,24 @@ export class OsmPreferences {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
private static keysStartingWith(allKeys: string[], key: string): string[] {
|
private static keysStartingWith(allKeys: string[], key: string): string[] {
|
||||||
const keys = allKeys.filter((k) => k === key || k.match(new RegExp(key + ":[0-9]+")))
|
|
||||||
|
const keys = allKeys.filter((k) => {
|
||||||
|
if (k === key) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!k.startsWith(key)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const match = k.match(OsmPreferences.endsWithNumber)
|
||||||
|
if (!match) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const matchLength = match[0].length
|
||||||
|
if (key.length + matchLength !== k.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
keys.sort()
|
keys.sort()
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
@ -247,7 +300,7 @@ export class OsmPreferences {
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
|
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
|
||||||
headers: { "Content-Type": "text/plain" },
|
headers: { "Content-Type": "text/plain" }
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -255,7 +308,6 @@ export class OsmPreferences {
|
||||||
reject(error)
|
reject(error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.debug("Preference ", k, "removed!")
|
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -289,33 +341,50 @@ export class OsmPreferences {
|
||||||
throw "Preference too long, at most 255 characters are supported"
|
throw "Preference too long, at most 255 characters are supported"
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
try {
|
||||||
this.auth.xhr(
|
|
||||||
{
|
return this.osmConnection.interact("user/preferences/" + encodeURIComponent(k),
|
||||||
method: "PUT",
|
"PUT", { "Content-Type": "text/plain" }, v)
|
||||||
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
|
} catch (e) {
|
||||||
headers: { "Content-Type": "text/plain" },
|
console.error("Could not upload preference due to", e)
|
||||||
content: v,
|
}
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
if (error) {
|
|
||||||
console.warn(`Could not set preference "${k}"'`, error)
|
|
||||||
reject(error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeAllWithPrefix(prefix: string) {
|
async removeAllWithPrefix(prefix: string) {
|
||||||
const keys = this.seenKeys
|
const keys = this.seenKeys
|
||||||
|
let somethingChanged = false
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (!key.startsWith(prefix)) {
|
if (!key.startsWith(prefix)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
console.log("Cleaning up preference", key)
|
||||||
await this.deleteKeyDirectly(key)
|
await this.deleteKeyDirectly(key)
|
||||||
|
somethingChanged = true
|
||||||
}
|
}
|
||||||
|
return somethingChanged
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cleanup() {
|
||||||
|
const prefixesToClean = ["mapcomplete-mapcomplete-", "mapcomplete-places-history", "unofficial-theme-", "mapcompleteplaces", "mapcompletethemes"] // TODO enable this one once the new system is in prod "mapcomplete-current-open-changeset-"]
|
||||||
|
let somethingChanged = false
|
||||||
|
for (const prefix of prefixesToClean) {
|
||||||
|
const hasChange = await this.removeAllWithPrefix(prefix) // Don't inline - short-circuiting
|
||||||
|
somethingChanged ||= hasChange
|
||||||
|
}
|
||||||
|
if (somethingChanged) {
|
||||||
|
this._allPreferences.ping()
|
||||||
|
}
|
||||||
|
|
||||||
|
const themes = this.osmConnection.getAllOpenChangesetsPreferences()
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
for (const theme of themes.data) {
|
||||||
|
const cs = this.osmConnection.getCurrentChangesetFor(theme)
|
||||||
|
if (now.getTime() - cs.data.opened > 24 * 60 * 60 * 1000) {
|
||||||
|
console.log("Clearing 'open changeset' for theme", theme, "; definitively expired by now")
|
||||||
|
cs.set(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,10 @@ import { AndroidPolyfill } from "../Web/AndroidPolyfill"
|
||||||
export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied"
|
export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied"
|
||||||
|
|
||||||
export interface GeoLocationPointProperties extends GeolocationCoordinates {
|
export interface GeoLocationPointProperties extends GeolocationCoordinates {
|
||||||
id: "gps"
|
id: "gps" | string
|
||||||
"user:location": "yes"
|
"user:location": "yes"
|
||||||
date: string
|
date: string,
|
||||||
|
alpha?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -26,10 +26,12 @@ class RoundRobinStore<T> {
|
||||||
private readonly _index: UIEventSource<number>
|
private readonly _index: UIEventSource<number>
|
||||||
private readonly _value: UIEventSource<T[]>
|
private readonly _value: UIEventSource<T[]>
|
||||||
public readonly value: Store<T[]>
|
public readonly value: Store<T[]>
|
||||||
|
private readonly _maxCount: number
|
||||||
|
|
||||||
constructor(store: UIEventSource<T[]>, index: UIEventSource<number>) {
|
constructor(store: UIEventSource<T[]>, index: UIEventSource<number>, maxCount: number) {
|
||||||
this._store = store
|
this._store = store
|
||||||
this._index = index
|
this._index = index
|
||||||
|
this._maxCount = maxCount
|
||||||
this._value = new UIEventSource([])
|
this._value = new UIEventSource([])
|
||||||
this.value = this._value
|
this.value = this._value
|
||||||
this._store.addCallbackD(() => this.set())
|
this._store.addCallbackD(() => this.set())
|
||||||
|
@ -40,6 +42,9 @@ class RoundRobinStore<T> {
|
||||||
const v = this._store.data
|
const v = this._store.data
|
||||||
const i = this._index.data
|
const i = this._index.data
|
||||||
const newList = Utils.NoNull(v.slice(i + 1, v.length).concat(v.slice(0, i + 1)))
|
const newList = Utils.NoNull(v.slice(i + 1, v.length).concat(v.slice(0, i + 1)))
|
||||||
|
if (newList.length === 0) {
|
||||||
|
this._index.set(0)
|
||||||
|
}
|
||||||
this._value.set(newList)
|
this._value.set(newList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +54,7 @@ class RoundRobinStore<T> {
|
||||||
*/
|
*/
|
||||||
public add(t: T) {
|
public add(t: T) {
|
||||||
const i = this._index.data
|
const i = this._index.data
|
||||||
this._index.set((i + 1) % this._store.data.length)
|
this._index.set((i + 1) % this._maxCount)
|
||||||
this._store.data[i] = t
|
this._store.data[i] = t
|
||||||
this._store.ping()
|
this._store.ping()
|
||||||
|
|
||||||
|
@ -78,15 +83,21 @@ export class OptionallySyncedHistory<T extends object | string> {
|
||||||
this.osmconnection = osmconnection
|
this.osmconnection = osmconnection
|
||||||
this._maxHistory = maxHistory
|
this._maxHistory = maxHistory
|
||||||
this._isSame = isSame
|
this._isSame = isSame
|
||||||
this.syncPreference = osmconnection.getPreference("preference-" + key + "-history", "sync")
|
this.syncPreference = osmconnection.getPreference("preference-" + key + "-history", {
|
||||||
|
defaultValue: "sync"
|
||||||
|
})
|
||||||
|
|
||||||
this.syncedBackingStore = Stores.fromArray(
|
this.syncedBackingStore = Stores.fromArray(
|
||||||
Utils.TimesT(maxHistory, (i) =>
|
Utils.TimesT(maxHistory, (i) => {
|
||||||
UIEventSource.asObject<T>(osmconnection.getPreference(key + "-history-" + i), undefined)
|
const pref = osmconnection.getPreference(key + "-hist-" + i + "-")
|
||||||
))
|
pref.addCallbackAndRun(v => console.trace(">>> pref", pref.tag, " is now ", v))
|
||||||
this.syncedOrdered = new RoundRobinStore<T>(this.syncedBackingStore,
|
return UIEventSource.asObject<T>(pref, undefined)
|
||||||
UIEventSource.asInt(osmconnection.getPreference(key + "-history-round-robin", "0"))
|
}))
|
||||||
)
|
|
||||||
|
const ringIndex = UIEventSource.asInt(osmconnection.getPreference(key + "-hist-round-robin", {
|
||||||
|
defaultValue: "0"
|
||||||
|
}))
|
||||||
|
this.syncedOrdered = new RoundRobinStore<T>(this.syncedBackingStore, ringIndex, 10)
|
||||||
const local = (this.local = LocalStorageSource.getParsed<T[]>(key + "-history", []))
|
const local = (this.local = LocalStorageSource.getParsed<T[]>(key + "-history", []))
|
||||||
const thisSession = (this.thisSession = new UIEventSource<T[]>(
|
const thisSession = (this.thisSession = new UIEventSource<T[]>(
|
||||||
[],
|
[],
|
||||||
|
@ -250,33 +261,33 @@ export default class UserRelatedState {
|
||||||
this.osmConnection = osmConnection
|
this.osmConnection = osmConnection
|
||||||
|
|
||||||
this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
|
this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
|
||||||
this.osmConnection.getPreference("show-all-questions", "false")
|
this.osmConnection.getPreference("show-all-questions", { defaultValue: "false" })
|
||||||
)
|
)
|
||||||
this.language = this.osmConnection.getPreference("language")
|
this.language = this.osmConnection.getPreference("language")
|
||||||
this.showTags = this.osmConnection.getPreference("show_tags")
|
this.showTags = this.osmConnection.getPreference("show_tags")
|
||||||
this.showCrosshair = this.osmConnection.getPreference("show_crosshair")
|
this.showCrosshair = this.osmConnection.getPreference("show_crosshair")
|
||||||
this.fixateNorth = this.osmConnection.getPreference("fixate-north")
|
this.fixateNorth = this.osmConnection.getPreference("fixate-north")
|
||||||
this.morePrivacy = this.osmConnection.getPreference("more_privacy", "no")
|
this.morePrivacy = this.osmConnection.getPreference("more_privacy", { defaultValue: "no" })
|
||||||
|
|
||||||
this.a11y = this.osmConnection.getPreference("a11y")
|
this.a11y = this.osmConnection.getPreference("a11y")
|
||||||
|
|
||||||
this.mangroveIdentity = new MangroveIdentity(
|
this.mangroveIdentity = new MangroveIdentity(
|
||||||
this.osmConnection.getPreference("identity", undefined, "mangrove"),
|
this.osmConnection.getPreference("identity", { defaultValue: undefined, prefix: "mangrove" }),
|
||||||
this.osmConnection.getPreference("identity-creation-date", undefined, "mangrove")
|
this.osmConnection.getPreference("identity-creation-date", {
|
||||||
)
|
defaultValue: undefined,
|
||||||
this.preferredBackgroundLayer = this.osmConnection.getPreference(
|
prefix: "mangrove"
|
||||||
"preferred-background-layer"
|
})
|
||||||
)
|
)
|
||||||
|
this.preferredBackgroundLayer = this.osmConnection.getPreference("preferred-background-layer")
|
||||||
|
|
||||||
this.addNewFeatureMode = this.osmConnection.getPreference(
|
this.addNewFeatureMode = this.osmConnection.getPreference("preferences-add-new-mode",
|
||||||
"preferences-add-new-mode",
|
{ defaultValue: "button_click_right" }
|
||||||
"button_click_right"
|
|
||||||
)
|
)
|
||||||
this.showScale = UIEventSource.asBoolean(
|
this.showScale = UIEventSource.asBoolean(
|
||||||
this.osmConnection.GetPreference("preference-show-scale", "false")
|
this.osmConnection.getPreference("preference-show-scale", { defaultValue: "false" })
|
||||||
)
|
)
|
||||||
|
|
||||||
this.imageLicense = this.osmConnection.getPreference("pictures-license", "CC0")
|
this.imageLicense = this.osmConnection.getPreference("pictures-license", { defaultValue: "CC0" })
|
||||||
this.installedUserThemes = UserRelatedState.initInstalledUserThemes(osmConnection)
|
this.installedUserThemes = UserRelatedState.initInstalledUserThemes(osmConnection)
|
||||||
this.translationMode = this.initTranslationMode()
|
this.translationMode = this.initTranslationMode()
|
||||||
this.homeLocation = this.initHomeLocation()
|
this.homeLocation = this.initHomeLocation()
|
||||||
|
@ -309,7 +320,7 @@ export default class UserRelatedState {
|
||||||
|
|
||||||
private initTranslationMode(): UIEventSource<"false" | "true" | "mobile" | undefined | string> {
|
private initTranslationMode(): UIEventSource<"false" | "true" | "mobile" | undefined | string> {
|
||||||
const translationMode: UIEventSource<undefined | "true" | "false" | "mobile" | string> =
|
const translationMode: UIEventSource<undefined | "true" | "false" | "mobile" | string> =
|
||||||
this.osmConnection.getPreference("translation-mode", "false")
|
this.osmConnection.getPreference("translation-mode", { defaultValue: "false" })
|
||||||
translationMode.addCallbackAndRunD((mode) => {
|
translationMode.addCallbackAndRunD((mode) => {
|
||||||
mode = mode.toLowerCase()
|
mode = mode.toLowerCase()
|
||||||
if (mode === "true" || mode === "yes") {
|
if (mode === "true" || mode === "yes") {
|
||||||
|
@ -358,7 +369,7 @@ export default class UserRelatedState {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(str)
|
return JSON.parse(str)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
e.warn(
|
console.warn(
|
||||||
"Removing theme " +
|
"Removing theme " +
|
||||||
id +
|
id +
|
||||||
" as it could not be parsed from the preferences; the content is:",
|
" as it could not be parsed from the preferences; the content is:",
|
||||||
|
@ -639,7 +650,7 @@ export default class UserRelatedState {
|
||||||
public getThemeDisabled(themeId: string, layerId: string): UIEventSource<string[]> {
|
public getThemeDisabled(themeId: string, layerId: string): UIEventSource<string[]> {
|
||||||
const flatSource = this.osmConnection.getPreference(
|
const flatSource = this.osmConnection.getPreference(
|
||||||
"disabled-questions-" + themeId + "-" + layerId,
|
"disabled-questions-" + themeId + "-" + layerId,
|
||||||
"[]"
|
{ defaultValue: "[]" }
|
||||||
)
|
)
|
||||||
return UIEventSource.asObject<string[]>(flatSource, [])
|
return UIEventSource.asObject<string[]>(flatSource, [])
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,11 +38,11 @@ export class IdbLocalStorage {
|
||||||
return src
|
return src
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SetDirectly(key: string, value: any): Promise<void> {
|
public static SetDirectly<T>(key: string, value: T): Promise<void> {
|
||||||
return idb.set(key, value)
|
return idb.set(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
static GetDirectly(key: string): Promise<any> {
|
static GetDirectly<T>(key: string): Promise<T> {
|
||||||
return idb.get(key)
|
return idb.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
import ThemeConfig from "../ThemeConfig/ThemeConfig"
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import { Map as MlMap } from "maplibre-gl"
|
import { Map as MlMap } from "maplibre-gl"
|
||||||
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
|
import { GeoLocationPointProperties, GeoLocationState } from "../../Logic/State/GeoLocationState"
|
||||||
import InitialMapPositioning from "../../Logic/Actors/InitialMapPositioning"
|
import InitialMapPositioning from "../../Logic/Actors/InitialMapPositioning"
|
||||||
import { MapLibreAdaptor } from "../../UI/Map/MapLibreAdaptor"
|
import { MapLibreAdaptor } from "../../UI/Map/MapLibreAdaptor"
|
||||||
import { ExportableMap, MapProperties } from "../MapProperties"
|
import { ExportableMap, MapProperties } from "../MapProperties"
|
||||||
|
@ -43,7 +43,7 @@ export class UserMapFeatureswitchState extends WithUserRelatedState {
|
||||||
readonly geolocationState: GeoLocationState
|
readonly geolocationState: GeoLocationState
|
||||||
readonly geolocation: GeoLocationHandler
|
readonly geolocation: GeoLocationHandler
|
||||||
readonly geolocationControl: GeolocationControlState
|
readonly geolocationControl: GeolocationControlState
|
||||||
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
|
readonly historicalUserLocations: WritableFeatureSource<Feature<Point, GeoLocationPointProperties>>
|
||||||
|
|
||||||
readonly availableLayers: { store: Store<RasterLayerPolygon[]> }
|
readonly availableLayers: { store: Store<RasterLayerPolygon[]> }
|
||||||
readonly currentView: FeatureSource<Feature<Polygon>>
|
readonly currentView: FeatureSource<Feature<Polygon>>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue