/** * A collection of functions to create commands which can be applied, undone * and redone on {@class org.openstreetmap.josm.gui.layer.OsmDataLayer}s. * * @module josm/command * @example * import { * buildAddCommand, * buildChangeCommand, * buildDeleteCommand * } from 'josm/command' */ /* global Java */ /* global Plugin */ const AddMultiCommand = Plugin.type('org.openstreetmap.josm.plugins.scripting.js.api.AddMultiCommand') const ChangeMultiCommand = Plugin.type('org.openstreetmap.josm.plugins.scripting.js.api.ChangeMultiCommand') const Change = Plugin.type('org.openstreetmap.josm.plugins.scripting.js.api.Change') import * as util from 'josm/util' import layers from 'josm/layers' const OsmPrimitive = Java.type('org.openstreetmap.josm.data.osm.OsmPrimitive') const OsmDataLayer = Java.type('org.openstreetmap.josm.gui.layer.OsmDataLayer') const Layer = Java.type('org.openstreetmap.josm.gui.layer.Layer') const UndoRedoHandler = Java.type('org.openstreetmap.josm.data.UndoRedoHandler') const CombineWayAction = Java.type('org.openstreetmap.josm.actions.CombineWayAction') const JavaDeleteCommand = Java.type('org.openstreetmap.josm.command.DeleteCommand') const LatLon = Java.type('org.openstreetmap.josm.data.coor.LatLon') const RelationMember = Java.type('org.openstreetmap.josm.data.osm.RelationMember') const ArrayList = Java.type('java.util.ArrayList') const Map = Java.type('java.util.Map') const HashMap = Java.type('java.util.HashMap') const HashSet = Java.type('java.util.HashSet') const Collection = Java.type('java.util.Collection') const Command = Java.type('org.openstreetmap.josm.command.Command') const System = Java.type('java.lang.System') function checkAndFlatten (primitives) { const ret = new HashSet() function visit (value) { if (util.isNothing(value)) return if (util.isCollection(value)) { util.each(value, visit) } else if (value instanceof OsmPrimitive) { ret.add(value) } else { util.assert(false, 'Unexpected object to add as OSM primitive, got {0}', value) } } visit(primitives) return ret } function toArray (collection) { if (util.isArray(collection)) return collection if (collection instanceof Collection) { const ret = [] for (const it = collection.iterator(); it.hasNext();) ret.push(it.next()) return ret } } class AbstractCommand { /** * Applies the command to a layer. * * @param {org.openstreetmap.josm.gui.layer.OsmDataLayer} layer the data layer */ applyTo(layer) { util.assert(util.isSomething(layer), 'layer: must not be null or undefined') util.assert(layer instanceof OsmDataLayer, 'layer: expected OsmDataLayer, got {0}', layer) const cmd = this.createJOSMCommand(layer) try { layer.getDataSet().beginUpdate() UndoRedoHandler.getInstance().add(cmd) } finally { layer.getDataSet().endUpdate() } } ensureOsmDataLayer(layer) { util.assert(util.isSomething(layer), 'layer: must not be null or undefined') util.assert(layer instanceof OsmDataLayer, 'layer: expected OsmDataLayer, got {0}', layer) } } /** * A command to add a collection of objects to a data layer. * * @param { java.util.Collection| array } objs the objects to add */ export class AddCommand extends AbstractCommand { constructor(objs) { super() util.assert(objs, 'objs: mandatory parameter missing') this._objs = toArray(checkAndFlatten(objs)) } /** * Creates the internal JOSM command for this command * * @param {org.openstreetmap.josm.gui.layer.OsmDataLayer} layer the data layer * @returns {org.openstreetmap.josm.command.Command} the command */ createJOSMCommand(layer) { super.ensureOsmDataLayer(layer) const list = new ArrayList() this._objs.forEach(obj => list.add(obj)) return new AddMultiCommand(layer, list) } } /** * Creates a command to add a collection of objects to a data layer. * * <strong>Signatures</strong> * <dl> * <dt><code class="signature">add(obj, obj, ...)</code> </dt> * <dd class="param-desc"><code>obj</code> are {@class org.openstreetmap.josm.data.osm.Node}s, * {@class org.openstreetmap.josm.data.osm.Way}s, or * {@class org.openstreetmap.josm.data.osm.Relations}s. Or javascript array * or Java collections thereof.</dd> * </dl> * * @example * import {buildAddCommand} from 'josm/command' * import layers from 'josm/layer' * import {NodeBuilder} from 'josm/builder' * const layer = layers.get('Data Layer 1') * * // add two nodes * buildAddCommand( * NodeBuilder.create(), * NodeBuilder.create() * ).applyTo(layer) * * @param {...(org.openstreetmap.josm.data.osm.OsmPrimitive | org.openstreetmap.josm.data.osm.OsmPrimitive[] | java.lang.Collection )} obj the primitives to add * @returns {module:josm/command.AddCommand} the command object */ export function buildAddCommand(){ const objs = toArray(checkAndFlatten(arguments)) return new AddCommand(objs) } /** * A command to delete a collection of objects in a data layer. * * @param {java.util.Collection|array} objs the objects to add */ export class DeleteCommand extends AbstractCommand { constructor(objs) { super() this._objs = toArray(checkAndFlatten(objs)) } /** * Creates the internal JOSM command for this command * * @param {org.openstreetmap.josm.gui.layer.OsmDataLayer} layer the data layer * @returns {org.openstreetmap.josm.command.Command} the command object */ createJOSMCommand(layer) { super.ensureOsmDataLayer(layer) const list = new ArrayList() this._objs.forEach(obj => list.add(obj)) return JavaDeleteCommand.delete(list, true /* alsoDeleteNodesInWay */, true /* silent */) } } /** * Creates a command to delete a collection of objects in a data layer. * * @example * import {buildDeleteCommand} from 'josm/command' * import layers from 'josm/layer' * import {NodeBuilder} from 'josm/builder' * const layer = layers.get('Data Layer 1') * // delete two nodes * buildDeleteCommand(NodeBuilder.create(),NodeBuilder.create()).applyTo(layer) * * * @param {...(org.openstreetmap.josm.data.osm.OsmPrimitive | org.openstreetmap.josm.data.osm.OsmPrimitive[] | java.lang.Collection )} obj the primitives to delete * @returns {module:josm/command.DeleteCommand} the command object */ export function buildDeleteCommand() { return new DeleteCommand(toArray(checkAndFlatten(arguments))) } function scheduleLatChangeFromPara (para, change) { if (!para || !util.isDef(para.lat)) return util.assert(util.isNumber(para.lat), 'lat: lat must be a number, got {0}', para.lat) util.assert(LatLon.isValidLat(para.lat), 'lat: expected a valid lat, got {0}', para.lat) change.withLatChange(para.lat) } function scheduleLonChangeFromPara (para, change) { if (!para || !util.isDef(para.lon)) return util.assert(util.isNumber(para.lon), 'lon: lon must be a number, got {0}', para.lon) util.assert(LatLon.isValidLon(para.lon), 'lon: expected a valid lon, got {0}', para.lon) change.withLonChange(para.lon) } function buildLatLon (obj) { util.assert(util.isSomething(obj), 'obj: must not be null or undefined') util.assert(typeof obj === 'object', 'obj: expected an object, got {0}', obj) util.assert(util.isNumber(obj.lat), 'obj.lat: expected a number, got {0}', obj.lat) util.assert(util.isNumber(obj.lon), 'obj.lon: expected a number, got {0}', obj.lon) util.assert(LatLon.isValidLat(obj.lat), 'obj.lat: expected a valid lat in the range [-90,90], got {0}', obj.lat) util.assert(LatLon.isValidLon(obj.lon), 'obj.lon: expected a valid lon in the range [-180,180], got {0}', obj.lon) return new LatLon(obj.lat, obj.lon) } function schedulePosChangeFromPara (para, change) { if (!para || !util.isDef(para.pos)) return util.assert(para.pos, 'pos must no be null') let pos = para.pos if (pos instanceof LatLon) { // OK } else if (typeof pos === 'object') { pos = buildLatLon(pos) } else { util.assert(false, 'pos: unexpected value, expected LatLon or object, got {0}', pos) } change.withPosChange(pos) } function scheduleNodeChangeFromPara (para, change) { if (!para || !util.isDef(para.nodes)) return // convert to a Java List ... const l = new ArrayList() for (let i = 0; i < para.nodes.length; i++) { l.add(para.nodes[i]) } // ... and pass it to the change command change.withNodeChange(l) } function scheduleMemberChangeFromPara (para, change) { if (!para || !util.isDef(para.members)) return const l = new ArrayList() if (para.members instanceof RelationMember) { l.add(para.members) } else if (para.members instanceof Collection) { l.addAll(para.members) } else if (util.isArray(para.members)) { for (let i = 0; i < para.members.length; i++) { l.add(para.members[i]) } } else { util.assert(false, 'Expected RelationMember, array or collection ' + 'of RelationMembers, got {0}', para.members) } change.withMemberChange(l) } function scheduleTagsChangeFromPara (para, change) { if (!para || !util.isDef(para.tags)) return util.assert(para.tags, 'tags must no be null') let tags = para.tags if (tags instanceof Map) { // OK } else if (typeof tags === 'object') { const map = new HashMap() for (let key in tags) { if (!util.hasProp(tags, key)) continue const value = tags[key] key = util.trim(key) map.put(key, value) } tags = map } else { util.assert(false, 'tags: unexpected value, expected Map or object, got {0}', tags) } change.withTagsChange(tags) } function changeFromParameters (para) { const change = new Change() scheduleLatChangeFromPara(para, change) scheduleLonChangeFromPara(para, change) schedulePosChangeFromPara(para, change) scheduleTagsChangeFromPara(para, change) scheduleNodeChangeFromPara(para, change) scheduleMemberChangeFromPara(para, change) return change } /** * A command to change a collection of objects in a data layer. * * @param {java.util.Collection|array} objs the objects to change * @param {org.openstreetmap.josm.plugins.scripting.js.api.Change} change the change specification */ export class ChangeCommand extends AbstractCommand { constructor(objs, change) { super() this._objs = toArray(checkAndFlatten(objs)) this._change = change } /** * Creates the internal JOSM command for this command * * @param {org.openstreetmap.josm.gui.layer.OsmDataLayer} layer the data layer * @returns {org.openstreetmap.josm.command.Command} the command object */ createJOSMCommand(layer) { super.ensureOsmDataLayer(layer) const list = new ArrayList() this._objs.forEach(obj => list.add(obj)) return new ChangeMultiCommand(layer, list, this._change) } } /** * A lat/lon position as a JavaScript object. * * @typedef {Object} LatLonSpec * @property {number} lat the latitude of the position * @property {number} lon the longitude of the position * @example * const latLonSpec = { * lat: 1.0, * lon: 1.0 * } */ /** * The change specification for a change command. * * @typedef {Object} ChangeSpec * @property {number} lat if present and applied to a node, changes the nodes latitude * @property {number} lon if present and applied to a node, changes the nodes longitude * @property {org.openstreetmap.josm.data.coor.LatLon| module:josm/command~LatLonSpec} pos if present and applied to a node, * changes the nodes position * @property {java.util.Map | object} tags if present, changes the tags of the target object * @property {java.util.List | org.openstreetmap.josm.data.osm.OsmPrimitive[]} nodes if present and applied to a way, changes * the ways nodes * @property {java.util.List | org.openstreetmap.josm.data.osm.RelationMember[]} nodes if present and applied to a relation, changes * the relations members * * @example * // change the positon of a node * const changeSpec1 = { * lat: 1.0, * lon: 2.0 * } * * // change the tags of one or more primitives * const changeSpec2 = { * tags: { * amentity: 'restaurant' * } * } */ /** * Creates a command to change a collection of objects in a data layer. * * The mandatory last argument is an object with named parameters. * * @example * import {buildChangeCommand} from 'josm/command' * import layers from 'josm/layers' * const layer = layers.get("Data Layer 1") * * // change the position of a node * buildChangeCommand(n1, {lat: 123.45, lon: 44.234}).applyTo(layer) * * // change the tags of a collection of primitives * buildChangeCommand(n1, n3, w1, r1, { * tags: {'mycustomtag': 'value'} * }).applyTo(layer) * * @returns {module:josm/command.ChangeCommand} the change command object * @param {...(org.openstreetmap.josm.data.osm.OsmPrimitive | org.openstreetmap.josm.data.osm.OsmPrimitive[] | java.lang.Collection )} objs the objects to change. See documentation. * @param {module:josm/command~ChangeSpec} change the change specification */ export function buildChangeCommand() { let objs = [] let change switch (arguments.length) { case 0: util.assert(false, 'Unexpected number of arguments, got {0} arguments', arguments.length) break default: { const a = arguments[arguments.length - 1] if (a instanceof OsmPrimitive) { util.assert(false, 'Argument {0}: unexpected last argument, expected named ' + 'parameters, got {0}', a) } else if (typeof a === 'object') { // last argument is an object with named parameters objs = Array.prototype.slice.call(arguments, 0, -1) change = changeFromParameters(a) } else { util.assert(false, 'Argument {0}: unexpected type of value, got {1}', arguments.length - 1, a) } } } const tochange = checkAndFlatten(objs) return new ChangeCommand(tochange, change) } /** * Accessor to the global command history. * <p> * Provides static methods to redo and undo commands. * * @summary Accessor to the global command history */ export class CommandHistory { /** * Undoes the last <code>depth</code> commands. * * @param {number} [depth=1] the number of commands to be undone */ static undo(depth) { if (util.isDef(depth)) { util.assert(util.isNumber(depth), 'depth: expected a number, got {0}', depth) util.assert(depth > 0, 'depth: expected number > 0, got {0}', depth) } const undoRedoHandler = UndoRedoHandler.getInstance() if (depth) { undoRedoHandler.undo(depth) } else { undoRedoHandler.undo() } } /** * Redoes the last <code>depth</code> commands. * * @param {number} [depth=1] the number of commands to be redone. */ static redo(depth) { if (util.isDef(depth)) { util.assert(util.isNumber(depth), 'depth: expected a number, got {0}', depth) util.assert(depth > 0, 'depth: expected number > 0, got {0}', depth) } const undoRedoHandler = UndoRedoHandler.getInstance() if (depth) { undoRedoHandler.redo(depth) } else { undoRedoHandler.redo() } } /** * Removes commands in the command history, either all commands, or only the * commands applied to a specific layer. * * @param {org.openstreetmap.josm.gui.layer.Layer} [layer] the * reference layer. Only commands applied to this layer are removed. Default * if missing: <strong>all</strong> commands are removed. */ static clear(layer) { const undoRedoHandler = UndoRedoHandler.getInstance() function clearAll () { undoRedoHandler.clean() } function clearForLayer (layer) { undoRedoHandler.clean(layer) } switch (arguments.length) { case 0: clearAll(); break case 1: { const layer = arguments[0] util.assert(layer instanceof Layer, 'Expected a Layer, got {0}', layer) clearForLayer(layer) break } default: util.assert(false, 'Unexpected number of arguments') } } } /** * Combines two or more ways into one resulting way. * <p> * Reuses the logic behind the JOSM standard menu entry Tools->Combine Ways. * If invoked from a script, this may trigger modal dialogs which are presented * to the user, in particular if the direction of the ways has to be reversed * because otherwise they could not be combined. * * @param ways the ways to be combined * @example * import {combineWays} from 'josm/command' * import layers from 'josm/layer' * const ds = layers.activeLayer.data * const ways = [ds.way(1), ds.way(2), ds.way(3)] * * // pass in an array ... * combineWays(ways) * // ... or the individual ways ... * combineWays(ds.way(1), ds.way(2), ds.way(3)) * // ... or any combination thereof. * * @summary Combines two or more ways into one resulting way. * @param {...org.openstreetmap.josm.data.osm.Way | array} ways the ways to be combined * @static */ export function combineWays() { // ways becomes a java.util.HashSet const ways = checkAndFlatten(arguments) // remove any primitives which are not nodes from the arguments const it = ways.iterator() while (it.hasNext()) { const primitive = it.next() if (primitive == null || !primitive.isWay) { it.remove() } } // at least two remaining ways required to combine them. If less, just // return, don't throw if (ways.size() <= 1) return const activeLayer = layers.activeLayer if (activeLayer == null) return const ret = CombineWayAction.combineWaysWorker(ways) // happens, if combineWayWorkers presents a modal dialog and the user // aborts it if (ret == null) return // ret.b is the SequenceCommand which combines the ways into one // resulting ways. Apply this command to the active layer. activeLayer.apply(ret.b) } /** * Combines the currently selected ways in the active layer into one resulting * way. * * Returns without effect if * <ul> * <li>there is no active layer</li> * <li>the active layer is not a data layer</li> * <li>there are less than two selected ways in the active layer</li> * </ul> * * Reuses the logic behind the JOSM standard menu entry Tools->Combine Ways. * If invoked from a script, this may trigger modal dialogs which are presented * to the user, in particular if the direction of the ways has to be reversed * because otherwise they could not be combined. * * @example * import {combineSelectedWays} from 'josm/command' * import layers from 'josm/layer' * const ds = layers.activeLayer.data * combineSelectedWays(ways) * * @summary Combines the currently selected ways. * @static */ export function combineSelectedWays() { const activeLayer = layers.activeLayer if (activeLayer == null) return const ways = activeLayer.data.selection.ways if (ways == null || ways.length <= 1) return combineWays(ways) }