Add metadata in changeset with (binned) distance to changed feature

This commit is contained in:
pietervdvn 2021-11-09 01:49:07 +01:00
parent e8ce53d5eb
commit 8e66313ef1
21 changed files with 178 additions and 41 deletions

View file

@ -6,6 +6,18 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {QueryParameters} from "../Web/QueryParameters";
import FeatureSource from "../FeatureSource/FeatureSource";
export interface GeoLocationPointProperties {
id: "gps",
"user:location": "yes",
"date": string,
"latitude": number
"longitude":number,
"speed": number,
"accuracy": number
"heading": number
"altitude":number
}
export default class GeoLocationHandler extends VariableUiElement {
private readonly currentLocation: FeatureSource
@ -184,10 +196,9 @@ export default class GeoLocationHandler extends VariableUiElement {
this.currentLocation = state.currentUserLocation
this._currentGPSLocation.addCallback((location) => {
self._previousLocationGrant.setData("granted");
console.log("Location is", location,)
const feature = {
"type": "Feature",
properties: {
properties: <GeoLocationPointProperties>{
id: "gps",
"user:location": "yes",
"date": new Date().toISOString(),

View file

@ -20,7 +20,11 @@ export interface ChangeDescription {
/**
* THe motivation for the change, e.g. 'deleted because does not exist anymore'
*/
specialMotivation?: string
specialMotivation?: string,
/**
* Added by Changes.ts
*/
distanceToObject?: number
},
/**

View file

@ -11,7 +11,7 @@ export default class ChangeLocationAction extends OsmChangeAction {
theme: string,
reason: string
}) {
super();
super(id,true);
if (!id.startsWith("node/")) {
throw "Invalid ID: only 'node/number' is accepted"
}

View file

@ -13,7 +13,7 @@ export default class ChangeTagAction extends OsmChangeAction {
theme: string,
changeType: "answer" | "soft-delete" | "add-image" | string
}) {
super();
super(elementId, true);
this._elementId = elementId;
this._tagsFilter = tagsFilter;
this._currentTags = currentTags;

View file

@ -31,7 +31,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
reusePointWithinMeters?: number,
theme: string, changeType: "create" | "import" | null
}) {
super()
super(null,basicTags !== undefined && basicTags.length > 0)
this._basicTags = basicTags;
this._lat = lat;
this._lon = lon;

View file

@ -24,13 +24,13 @@ export default class CreateNewWayAction extends OsmChangeAction {
options: {
theme: string
}) {
super()
super(null,true)
this.coordinates = coordinates;
this.tags = tags;
this._options = options;
}
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
const newElements: ChangeDescription[] = []
@ -46,7 +46,7 @@ export default class CreateNewWayAction extends OsmChangeAction {
changeType: null,
theme: this._options.theme
})
await changes.applyAction(newPoint)
newElements.push(...await newPoint.CreateChangeDescriptions(changes))
pointIds.push(newPoint.newElementIdNumber)
}

View file

@ -46,7 +46,7 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction {
state: FeaturePipelineState,
config: MergePointConfig[]
) {
super();
super(null,true);
this._tags = tags;
this._state = state;
this._config = config;
@ -195,8 +195,7 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction {
theme
})
allChanges.push(...(await newWay.Perform(changes)))
allChanges.push(...(await newWay.CreateChangeDescriptions(changes)))
return allChanges
}

View file

@ -27,7 +27,7 @@ export default class DeleteAction extends OsmChangeAction {
specialMotivation: string
},
hardDelete: boolean) {
super()
super(id,true)
this._id = id;
this._hardDelete = hardDelete;
this.meta = {...meta, changeType: "deletion"};

View file

@ -8,6 +8,18 @@ import {ChangeDescription} from "./ChangeDescription";
export default abstract class OsmChangeAction {
private isUsed = false
public readonly trackStatistics: boolean;
/**
* The ID of the object that is the center of this change.
* Null if the action creates a new object
* Undefined if such an id does not make sense
*/
public readonly mainObjectId: string;
constructor(mainObjectId: string, trackStatistics: boolean = true) {
this.trackStatistics = trackStatistics;
this.mainObjectId = mainObjectId
}
public Perform(changes: Changes) {
if (this.isUsed) {
@ -18,6 +30,4 @@ export default abstract class OsmChangeAction {
}
protected abstract CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]>
}

View file

@ -16,11 +16,10 @@ abstract class AbstractRelationSplitHandler extends OsmChangeAction {
protected readonly _theme: string;
constructor(input: RelationSplitInput, theme: string) {
super()
super("relation/"+input.relation.id, false)
this._input = input;
this._theme = theme;
}
/**
* Returns which node should border the member at the given index
*/

View file

@ -41,7 +41,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
newTags?: Tag[]
}
) {
super();
super(wayToReplaceId, false);
this.state = state;
this.feature = feature;
this.wayToReplaceId = wayToReplaceId;

View file

@ -26,7 +26,7 @@ export default class SplitAction extends OsmChangeAction {
* @param toleranceInMeters: if a splitpoint closer then this amount of meters to an existing point, the existing point will be used to split the line instead of a new point
*/
constructor(wayId: string, splitPointCoordinates: [number, number][], meta: { theme: string }, toleranceInMeters = 5) {
super()
super(wayId,true)
this.wayId = wayId;
this._splitPointsCoordinates = splitPointCoordinates
this._toleranceInMeters = toleranceInMeters;

View file

@ -8,6 +8,11 @@ import {Utils} from "../../Utils";
import {LocalStorageSource} from "../Web/LocalStorageSource";
import SimpleMetaTagger from "../SimpleMetaTagger";
import CreateNewNodeAction from "./Actions/CreateNewNodeAction";
import FeatureSource from "../FeatureSource/FeatureSource";
import {ElementStorage} from "../ElementStorage";
import {GeoLocationPointProperties} from "../Actors/GeoLocationHandler";
import {GeoOperations} from "../GeoOperations";
import {ChangesetTag} from "./ChangesetHandler";
/**
* Handles all changes made to OSM.
@ -28,6 +33,8 @@ export class Changes {
private readonly previouslyCreated: OsmObject[] = []
private readonly _leftRightSensitive: boolean;
private _state : { allElements: ElementStorage; historicalUserLocations: FeatureSource }
constructor(leftRightSensitive: boolean = false) {
this._leftRightSensitive = leftRightSensitive;
// We keep track of all changes just as well
@ -113,14 +120,71 @@ export class Changes {
})
}
public async applyAction(action: OsmChangeAction): Promise<void> {
this.applyChanges(await action.Perform(this))
private calculateDistanceToChanges(change: OsmChangeAction, changeDescriptions: ChangeDescription[]){
if (this._state === undefined) {
// No state loaded -> we can't calculate...
return;
}
if(!change.trackStatistics){
// Probably irrelevant, such as a new helper node
return;
}
const now = new Date()
const recentLocationPoints = this._state.historicalUserLocations.features.data.map(ff => ff.feature)
.filter(feat => feat.geometry.type === "Point")
.filter(feat => {
const visitTime = new Date((<GeoLocationPointProperties>feat.properties).date)
// In seconds
const diff = (now.getTime() - visitTime.getTime()) / 1000
return diff < Constants.nearbyVisitTime;
})
if(recentLocationPoints.length === 0){
// Probably no GPS enabled/no fix
return;
}
public async applyActions(actions: OsmChangeAction[]) {
for (const action of actions) {
await this.applyAction(action)
// The applicable points, contain information in their properties about location, time and GPS accuracy
// They are all GeoLocationPointProperties
// We walk every change and determine the closest distance possible
// Only if the change itself does _not_ contain any coordinates, we fall back and search the original feature in the state
const changedObjectCoordinates : [number, number][] = []
const feature = this._state.allElements.ContainingFeatures.get(change.mainObjectId)
if(feature !== undefined){
changedObjectCoordinates.push(GeoOperations.centerpointCoordinates(feature))
}
for (const changeDescription of changeDescriptions) {
const chng : {lat: number, lon: number} | {coordinates : [number,number][]} | {members} = changeDescription.changes
if(chng === undefined){
continue
}
if(chng["lat"] !== undefined){
changedObjectCoordinates.push([chng["lat"],chng["lon"]])
}
if(chng["coordinates"] !== undefined){
changedObjectCoordinates.push(...chng["coordinates"])
}
}
const leastDistance = Math.min(...changedObjectCoordinates.map(coor =>
Math.min(...recentLocationPoints.map(gpsPoint => {
const otherCoor = GeoOperations.centerpointCoordinates(gpsPoint)
const dist = GeoOperations.distanceBetween(coor, otherCoor) * 1000;
console.log("Comparing ", coor, "and ", otherCoor, " --> ", dist)
return dist
}))
))
return leastDistance
}
public async applyAction(action: OsmChangeAction): Promise<void> {
const changeDescriptions = await action.Perform(this)
const distanceToObject = this.calculateDistanceToChanges(action, changeDescriptions)
changeDescriptions[0].meta.distanceToObject = distanceToObject
this.applyChanges(changeDescriptions)
}
public applyChanges(changes: ChangeDescription[]) {
@ -131,6 +195,13 @@ export class Changes {
this.allChanges.ping()
}
public useLocationHistory(state: {
allElements: ElementStorage,
historicalUserLocations: FeatureSource
}){
this._state= state
}
public registerIdRewrites(mappings: Map<string, string>): void {
CreateNewNodeAction.registerIdRewrites(mappings)
}
@ -162,7 +233,6 @@ export class Changes {
return true
}
const meta = pending[0].meta
const perType = Array.from(
Utils.Hist(pending.filter(descr => descr.meta.changeType !== undefined && descr.meta.changeType !== null)
@ -177,16 +247,46 @@ export class Changes {
key: descr.meta.changeType + ":" + descr.type + "/" + descr.id,
value: descr.meta.specialMotivation
}))
const metatags = [{
const distances = Utils.NoNull(pending.map(descr => descr.meta.distanceToObject));
distances.sort((a, b) => a - b)
const perBinCount = Constants.distanceToChangeObjectBins.map(_ => 0)
let j = 0;
const maxDistances = Constants.distanceToChangeObjectBins
for (let i = 0; i < maxDistances.length; i++){
const maxDistance = maxDistances[i];
// distances is sorted in ascending order, so as soon as one is to big, all the resting elements will be bigger too
while(j < distances.length && distances[j] < maxDistance){
perBinCount[i] ++
j++
}
}
const perBinMessage = Utils.NoNull(perBinCount.map((count, i) => {
if(count === 0){
return undefined
}
return {
key: "change_within_"+maxDistances[i]+"m",
value: count,
aggregate:true
}
}))
// This method is only called with changedescriptions for this theme
const theme = pending[0].meta.theme
const metatags : ChangesetTag[] = [{
key: "comment",
value: "Adding data with #MapComplete for theme #" + meta.theme
value: "Adding data with #MapComplete for theme #" + theme
},
{
key: "theme",
value: meta.theme
value: theme
},
...perType,
...motivations
...motivations,
...perBinMessage
]
await State.state.osmConnection.changesetHandler.UploadChangeset(

View file

@ -78,6 +78,7 @@ export class ChangesetHandler {
}
if (this._dryRun) {
const changesetXML = generateChangeXML(123456);
console.log("Metatags are", extraMetaTags)
console.log(changesetXML);
return;
}

View file

@ -11,6 +11,7 @@ import {Utils} from "../../Utils";
import ChangeToElementsActor from "../Actors/ChangeToElementsActor";
import PendingChangesUploader from "../Actors/PendingChangesUploader";
import TitleHandler from "../Actors/TitleHandler";
import FeatureSource from "../FeatureSource/FeatureSource";
/**
* The part of the state keeping track of where the elements, loading them, configuring the feature pipeline etc
@ -50,7 +51,6 @@ export default class ElementsState extends FeatureSwitchState {
super(layoutToUse);
this.changes = new Changes(layoutToUse?.isLeftRightSensitive() ?? false)
{
// -- Location control initialization
const zoom = UIEventSource.asFloat(

View file

@ -14,7 +14,7 @@ import {QueryParameters} from "../Web/QueryParameters";
import * as personal from "../../assets/themes/personal/personal.json";
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer";
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource";
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource";
/**
@ -209,7 +209,6 @@ export default class MapState extends UserRelatedState {
const feature = JSON.parse(JSON.stringify(location.feature))
feature.properties.id = "gps/"+i
i++
console.log("New location: ", feature)
features.data.push({feature, freshness: new Date()})
histCoordinates.push(feature.geometry.coordinates)
@ -224,7 +223,7 @@ export default class MapState extends UserRelatedState {
let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_track")[0]
this.historicalUserLocations = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0), features);
this.changes.useLocationHistory(this)
}
private initHomeLocation() {

View file

@ -11,6 +11,7 @@ import ElementsState from "./ElementsState";
import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater";
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource";
import FeatureSource from "../FeatureSource/FeatureSource";
import {Feature} from "@turf/turf";
/**
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,

View file

@ -2,7 +2,7 @@ import {Utils} from "../Utils";
export default class Constants {
public static vNumber = "0.12.3";
public static vNumber = "0.12.4";
public static ImgurApiKey = '7070e7167f0a25a'
public static readonly mapillary_client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2'
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
@ -39,6 +39,19 @@ export default class Constants {
* (Note that pendingChanges might upload sooner if the popup is closed or similar)
*/
static updateTimeoutSec: number = 30;
/**
* If the contributor has their GPS location enabled and makes a change,
* the points visited less then `nearbyVisitTime`-seconds ago will be inspected.
* The point closest to the changed feature will be considered and this distance will be tracked.
* ALl these distances are used to calculate a nearby-score
*/
static nearbyVisitTime: number= 30 * 60;
/**
* If a user makes a change, the distance to the changed object is calculated.
* If a user makes multiple changes, all these distances are put into multiple bins, depending on this distance.
* For every bin, the totals are uploaded as metadata
*/
static distanceToChangeObjectBins = [25,50,100,500,1000,5000]
private static isRetina(): boolean {
if (Utils.runningFromConsole) {

View file

@ -15,7 +15,7 @@ export default class AllThemesGui {
try {
new FixedUiElement("").AttachTo("centermessage")
const state = new UserRelatedState(undefined);
const state = new UserRelatedState(undefined, undefined);
const intro = new Combine([
LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages())
.SetClass("absolute top-2 right-3"),

View file

@ -95,7 +95,7 @@ export default class SplitRoadWizard extends Toggle {
const points = splitPoints.data.map((f, i) => [f.feature, i])
.filter(p => GeoOperations.distanceBetween(p[0].geometry.coordinates, coordinates) * 1000 < 5)
.map(p => p[1])
.sort()
.sort((a, b) => a - b)
.reverse()
if (points.length > 0) {
for (const point of points) {

View file

@ -52,7 +52,7 @@ export default class ActorsSpec extends T {
[
"download latest version",
() => {
const state = new UserRelatedState(AllKnownLayouts.allKnownLayouts.get("bookcases"))
const state = new UserRelatedState(AllKnownLayouts.allKnownLayouts.get("bookcases"), undefined)
const feature = {
"type": "Feature",
"id": "node/5568693115",