Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle pending node dependencies #282

Merged
merged 8 commits into from
Jun 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions __tests__/utils/__snapshots__/edit-to-osm-change.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`test function to convert edit JSON to osm-change XML for a single feature should create a modify action 1`] = `"<osmChange><modify><node id=\\"1\\" changeset=\\"123\\" version=\\"2\\" lon=\\"-2\\" lat=\\"2\\"><tag k=\\"building\\" v=\\"no\\"/></node></modify></osmChange>"`;
exports[`test function to convert edit JSON to osm-change XML for a single feature should create a modify action 1`] = `
Object {
"nodeIdMap": null,
"osmChangeXML": "<osmChange><modify><node id=\\"1\\" changeset=\\"123\\" version=\\"2\\" lon=\\"-2\\" lat=\\"2\\"><tag k=\\"building\\" v=\\"no\\"/></node></modify></osmChange>",
}
`;

exports[`test function to convert edit JSON to osm-change XML for a single feature should create a simple create action 1`] = `"<osmChange><create><node id=\\"-1\\" changeset=\\"123\\" version=\\"1\\" lon=\\"-1\\" lat=\\"1\\"><tag k=\\"building\\" v=\\"yes\\"/></node></create></osmChange>"`;
exports[`test function to convert edit JSON to osm-change XML for a single feature should create a simple create action 1`] = `
Object {
"nodeIdMap": null,
"osmChangeXML": "<osmChange><create><node id=\\"-1\\" changeset=\\"123\\" version=\\"1\\" lon=\\"-1\\" lat=\\"1\\"><tag k=\\"building\\" v=\\"yes\\"/></node></create></osmChange>",
}
`;

exports[`test function to convert edit JSON to osm-change XML for a single feature should generate XML for delete actions correctly 1`] = `"<osmChange><delete if-unused=\\"true\\"><node id=\\"1\\" changeset=\\"123\\" version=\\"2\\" lon=\\"-1\\" lat=\\"1\\"><tag k=\\"building\\" v=\\"yes\\"/></node></delete></osmChange>"`;
exports[`test function to convert edit JSON to osm-change XML for a single feature should generate XML for delete actions correctly 1`] = `
Object {
"nodeIdMap": null,
"osmChangeXML": "<osmChange><delete if-unused=\\"true\\"><node id=\\"1\\" changeset=\\"123\\" version=\\"2\\" lon=\\"-1\\" lat=\\"1\\"><tag k=\\"building\\" v=\\"yes\\"/></node></delete></osmChange>",
}
`;

exports[`test function to convert edit JSON to osm-change XML for a single feature should modify a way with tags correctly 1`] = `"<osmChange><modify><way id=\\"1\\" changeset=\\"123\\" version=\\"3\\"><tag k=\\"highway\\" v=\\"tertiary\\"/><tag k=\\"name\\" v=\\"bar\\"/><nd ref=\\"1\\"/><nd ref=\\"4\\"/><nd ref=\\"7\\"/></way></modify></osmChange>"`;
exports[`test function to convert edit JSON to osm-change XML for a single feature should modify a way with tags correctly 1`] = `
Object {
"nodeIdMap": null,
"osmChangeXML": "<osmChange><modify><way id=\\"1\\" changeset=\\"123\\" version=\\"3\\"><tag k=\\"highway\\" v=\\"tertiary\\"/><tag k=\\"name\\" v=\\"bar\\"/><nd ref=\\"1\\"/><nd ref=\\"4\\"/><nd ref=\\"7\\"/></way></modify></osmChange>",
}
`;
1 change: 1 addition & 0 deletions app/actions/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const CLEAR_EDIT = 'CLEAR_EDIT'
export const CLEAR_UPLOADED_EDITS = 'CLEAR_UPLOADED_EDITS'
export const PURGE_ALL_EDITS = 'PURGE_ALL_EDITS'
export const SET_EDIT_STATUS = 'SET_EDIT_STATUS'
export const NEW_NODE_MAPPING = 'NEW_NODE_MAPPING'

export const MODIFY_EDIT_VERSION = 'MODIFY_EDIT_VERSION'
export const MODIFY_EDIT_TO_CREATE = 'MODIFY_EDIT_TO_CREATE'
Expand Down
9 changes: 8 additions & 1 deletion app/actions/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,19 @@ export function uploadEdits (editIds) {
}
dispatch(startEditUpload(edit))
try {
const changesetId = await uploadEdit(edit)
const { changesetId, newNodeIdMap } = await uploadEdit(edit)
dispatch(editUploaded(edit, changesetId))
const feature = edit.type === 'delete' ? edit.oldFeature : edit.newFeature

// update the list of modified tiles with ones that touch the feature being uploaded
featureToTiles(feature).forEach(modifiedTiles.add, modifiedTiles)

if (newNodeIdMap) {
dispatch({
'type': types.NEW_NODE_MAPPING,
newNodeIdMap
})
}
} catch (e) {
console.warn('Upload failed', e, edit)
dispatch(setNotification({ level: 'error', message: 'Upload failed' }))
Expand Down
9 changes: 8 additions & 1 deletion app/actions/wayEditing.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,14 @@ export function findNearestFeatures (node) {
}

if (!nearest || !nearest.nearestNode) {
const visibleFeatures = getVisibleFeatures(getState())
const mapFeatures = getVisibleFeatures(getState())
const pendingFeatures = getState().edit.editsGeojson
let visibleFeatures
if (pendingFeatures.features.length) {
visibleFeatures = featureCollection(mapFeatures.features.concat(pendingFeatures.features))
} else {
visibleFeatures = featureCollection(mapFeatures.features)
}
nearest = await findNearest(node, visibleFeatures)
}

Expand Down
101 changes: 101 additions & 0 deletions app/reducers/edit.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as types from '../actions/actionTypes'
import editsToGeojson from '../utils/edits-to-geojson'
import _cloneDeep from 'lodash.clonedeep'

const initialState = {
edits: [], // array of edit actions
Expand Down Expand Up @@ -261,6 +262,106 @@ export default function (state = initialState, action) {
addPointGeometry: action.geometry
}
}

case types.NEW_NODE_MAPPING: {
let edits = [...state.edits]

// go through each edit
// replace the observeid with new id
if (edits.length) {
edits.forEach(edit => {
const feature = _cloneDeep(edit.newFeature)
if (feature) {
const newNdrefs = []
const addedNodes = []
const deletedNodes = []
const mergedNodes = []
const movedNodes = []
const nodes = []

const newNodeIdMap = action.newNodeIdMap
const newNodeIdMapKeys = Object.keys(newNodeIdMap)

// replace it in ndrefs of the way
feature.properties.ndrefs.map(oldRef => {
let newRef = oldRef
if (newNodeIdMapKeys.includes(oldRef)) {
newRef = action.newNodeIdMap[oldRef]
}
newNdrefs.push(newRef)
}, newNdrefs)

feature.properties.ndrefs = newNdrefs

// replace it in addedNodes, deletedNodes, movedNodes, mergedNodes
if (feature.wayEditingHistory.addedNodes.length) {
feature.wayEditingHistory.addedNodes.map(oldRef => {
let newRef = oldRef
if (newNodeIdMapKeys.includes(oldRef)) {
newRef = action.newNodeIdMap[oldRef]
}
addedNodes.push(newRef)
}, addedNodes)
}
feature.wayEditingHistory.addedNodes = addedNodes

if (feature.wayEditingHistory.movedNodes.length) {
feature.wayEditingHistory.movedNodes.map(oldRef => {
let newRef = oldRef
if (newNodeIdMapKeys.includes(oldRef)) {
newRef = action.newNodeIdMap[oldRef]
}
movedNodes.push(newRef)
}, movedNodes)
}
feature.wayEditingHistory.movedNodes = movedNodes

if (feature.wayEditingHistory.deletedNodes.length) {
feature.wayEditingHistory.deletedNodes.map(oldRef => {
let newRef = oldRef
if (newNodeIdMapKeys.includes(oldRef)) {
newRef = action.newNodeIdMap[oldRef]
}
deletedNodes.push(newRef)
}, deletedNodes)
}
feature.wayEditingHistory.deletedNodes = deletedNodes

if (feature.wayEditingHistory.mergedNodes.length) {
feature.wayEditingHistory.mergedNodes.map(oldRef => {
const thisMerge = oldRef
if (newNodeIdMapKeys.includes(oldRef.sourceNode)) {
thisMerge.sourceNode = action.newNodeIdMap[oldRef]
}

if (newNodeIdMapKeys.includes(oldRef.destinationNode)) {
thisMerge.destinationNode = action.newNodeIdMap[oldRef]
}
mergedNodes.push(thisMerge)
}, mergedNodes)
}
feature.wayEditingHistory.mergedNodes = mergedNodes

// replace it in wayEditHistory.way.nodes
feature.wayEditingHistory.way.nodes.forEach(oldNode => {
const thisNode = _cloneDeep(oldNode)
if (newNodeIdMapKeys.includes(oldNode.properties.id)) {
thisNode.id = action.newNodeIdMap[oldNode.properties.id]
thisNode.properties.id = action.newNodeIdMap[oldNode.properties.id]
thisNode.properties.version = 1
}
nodes.push(thisNode)
})
feature.wayEditingHistory.way.nodes = nodes
}
edit.newFeature = feature
})
}
return {
...state,
edits
}
}
}
return state
}
3 changes: 2 additions & 1 deletion app/services/osm-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ export async function uploadOsmChange (changeXML, changesetId) {
}
throw new ChangesetNotFoundError(undefined, responseText)
default:
return responseText
const jsonResponse = await xml2jsonPromise(responseText)
return jsonResponse
}
}

Expand Down
17 changes: 14 additions & 3 deletions app/utils/edit-to-osm-change.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ import _get from 'lodash.get'
export default function (edit, changesetId, memberNodes = null) {
console.log('edit', edit)
const isSimpleChange = !(edit.newFeature && edit.newFeature.wayEditingHistory)
return isSimpleChange ? getSimpleChange(edit, changesetId, memberNodes) : getComplexChange(edit, changesetId)
if (isSimpleChange) {
return {
osmChangeXML: getSimpleChange(edit, changesetId, memberNodes),
nodeIdMap: null
}
} else {
return getComplexChange(edit, changesetId)
}
}

/**
Expand Down Expand Up @@ -100,7 +107,7 @@ function getComplexChange (edit, changesetId) {
const node = wayEditingHistory.way.nodes.find(nd => nd.properties.id === addedNodeId)
console.log('node', node)
const id = node.properties.id
if (!creates.find(create => create.id === nodeIdMap[id])) {
if (isNewId(id) && !creates.find(create => create.id === nodeIdMap[id])) {
creates.push({
type: 'node',
id: nodeIdMap[id],
Expand Down Expand Up @@ -189,7 +196,11 @@ function getComplexChange (edit, changesetId) {
}
}

return getXMLForChanges({ creates, modifies, deletes }, changesetId)
const XMLForChanges = getXMLForChanges({ creates, modifies, deletes }, changesetId)
return {
osmChangeXML: XMLForChanges,
nodeIdMap: nodeIdMap
}
}

function getXMLForChanges ({ creates, modifies, deletes }, changesetId) {
Expand Down
27 changes: 21 additions & 6 deletions app/utils/nearest.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import turfPointToLineDistance from '@turf/point-to-line-distance'
import turfDistance from '@turf/distance'
import turfNearestPointOnLine from '@turf/nearest-point-on-line'
import { lineString } from '@turf/helpers'
import { lineString, featureCollection } from '@turf/helpers'
import { nodesGeojson } from '../utils/nodes-to-geojson'
import _sortBy from 'lodash.sortby'
import getRandomId from './get-random-id'
import _find from 'lodash.find'

// FIXME: adjust these based on interactions
const threshold = 0.0005
Expand Down Expand Up @@ -106,11 +107,25 @@ function getEdge (polygon) {
}

async function getNodes (feature) {
// get all nodes of this way
const nodeIds = feature.properties.ndrefs.map(n => {
return `node/${n}`
})
const geojson = await nodesGeojson(nodeIds)
let geojson = featureCollection([])
// if this feature is not yet uploaded
// fetch member nodes from within the feature props
if (feature.properties.id.split('/')[1].startsWith('observe')) {
const nodeCollection = feature.wayEditingHistory.way.nodes
feature.properties.ndrefs.forEach(nd => {
const thisNode = _find(nodeCollection, (node) => {
return node.properties.id === nd
})
if (thisNode) geojson.features.push(thisNode)
})
} else {
// get all nodes of this way
const nodeIds = feature.properties.ndrefs.map(n => {
return `node/${n}`
})
geojson = await nodesGeojson(nodeIds)
}

return geojson
}

Expand Down
31 changes: 27 additions & 4 deletions app/utils/upload-edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
} from '../services/osm-api'

import { version } from '../../package.json'
import _find from 'lodash.find'

/**
* Takes a single edit object and creates a changeset for it and uploads it to the OSM API
* @param {Object} edit - edit object with `id`, `oldFeature`, `newFeature`, `type`
Expand All @@ -28,11 +30,32 @@ export default async function uploadEdit (edit) {
let changesetId
changesetId = await createChangeset(changesetXML)
console.log('changeset ID', changesetId)
const osmChangeXML = editToOSMChange(edit, changesetId, memberNodes)
const { osmChangeXML, nodeIdMap } = editToOSMChange(edit, changesetId, memberNodes)
console.log('osm change xml', osmChangeXML)
await uploadOsmChange(osmChangeXML, changesetId)
console.log('osm change uploaded')
const response = await uploadOsmChange(osmChangeXML, changesetId)
console.log('osm change uploaded', response)
let newNodeIdMap = null
await closeChangeset(changesetId)
console.log('changeset closed')
return changesetId
if (nodeIdMap && response && response.hasOwnProperty('diffResult')) {
newNodeIdMap = getNewNodeIds(nodeIdMap, response)
}
return {
changesetId,
newNodeIdMap
}
}

function getNewNodeIds (nodeIdMap, response) {
const mapping = {}
Object.keys(nodeIdMap).forEach(observeNode => {
const match = _find(response.diffResult.node, (osmNode) => {
return osmNode['$'].old_id === String(nodeIdMap[observeNode])
})

if (match && match.$ && match.$.new_id) {
mapping[observeNode] = match.$.new_id
}
})
return mapping
}