Skip to content

Commit

Permalink
Support importing an osmChange file
Browse files Browse the repository at this point in the history
  • Loading branch information
k-yle committed Aug 12, 2024
1 parent ba8d52e commit 675e39f
Show file tree
Hide file tree
Showing 20 changed files with 733 additions and 5 deletions.
1 change: 0 additions & 1 deletion config/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export default [
'indent': ['off', 4],
'keyword-spacing': 'error',
'linebreak-style': ['error', 'unix'],
'no-await-in-loop': 'error',
'no-caller': 'error',
'no-catch-shadow': 'error',
'no-console': 'warn',
Expand Down
13 changes: 13 additions & 0 deletions css/80_app.css
Original file line number Diff line number Diff line change
Expand Up @@ -135,19 +135,29 @@ em {
strong {
font-weight: bold;
}

.button-link,
a,
a:visited,
a:active {
color: #7092ff;
}
.button-link:focus,
a:focus {
color: #597be7;
}
@media (hover: hover) {
.button-link:hover,
a:hover {
color: #597be7;
}
}

.button-link {
display: flex;
align-items: center;
}

kbd {
display: inline-block;
text-align: center;
Expand Down Expand Up @@ -405,6 +415,9 @@ button.secondary-action:focus,
button.secondary-action:active {
background: #cccccc;
}
button.secondary-action.small {
padding: 2px 5px;
}

button.action.disabled,
button[disabled].action {
Expand Down
8 changes: 8 additions & 0 deletions data/core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,14 @@ en:
area: This feature can't follow the area because it is only connected at a single point. Add another point manually to continue.
generic: This feature can't follow the other feature because they are only connected at a single point. Add another point manually to continue.
unknown: This feature can't follow another feature.
import_from_file:
title: Load edits from file
tooltip: Import an osmChange file or osmPatch file into the editor.
loading: Importing file…
error: Failed to import file.
annotation:
osmChange: Imported an osmChange file
osmPatch: Imported an osmPatch file
reflect:
title:
long: Flip Long
Expand Down
6 changes: 6 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"downlevelIteration": true,
"types": ["modules/global.d.ts"]
}
}
156 changes: 156 additions & 0 deletions modules/actions/import_osmChange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// @ts-check
/** @import * as Osm from 'osm-api' */
import { osmNode, osmRelation, osmWay } from '../osm';
import { actionAddEntity } from './add_entity';
import { actionChangeTags } from './change_tags';
import { actionDeleteNode } from './delete_node';
import { actionDeleteRelation } from './delete_relation';
import { actionDeleteWay } from './delete_way';
import { actionMoveNode } from './move_node';
import { actionReplaceRelationMembers } from './replace_relation_members';
import { actionReplaceWayNodes } from './replace_way_nodes';

/**
* A map of the IDs from the osmPatch file to the IDs in our graph.
* @typedef {Record<Osm.OsmFeatureType, Record<number, string>>} IDMap
*/

/**
* @param {Osm.OsmFeatureType} type
* @param {number} id
* @param {IDMap} idMap
*/
const getId = (type, id, idMap) => {
const mappedId = id > 0 ? type[0] + id : idMap[type][id];
if (mappedId === undefined) {
throw new Error(`No entry in idMap for ${type} ${id}`);
}
return mappedId;
};

/**
* @param {Osm.OsmChange} osmChange
* @param {boolean} allowConflicts
*/
export function actionImportOsmChange(osmChange, allowConflicts) {
/** @param {iD.Graph} graph */
return (graph) => {
/** @type {IDMap} */
const idMap = { node: {}, way: {}, relation: {} };

// check that the versions from the osmChange file match the
// versions in our graph. If not, there are conflicts.
if (!allowConflicts) {
for (const feature of [...osmChange.modify, ...osmChange.delete]) {
const entity = graph.entity(getId(feature.type, feature.id, idMap));
if (+entity.version !== feature.version) {
throw new Error('Conflicts');
}
}
}

// create placeholders in the graph for all new features, so
// that all new features are allocated an ID.
for (const feature of osmChange.create) {
switch (feature.type) {
case 'node': {
const entity = osmNode({
tags: feature.tags,
loc: [feature.lon, feature.lat],
});
idMap[feature.type][feature.id] = entity.id;
graph = actionAddEntity(entity)(graph);
break;
}

case 'way': {
const entity = osmWay({
tags: feature.tags,
// `nodes` are added later, once an ID has
// been allocated to all new features
nodes: [],
});
idMap[feature.type][feature.id] = entity.id;
graph = actionAddEntity(entity)(graph);
break;
}

case 'relation': {
const entity = osmRelation({
tags: feature.tags,
// `members` are added later, once an ID has
// been allocated to all new features
members: [],
});
idMap[feature.type][feature.id] = entity.id;
graph = actionAddEntity(entity)(graph);
break;
}

default:
// eslint-disable-next-line no-unused-expressions -- exhaustivity check
/** @satisfies {never} */ (feature);
}
}

// loop through the `create` features again, to set the loc/nodes/members
// we can also handle the `modify` features at the same time.
for (const feature of [...osmChange.create, ...osmChange.modify]) {
const entityId = getId(feature.type, feature.id, idMap);

// firstly, change tags
graph = actionChangeTags(entityId, feature.tags)(graph);

// secondly, change loc/nodes/members
switch (feature.type) {
case 'node':
graph = actionMoveNode(entityId, [feature.lon, feature.lat])(graph);
break;

case 'way': {
const newNodes = feature.nodes.map((id) => getId('node', id, idMap));
graph = actionReplaceWayNodes(entityId, newNodes)(graph);
break;
}

case 'relation': {
const newMembers = feature.members.map((member) => ({
id: getId(member.type, member.ref, idMap),
role: member.role,
type: member.type,
}));
graph = actionReplaceRelationMembers(entityId, newMembers)(graph);
break;
}

default:
// eslint-disable-next-line no-unused-expressions -- exhaustivity check
/** @satisfies {never} */ (feature);
}
}

// delete
for (const feature of osmChange.delete) {
const entityId = getId(feature.type, feature.id, idMap);
switch (feature.type) {
case 'node':
graph = actionDeleteNode(entityId)(graph);
break;

case 'way':
graph = actionDeleteWay(entityId)(graph);
break;

case 'relation':
graph = actionDeleteRelation(entityId)(graph);
break;

default:
// eslint-disable-next-line no-unused-expressions -- exhaustivity check
/** @satisfies {never} */ (feature);
}
}

return graph;
};
}
Loading

0 comments on commit 675e39f

Please sign in to comment.