| 1 | /**
|
|---|
| 2 | * Terracer: A JOSM Plugin for terraced houses.
|
|---|
| 3 | *
|
|---|
| 4 | * Copyright 2009 CloudMade Ltd.
|
|---|
| 5 | *
|
|---|
| 6 | * Released under the GPLv2, see LICENSE file for details.
|
|---|
| 7 | */
|
|---|
| 8 | package terracer;
|
|---|
| 9 |
|
|---|
| 10 | import static org.openstreetmap.josm.tools.I18n.tr;
|
|---|
| 11 | import static org.openstreetmap.josm.tools.I18n.trn;
|
|---|
| 12 |
|
|---|
| 13 | import java.awt.event.ActionEvent;
|
|---|
| 14 | import java.awt.event.KeyEvent;
|
|---|
| 15 | import java.util.ArrayList;
|
|---|
| 16 | import java.util.Arrays;
|
|---|
| 17 | import java.util.Collection;
|
|---|
| 18 | import java.util.Collections;
|
|---|
| 19 | import java.util.Comparator;
|
|---|
| 20 | import java.util.HashSet;
|
|---|
| 21 | import java.util.Iterator;
|
|---|
| 22 | import java.util.LinkedList;
|
|---|
| 23 | import java.util.List;
|
|---|
| 24 | import java.util.Set;
|
|---|
| 25 | import java.util.regex.Matcher;
|
|---|
| 26 | import java.util.regex.Pattern;
|
|---|
| 27 |
|
|---|
| 28 | import javax.swing.JOptionPane;
|
|---|
| 29 |
|
|---|
| 30 | import org.openstreetmap.josm.Main;
|
|---|
| 31 | import org.openstreetmap.josm.actions.JosmAction;
|
|---|
| 32 | import org.openstreetmap.josm.command.AddCommand;
|
|---|
| 33 | import org.openstreetmap.josm.command.ChangeCommand;
|
|---|
| 34 | import org.openstreetmap.josm.command.ChangePropertyCommand;
|
|---|
| 35 | import org.openstreetmap.josm.command.Command;
|
|---|
| 36 | import org.openstreetmap.josm.command.DeleteCommand;
|
|---|
| 37 | import org.openstreetmap.josm.command.SequenceCommand;
|
|---|
| 38 | import org.openstreetmap.josm.data.osm.Node;
|
|---|
| 39 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
|---|
| 40 | import org.openstreetmap.josm.data.osm.Relation;
|
|---|
| 41 | import org.openstreetmap.josm.data.osm.RelationMember;
|
|---|
| 42 | import org.openstreetmap.josm.data.osm.Tag;
|
|---|
| 43 | import org.openstreetmap.josm.data.osm.TagCollection;
|
|---|
| 44 | import org.openstreetmap.josm.data.osm.Way;
|
|---|
| 45 | import org.openstreetmap.josm.gui.ExtendedDialog;
|
|---|
| 46 | import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
|
|---|
| 47 | import org.openstreetmap.josm.tools.Pair;
|
|---|
| 48 | import org.openstreetmap.josm.tools.Shortcut;
|
|---|
| 49 | import org.openstreetmap.josm.tools.UserCancelException;
|
|---|
| 50 |
|
|---|
| 51 | /**
|
|---|
| 52 | * Terraces a quadrilateral, closed way into a series of quadrilateral,
|
|---|
| 53 | * closed ways. If two ways are selected and one of them can be identified as
|
|---|
| 54 | * a street (highway=*, name=*) then the given street will be added
|
|---|
| 55 | * to the 'associatedStreet' relation.
|
|---|
| 56 | *
|
|---|
| 57 | *
|
|---|
| 58 | * At present it only works on quadrilaterals, but there is no reason
|
|---|
| 59 | * why it couldn't be extended to work with other shapes too. The
|
|---|
| 60 | * algorithm employed is naive, but it works in the simple case.
|
|---|
| 61 | *
|
|---|
| 62 | * @author zere
|
|---|
| 63 | */
|
|---|
| 64 | public final class TerracerAction extends JosmAction {
|
|---|
| 65 |
|
|---|
| 66 | // smsms1 asked for the last value to be remembered to make it easier to do
|
|---|
| 67 | // repeated terraces. this is the easiest, but not necessarily nicest, way.
|
|---|
| 68 | // private static String lastSelectedValue = "";
|
|---|
| 69 |
|
|---|
| 70 | Collection<Command> commands;
|
|---|
| 71 |
|
|---|
| 72 | private Collection<OsmPrimitive> primitives;
|
|---|
| 73 | private TagCollection tagsInConflict;
|
|---|
| 74 |
|
|---|
| 75 | public TerracerAction() {
|
|---|
| 76 | super(tr("Terrace a building"), "terrace",
|
|---|
| 77 | tr("Creates individual buildings from a long building."),
|
|---|
| 78 | Shortcut.registerShortcut("tools:Terracer", tr("Tool: {0}",
|
|---|
| 79 | tr("Terrace a building")), KeyEvent.VK_T,
|
|---|
| 80 | Shortcut.SHIFT), true);
|
|---|
| 81 | }
|
|---|
| 82 |
|
|---|
| 83 | protected static final Set<Relation> findAssociatedStreets(Collection<OsmPrimitive> objects) {
|
|---|
| 84 | Set<Relation> result = new HashSet<>();
|
|---|
| 85 | if (objects != null) {
|
|---|
| 86 | for (OsmPrimitive c : objects) {
|
|---|
| 87 | if (c != null) {
|
|---|
| 88 | for (OsmPrimitive p : c.getReferrers()) {
|
|---|
| 89 | if (p instanceof Relation && "associatedStreet".equals(p.get("type"))) {
|
|---|
| 90 | result.add((Relation) p);
|
|---|
| 91 | }
|
|---|
| 92 | }
|
|---|
| 93 | }
|
|---|
| 94 | }
|
|---|
| 95 | }
|
|---|
| 96 | return result;
|
|---|
| 97 | }
|
|---|
| 98 |
|
|---|
| 99 | private static final class InvalidUserInputException extends Exception {
|
|---|
| 100 | InvalidUserInputException(String message) {
|
|---|
| 101 | super(message);
|
|---|
| 102 | }
|
|---|
| 103 | }
|
|---|
| 104 |
|
|---|
| 105 | /**
|
|---|
| 106 | * Checks that the selection is OK. If not, displays error message. If so
|
|---|
| 107 | * calls to terraceBuilding(), which does all the real work.
|
|---|
| 108 | */
|
|---|
| 109 | @Override
|
|---|
| 110 | public void actionPerformed(ActionEvent e) {
|
|---|
| 111 | Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected();
|
|---|
| 112 | Way outline = null;
|
|---|
| 113 | Way street = null;
|
|---|
| 114 | String streetname = null;
|
|---|
| 115 | ArrayList<Node> housenumbers = new ArrayList<>();
|
|---|
| 116 | Node init = null;
|
|---|
| 117 |
|
|---|
| 118 | try {
|
|---|
| 119 | if (sel.size() == 1) {
|
|---|
| 120 | OsmPrimitive prim = sel.iterator().next();
|
|---|
| 121 |
|
|---|
| 122 | if (!(prim instanceof Way))
|
|---|
| 123 | throw new InvalidUserInputException(prim+" is not a way");
|
|---|
| 124 |
|
|---|
| 125 | outline = (Way) prim;
|
|---|
| 126 | } else if (sel.size() > 1) {
|
|---|
| 127 | List<Way> ways = OsmPrimitive.getFilteredList(sel, Way.class);
|
|---|
| 128 | Iterator<Way> wit = ways.iterator();
|
|---|
| 129 | while (wit.hasNext()) {
|
|---|
| 130 | Way way = wit.next();
|
|---|
| 131 | if (way.hasKey("building")) {
|
|---|
| 132 | if (outline != null)
|
|---|
| 133 | // already have a building
|
|---|
| 134 | throw new InvalidUserInputException("already have a building");
|
|---|
| 135 | outline = way;
|
|---|
| 136 | } else if (way.hasKey("highway")) {
|
|---|
| 137 | if (street != null)
|
|---|
| 138 | // already have a street
|
|---|
| 139 | throw new InvalidUserInputException("already have a street");
|
|---|
| 140 | street = way;
|
|---|
| 141 | streetname = street.get("name");
|
|---|
| 142 | if (streetname == null)
|
|---|
| 143 | throw new InvalidUserInputException("street does not have any name");
|
|---|
| 144 | } else
|
|---|
| 145 | throw new InvalidUserInputException(way+" is neither a building nor a highway");
|
|---|
| 146 | }
|
|---|
| 147 |
|
|---|
| 148 | if (outline == null)
|
|---|
| 149 | throw new InvalidUserInputException("no outline way found");
|
|---|
| 150 |
|
|---|
| 151 | List<Node> nodes = OsmPrimitive.getFilteredList(sel, Node.class);
|
|---|
| 152 | Iterator<Node> nit = nodes.iterator();
|
|---|
| 153 | // Actually this should test if the selected address nodes lie
|
|---|
| 154 | // within the selected outline. Any ideas how to do this?
|
|---|
| 155 | while (nit.hasNext()) {
|
|---|
| 156 | Node node = nit.next();
|
|---|
| 157 | if (node.hasKey("addr:housenumber")) {
|
|---|
| 158 | String nodesStreetName = node.get("addr:street");
|
|---|
| 159 | // if a node has a street name if must be equal
|
|---|
| 160 | // to the one of the other address nodes
|
|---|
| 161 | if (nodesStreetName != null) {
|
|---|
| 162 | if (streetname == null)
|
|---|
| 163 | streetname = nodesStreetName;
|
|---|
| 164 | else if (!nodesStreetName.equals(streetname))
|
|---|
| 165 | throw new InvalidUserInputException("addr:street does not match street name");
|
|---|
| 166 | }
|
|---|
| 167 |
|
|---|
| 168 | housenumbers.add(node);
|
|---|
| 169 | } else {
|
|---|
| 170 | // A given node might not be an address node but then
|
|---|
| 171 | // it has to be part of the building to help getting
|
|---|
| 172 | // the number direction right.
|
|---|
| 173 | if (!outline.containsNode(node) || init != null)
|
|---|
| 174 | throw new InvalidUserInputException("node problem");
|
|---|
| 175 | init = node;
|
|---|
| 176 | }
|
|---|
| 177 | }
|
|---|
| 178 |
|
|---|
| 179 | Collections.sort(housenumbers, new HousenumberNodeComparator());
|
|---|
| 180 | }
|
|---|
| 181 |
|
|---|
| 182 | if (outline == null || !outline.isClosed() || outline.getNodesCount() < 5)
|
|---|
| 183 | throw new InvalidUserInputException("wrong or missing outline");
|
|---|
| 184 |
|
|---|
| 185 | } catch (InvalidUserInputException ex) {
|
|---|
| 186 | Main.warn("Terracer: "+ex.getMessage());
|
|---|
| 187 | new ExtendedDialog(Main.parent, tr("Invalid selection"), new String[] {"OK"})
|
|---|
| 188 | .setButtonIcons(new String[] {"ok"}).setIcon(JOptionPane.INFORMATION_MESSAGE)
|
|---|
| 189 | .setContent(tr("Select a single, closed way of at least four nodes. " +
|
|---|
| 190 | "(Optionally you can also select a street for the addr:street tag " +
|
|---|
| 191 | "and a node to mark the start of numbering.)"))
|
|---|
| 192 | .showDialog();
|
|---|
| 193 | return;
|
|---|
| 194 | }
|
|---|
| 195 |
|
|---|
| 196 | Relation associatedStreet = null;
|
|---|
| 197 |
|
|---|
| 198 | // Try to find an associatedStreet relation that could be reused from housenumbers, outline and street.
|
|---|
| 199 | Set<OsmPrimitive> candidates = new HashSet<OsmPrimitive>(housenumbers);
|
|---|
| 200 | candidates.add(outline);
|
|---|
| 201 | if (street != null) {
|
|---|
| 202 | candidates.add(street);
|
|---|
| 203 | }
|
|---|
| 204 |
|
|---|
| 205 | Set<Relation> associatedStreets = findAssociatedStreets(candidates);
|
|---|
| 206 |
|
|---|
| 207 | if (!associatedStreets.isEmpty()) {
|
|---|
| 208 | associatedStreet = associatedStreets.iterator().next();
|
|---|
| 209 | if (associatedStreets.size() > 1) {
|
|---|
| 210 | // TODO: Deal with multiple associated Streets
|
|---|
| 211 | Main.warn("Terracer: Found "+associatedStreets.size()+" associatedStreet relations. Considering the first one only.");
|
|---|
| 212 | }
|
|---|
| 213 | }
|
|---|
| 214 |
|
|---|
| 215 | if (streetname == null && associatedStreet != null && associatedStreet.hasKey("name")) {
|
|---|
| 216 | streetname = associatedStreet.get("name");
|
|---|
| 217 | }
|
|---|
| 218 |
|
|---|
| 219 | if (housenumbers.size() == 1) {
|
|---|
| 220 | // Special case of one outline and one address node.
|
|---|
| 221 | // Don't open the dialog
|
|---|
| 222 | try {
|
|---|
| 223 | terraceBuilding(outline, init, street, associatedStreet, 0, null, null, 0, housenumbers, streetname, associatedStreet != null, false, "yes");
|
|---|
| 224 | } catch (UserCancelException ex) {
|
|---|
| 225 | // Ignore
|
|---|
| 226 | } finally {
|
|---|
| 227 | this.commands.clear();
|
|---|
| 228 | this.commands = null;
|
|---|
| 229 | }
|
|---|
| 230 | } else {
|
|---|
| 231 | String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size());
|
|---|
| 232 | // show input dialog.
|
|---|
| 233 | new HouseNumberInputHandler(this, outline, init, street, streetname, outline.get("building"),
|
|---|
| 234 | associatedStreet, housenumbers, title).dialog.showDialog();
|
|---|
| 235 | }
|
|---|
| 236 | }
|
|---|
| 237 |
|
|---|
| 238 | public Integer getNumber(String number) {
|
|---|
| 239 | try {
|
|---|
| 240 | return Integer.parseInt(number);
|
|---|
| 241 | } catch (NumberFormatException ex) {
|
|---|
| 242 | return null;
|
|---|
| 243 | }
|
|---|
| 244 | }
|
|---|
| 245 |
|
|---|
| 246 | /**
|
|---|
| 247 | * Sorts the house number nodes according their numbers only
|
|---|
| 248 | *
|
|---|
| 249 | * @param house
|
|---|
| 250 | * number nodes
|
|---|
| 251 | */
|
|---|
| 252 | class HousenumberNodeComparator implements Comparator<Node> {
|
|---|
| 253 | private final Pattern pat = Pattern.compile("^(\\d+)\\s*(.*)");
|
|---|
| 254 |
|
|---|
| 255 | @Override
|
|---|
| 256 | public int compare(Node node1, Node node2) {
|
|---|
| 257 | // It's necessary to strip off trailing non-numbers so we can
|
|---|
| 258 | // compare the numbers itself numerically since string comparison
|
|---|
| 259 | // doesn't work for numbers with different number of digits,
|
|---|
| 260 | // e.g. 9 is higher than 11
|
|---|
| 261 | String node1String = node1.get("addr:housenumber");
|
|---|
| 262 | String node2String = node2.get("addr:housenumber");
|
|---|
| 263 | Matcher mat = pat.matcher(node1String);
|
|---|
| 264 | if (mat.find()) {
|
|---|
| 265 | Integer node1Int = Integer.valueOf(mat.group(1));
|
|---|
| 266 | String node1Rest = mat.group(2);
|
|---|
| 267 | mat = pat.matcher(node2String);
|
|---|
| 268 | if (mat.find()) {
|
|---|
| 269 | Integer node2Int = Integer.valueOf(mat.group(1));
|
|---|
| 270 | // If the numbers are the same, the rest has to make the decision,
|
|---|
| 271 | // e.g. when comparing 23, 23a and 23b.
|
|---|
| 272 | if (node1Int.equals(node2Int))
|
|---|
| 273 | {
|
|---|
| 274 | String node2Rest = mat.group(2);
|
|---|
| 275 | return node1Rest.compareTo(node2Rest);
|
|---|
| 276 | }
|
|---|
| 277 |
|
|---|
| 278 | return node1Int.compareTo(node2Int);
|
|---|
| 279 | }
|
|---|
| 280 | }
|
|---|
| 281 |
|
|---|
| 282 | return node1String.compareTo(node2String);
|
|---|
| 283 | }
|
|---|
| 284 | }
|
|---|
| 285 |
|
|---|
| 286 | /**
|
|---|
| 287 | * Terraces a single, closed, quadrilateral way.
|
|---|
| 288 | *
|
|---|
| 289 | * Any node must be adjacent to both a short and long edge, we naively
|
|---|
| 290 | * choose the longest edge and its opposite and interpolate along them
|
|---|
| 291 | * linearly to produce new nodes. Those nodes are then assembled into
|
|---|
| 292 | * closed, quadrilateral ways and left in the selection.
|
|---|
| 293 | *
|
|---|
| 294 | * @param outline The closed, quadrilateral way to terrace.
|
|---|
| 295 | * @param init The node that hints at which side to start the numbering
|
|---|
| 296 | * @param street The street, the buildings belong to (may be null)
|
|---|
| 297 | * @param associatedStreet
|
|---|
| 298 | * @param segments The number of segments to generate
|
|---|
| 299 | * @param start Starting housenumber
|
|---|
| 300 | * @param end Ending housenumber
|
|---|
| 301 | * @param step The step width to use
|
|---|
| 302 | * @param housenumbers List of housenumbers to use. From and To are ignored
|
|---|
| 303 | * if this is set.
|
|---|
| 304 | * @param streetName the name of the street, derived from the street line
|
|---|
| 305 | * or the house numbers (may be null)
|
|---|
| 306 | * @param handleRelations If the user likes to add a relation or extend an
|
|---|
| 307 | * existing relation
|
|---|
| 308 | * @param keepOutline If the outline way should be kept
|
|---|
| 309 | * @param buildingValue The value for {@code building} key to add
|
|---|
| 310 | * @throws UserCancelException
|
|---|
| 311 | */
|
|---|
| 312 | public void terraceBuilding(final Way outline, Node init, Way street, Relation associatedStreet, Integer segments,
|
|---|
| 313 | String start, String end, int step, List<Node> housenumbers, String streetName, boolean handleRelations,
|
|---|
| 314 | boolean keepOutline, String buildingValue) throws UserCancelException {
|
|---|
| 315 | final int nb;
|
|---|
| 316 | Integer to = null, from = null;
|
|---|
| 317 | if (housenumbers == null || housenumbers.isEmpty()) {
|
|---|
| 318 | to = getNumber(end);
|
|---|
| 319 | from = getNumber(start);
|
|---|
| 320 | if (to != null && from != null) {
|
|---|
| 321 | nb = 1 + (to.intValue() - from.intValue()) / step;
|
|---|
| 322 | } else if (segments != null) {
|
|---|
| 323 | nb = segments.intValue();
|
|---|
| 324 | } else {
|
|---|
| 325 | // if we get here, there is is a bug in the input validation.
|
|---|
| 326 | throw new TerracerRuntimeException(
|
|---|
| 327 | "Could not determine segments from parameters, this is a bug. "
|
|---|
| 328 | + "Parameters were: segments " + segments
|
|---|
| 329 | + " from " + from + " to " + to + " step " + step);
|
|---|
| 330 | }
|
|---|
| 331 | } else {
|
|---|
| 332 | nb = housenumbers.size();
|
|---|
| 333 | }
|
|---|
| 334 |
|
|---|
| 335 | // now find which is the longest side connecting the first node
|
|---|
| 336 | Pair<Way, Way> interp = findFrontAndBack(outline);
|
|---|
| 337 |
|
|---|
| 338 | final boolean swap = init != null && (interp.a.lastNode().equals(init) || interp.b.lastNode().equals(init));
|
|---|
| 339 |
|
|---|
| 340 | final double frontLength = wayLength(interp.a);
|
|---|
| 341 | final double backLength = wayLength(interp.b);
|
|---|
| 342 |
|
|---|
| 343 | // new nodes array to hold all intermediate nodes
|
|---|
| 344 | // This set will contain at least 4 existing nodes from the original outline
|
|---|
| 345 | // (those, which coordinates match coordinates of outline nodes)
|
|---|
| 346 | Node[][] newNodes = new Node[2][nb + 1];
|
|---|
| 347 | // This list will contain nodes of the outline that are used in new lines.
|
|---|
| 348 | // These nodes will not be deleted with the outline (if deleting was prompted).
|
|---|
| 349 | List<Node> reusedNodes = new ArrayList<>();
|
|---|
| 350 |
|
|---|
| 351 | this.commands = new LinkedList<>();
|
|---|
| 352 | Collection<Way> ways = new LinkedList<>();
|
|---|
| 353 |
|
|---|
| 354 | if (nb > 1) {
|
|---|
| 355 | // add required new nodes and build list of nodes to reuse
|
|---|
| 356 | for (int i = 0; i <= nb; ++i) {
|
|---|
| 357 | int iDir = swap ? nb - i : i;
|
|---|
| 358 | newNodes[0][i] = interpolateAlong(interp.a, frontLength * iDir / nb);
|
|---|
| 359 | newNodes[1][i] = interpolateAlong(interp.b, backLength * iDir / nb);
|
|---|
| 360 | if (!outline.containsNode(newNodes[0][i]))
|
|---|
| 361 | this.commands.add(new AddCommand(newNodes[0][i]));
|
|---|
| 362 | else
|
|---|
| 363 | reusedNodes.add(newNodes[0][i]);
|
|---|
| 364 | if (!outline.containsNode(newNodes[1][i]))
|
|---|
| 365 | this.commands.add(new AddCommand(newNodes[1][i]));
|
|---|
| 366 | else
|
|---|
| 367 | reusedNodes.add(newNodes[1][i]);
|
|---|
| 368 | }
|
|---|
| 369 |
|
|---|
| 370 | // assemble new quadrilateral, closed ways
|
|---|
| 371 | for (int i = 0; i < nb; ++i) {
|
|---|
| 372 | final Way terr;
|
|---|
| 373 | if (i > 0 || keepOutline) {
|
|---|
| 374 | terr = new Way();
|
|---|
| 375 | // add the tags of the outline to each building (e.g. source=*)
|
|---|
| 376 | TagCollection.from(outline).applyTo(terr);
|
|---|
| 377 | } else {
|
|---|
| 378 | terr = new Way(outline);
|
|---|
| 379 | terr.setNodes(null);
|
|---|
| 380 | }
|
|---|
| 381 |
|
|---|
| 382 | terr.addNode(newNodes[0][i]);
|
|---|
| 383 | terr.addNode(newNodes[0][i + 1]);
|
|---|
| 384 | terr.addNode(newNodes[1][i + 1]);
|
|---|
| 385 | terr.addNode(newNodes[1][i]);
|
|---|
| 386 | terr.addNode(newNodes[0][i]);
|
|---|
| 387 |
|
|---|
| 388 | ways.add(addressBuilding(terr, street, streetName, associatedStreet, housenumbers, i,
|
|---|
| 389 | from != null ? Integer.toString(from + i * step) : null, buildingValue));
|
|---|
| 390 |
|
|---|
| 391 | if (i > 0 || keepOutline) {
|
|---|
| 392 | this.commands.add(new AddCommand(terr));
|
|---|
| 393 | } else {
|
|---|
| 394 | this.commands.add(new ChangeCommand(outline, terr));
|
|---|
| 395 | }
|
|---|
| 396 | }
|
|---|
| 397 |
|
|---|
| 398 | if (!keepOutline) {
|
|---|
| 399 | // Delete outline nodes having no tags and referrers but the outline itself
|
|---|
| 400 | List<Node> nodes = outline.getNodes();
|
|---|
| 401 | ArrayList<Node> nodesToDelete = new ArrayList<>();
|
|---|
| 402 | for (Node n : nodes)
|
|---|
| 403 | if (!n.hasKeys() && n.getReferrers().size() == 1 && !reusedNodes.contains(n))
|
|---|
| 404 | nodesToDelete.add(n);
|
|---|
| 405 | if (!nodesToDelete.isEmpty())
|
|---|
| 406 | this.commands.add(DeleteCommand.delete(Main.main.getEditLayer(), nodesToDelete));
|
|---|
| 407 | }
|
|---|
| 408 | } else {
|
|---|
| 409 | // Single building, just add the address details
|
|---|
| 410 | ways.add(addressBuilding(outline, street, streetName, associatedStreet, housenumbers, 0, start, buildingValue));
|
|---|
| 411 | }
|
|---|
| 412 |
|
|---|
| 413 | // Remove the address nodes since their tags have been incorporated into the terraces.
|
|---|
| 414 | // Or should removing them also be an option?
|
|---|
| 415 | if (!housenumbers.isEmpty()) {
|
|---|
| 416 | commands.add(DeleteCommand.delete(Main.main.getEditLayer(),
|
|---|
| 417 | housenumbers, true, true));
|
|---|
| 418 | }
|
|---|
| 419 |
|
|---|
| 420 | if (handleRelations) { // create a new relation or merge with existing
|
|---|
| 421 | if (associatedStreet == null) { // create a new relation
|
|---|
| 422 | addNewAssociatedStreetRelation(street, streetName, ways);
|
|---|
| 423 | } else { // relation exists already - add new members
|
|---|
| 424 | updateAssociatedStreetRelation(associatedStreet, housenumbers, ways);
|
|---|
| 425 | }
|
|---|
| 426 | }
|
|---|
| 427 |
|
|---|
| 428 | Main.main.undoRedo.add(createTerracingCommand(outline));
|
|---|
| 429 | if (nb <= 1 && street != null) {
|
|---|
| 430 | // Select the way (for quick selection of a new house (with the same way))
|
|---|
| 431 | Main.main.getCurrentDataSet().setSelected(street);
|
|---|
| 432 | } else {
|
|---|
| 433 | // Select the new building outlines (for quick reversing)
|
|---|
| 434 | Main.main.getCurrentDataSet().setSelected(ways);
|
|---|
| 435 | }
|
|---|
| 436 | }
|
|---|
| 437 |
|
|---|
| 438 | private void updateAssociatedStreetRelation(Relation associatedStreet, List<Node> housenumbers, Collection<Way> ways) {
|
|---|
| 439 | Relation newAssociatedStreet = new Relation(associatedStreet);
|
|---|
| 440 | // remove housenumbers as they have been deleted
|
|---|
| 441 | newAssociatedStreet.removeMembersFor(housenumbers);
|
|---|
| 442 | for (Way w : ways) {
|
|---|
| 443 | newAssociatedStreet.addMember(new RelationMember("house", w));
|
|---|
| 444 | }
|
|---|
| 445 | /*if (!keepOutline) {
|
|---|
| 446 | newAssociatedStreet.removeMembersFor(outline);
|
|---|
| 447 | }*/
|
|---|
| 448 | this.commands.add(new ChangeCommand(associatedStreet, newAssociatedStreet));
|
|---|
| 449 | }
|
|---|
| 450 |
|
|---|
| 451 | private void addNewAssociatedStreetRelation(Way street, String streetName, Collection<Way> ways) {
|
|---|
| 452 | Relation associatedStreet = new Relation();
|
|---|
| 453 | associatedStreet.put("type", "associatedStreet");
|
|---|
| 454 | if (street != null) { // a street was part of the selection
|
|---|
| 455 | associatedStreet.put("name", street.get("name"));
|
|---|
| 456 | associatedStreet.addMember(new RelationMember("street", street));
|
|---|
| 457 | } else {
|
|---|
| 458 | associatedStreet.put("name", streetName);
|
|---|
| 459 | }
|
|---|
| 460 | for (Way w : ways) {
|
|---|
| 461 | associatedStreet.addMember(new RelationMember("house", w));
|
|---|
| 462 | }
|
|---|
| 463 | this.commands.add(new AddCommand(associatedStreet));
|
|---|
| 464 | }
|
|---|
| 465 |
|
|---|
| 466 | private Command createTerracingCommand(final Way outline) {
|
|---|
| 467 | return new SequenceCommand(tr("Terrace"), commands) {
|
|---|
| 468 | @Override
|
|---|
| 469 | public boolean executeCommand() {
|
|---|
| 470 | boolean result = super.executeCommand();
|
|---|
| 471 | if (result && tagsInConflict != null) {
|
|---|
| 472 | try {
|
|---|
| 473 | // Build conflicts commands only after all primitives have been added to dataset to fix #8942
|
|---|
| 474 | List<Command> conflictCommands = CombinePrimitiveResolverDialog.launchIfNecessary(
|
|---|
| 475 | tagsInConflict, primitives, Collections.singleton(outline));
|
|---|
| 476 | if (!conflictCommands.isEmpty()) {
|
|---|
| 477 | List<Command> newCommands = new ArrayList<>(commands);
|
|---|
| 478 | newCommands.addAll(conflictCommands);
|
|---|
| 479 | setSequence(newCommands.toArray(new Command[0]));
|
|---|
| 480 | // Run conflicts commands
|
|---|
| 481 | for (int i = 0; i < conflictCommands.size(); i++) {
|
|---|
| 482 | result = conflictCommands.get(i).executeCommand();
|
|---|
| 483 | if (!result && !continueOnError) {
|
|---|
| 484 | setSequenceComplete(false);
|
|---|
| 485 | undoCommands(commands.size()+i-1);
|
|---|
| 486 | return false;
|
|---|
| 487 | }
|
|---|
| 488 | }
|
|---|
| 489 | }
|
|---|
| 490 | } catch (UserCancelException e) {
|
|---|
| 491 | // Ignore
|
|---|
| 492 | }
|
|---|
| 493 | }
|
|---|
| 494 | return result;
|
|---|
| 495 | }
|
|---|
| 496 | };
|
|---|
| 497 | }
|
|---|
| 498 |
|
|---|
| 499 | /**
|
|---|
| 500 | * Adds address details to a single building
|
|---|
| 501 | *
|
|---|
| 502 | * @param outline The closed, quadrilateral way to add the address to.
|
|---|
| 503 | * @param street The street, the buildings belong to (may be null)
|
|---|
| 504 | * @param streetName the name of a street (may be null). Used if not null and street is null.
|
|---|
| 505 | * @param associatedStreet The associated street. Used to determine if addr:street should be set or not.
|
|---|
| 506 | * @param buildingValue The value for {@code building} key to add
|
|---|
| 507 | * @return {@code outline}
|
|---|
| 508 | * @throws UserCancelException
|
|---|
| 509 | */
|
|---|
| 510 | private Way addressBuilding(Way outline, Way street, String streetName, Relation associatedStreet,
|
|---|
| 511 | List<Node> housenumbers, int i, String defaultNumber, String buildingValue) throws UserCancelException {
|
|---|
| 512 | Node houseNum = (housenumbers != null && i >= 0 && i < housenumbers.size()) ? housenumbers.get(i) : null;
|
|---|
| 513 | boolean buildingAdded = false;
|
|---|
| 514 | boolean numberAdded = false;
|
|---|
| 515 | if (houseNum != null) {
|
|---|
| 516 | primitives = Arrays.asList(new OsmPrimitive[]{houseNum, outline});
|
|---|
| 517 |
|
|---|
| 518 | TagCollection tagsToCopy = TagCollection.unionOfAllPrimitives(primitives).getTagsFor(houseNum.keySet());
|
|---|
| 519 | tagsInConflict = tagsToCopy.getTagsFor(tagsToCopy.getKeysWithMultipleValues());
|
|---|
| 520 | tagsToCopy = tagsToCopy.minus(tagsInConflict).minus(TagCollection.from(outline));
|
|---|
| 521 |
|
|---|
| 522 | for (Tag tag : tagsToCopy) {
|
|---|
| 523 | this.commands.add(new ChangePropertyCommand(outline, tag.getKey(), tag.getValue()));
|
|---|
| 524 | }
|
|---|
| 525 |
|
|---|
| 526 | buildingAdded = houseNum.hasKey("building");
|
|---|
| 527 | numberAdded = houseNum.hasKey("addr:housenumber");
|
|---|
| 528 | }
|
|---|
| 529 | if (!buildingAdded && buildingValue != null && !buildingValue.isEmpty()) {
|
|---|
| 530 | this.commands.add(new ChangePropertyCommand(outline, "building", buildingValue));
|
|---|
| 531 | }
|
|---|
| 532 | if (defaultNumber != null && !numberAdded) {
|
|---|
| 533 | this.commands.add(new ChangePropertyCommand(outline, "addr:housenumber", defaultNumber));
|
|---|
| 534 | }
|
|---|
| 535 | // Only put addr:street if no relation exists or if it has no name
|
|---|
| 536 | if (associatedStreet == null || !associatedStreet.hasKey("name")) {
|
|---|
| 537 | if (street != null) {
|
|---|
| 538 | this.commands.add(new ChangePropertyCommand(outline, "addr:street", street.get("name")));
|
|---|
| 539 | } else if (streetName != null && !streetName.trim().isEmpty()) {
|
|---|
| 540 | this.commands.add(new ChangePropertyCommand(outline, "addr:street", streetName.trim()));
|
|---|
| 541 | }
|
|---|
| 542 | }
|
|---|
| 543 | return outline;
|
|---|
| 544 | }
|
|---|
| 545 |
|
|---|
| 546 | /**
|
|---|
| 547 | * Creates a node at a certain distance along a way, as calculated by the
|
|---|
| 548 | * great circle distance.
|
|---|
| 549 | *
|
|---|
| 550 | * Note that this really isn't an efficient way to do this and leads to
|
|---|
| 551 | * O(N^2) running time for the main algorithm, but its simple and easy
|
|---|
| 552 | * to understand, and probably won't matter for reasonable-sized ways.
|
|---|
| 553 | *
|
|---|
| 554 | * @param w The way to interpolate.
|
|---|
| 555 | * @param l The length at which to place the node.
|
|---|
| 556 | * @return A node at a distance l along w from the first point.
|
|---|
| 557 | */
|
|---|
| 558 | private Node interpolateAlong(Way w, double l) {
|
|---|
| 559 | List<Pair<Node,Node>> pairs = w.getNodePairs(false);
|
|---|
| 560 | for (int i = 0; i < pairs.size(); ++i) {
|
|---|
| 561 | Pair<Node,Node> p = pairs.get(i);
|
|---|
| 562 | final double seg_length = p.a.getCoor().greatCircleDistance(p.b.getCoor());
|
|---|
| 563 | if (l <= seg_length || i == pairs.size() - 1) {
|
|---|
| 564 | // be generous on the last segment (numerical roudoff can lead to a small overshoot)
|
|---|
| 565 | return interpolateNode(p.a, p.b, l / seg_length);
|
|---|
| 566 | } else {
|
|---|
| 567 | l -= seg_length;
|
|---|
| 568 | }
|
|---|
| 569 | }
|
|---|
| 570 | // we shouldn't get here
|
|---|
| 571 | throw new IllegalStateException();
|
|---|
| 572 | }
|
|---|
| 573 |
|
|---|
| 574 | /**
|
|---|
| 575 | * Calculates the great circle length of a way by summing the great circle
|
|---|
| 576 | * distance of each pair of nodes.
|
|---|
| 577 | *
|
|---|
| 578 | * @param w The way to calculate length of.
|
|---|
| 579 | * @return The length of the way.
|
|---|
| 580 | */
|
|---|
| 581 | private double wayLength(Way w) {
|
|---|
| 582 | double length = 0.0;
|
|---|
| 583 | for (Pair<Node, Node> p : w.getNodePairs(false)) {
|
|---|
| 584 | length += p.a.getCoor().greatCircleDistance(p.b.getCoor());
|
|---|
| 585 | }
|
|---|
| 586 | return length;
|
|---|
| 587 | }
|
|---|
| 588 |
|
|---|
| 589 | /**
|
|---|
| 590 | * Given a way, try and find a definite front and back by looking at the
|
|---|
| 591 | * segments to find the "sides". Sides are assumed to be single segments
|
|---|
| 592 | * which cannot be contiguous.
|
|---|
| 593 | *
|
|---|
| 594 | * @param w The way to analyse.
|
|---|
| 595 | * @return A pair of ways (front, back) pointing in the same directions.
|
|---|
| 596 | */
|
|---|
| 597 | private Pair<Way, Way> findFrontAndBack(Way w) {
|
|---|
| 598 | // calculate the "side-ness" score for each segment of the way
|
|---|
| 599 | double[] sideness = calculateSideness(w);
|
|---|
| 600 |
|
|---|
| 601 | // find the largest two sidenesses which are not contiguous
|
|---|
| 602 | int[] indexes = sortedIndexes(sideness);
|
|---|
| 603 | int side1 = indexes[0];
|
|---|
| 604 | int side2 = indexes[1];
|
|---|
| 605 | // if side2 is contiguous with side1 then look further down the
|
|---|
| 606 | // list. we know there are at least 4 sides, as anything smaller
|
|---|
| 607 | // than a quadrilateral would have been rejected at an earlier stage.
|
|---|
| 608 | if (indexDistance(side1, side2, indexes.length) < 2) {
|
|---|
| 609 | side2 = indexes[2];
|
|---|
| 610 | }
|
|---|
| 611 | if (indexDistance(side1, side2, indexes.length) < 2) {
|
|---|
| 612 | side2 = indexes[3];
|
|---|
| 613 | }
|
|---|
| 614 |
|
|---|
| 615 | // if the second side has a shorter length and an approximately equal
|
|---|
| 616 | // sideness then its better to choose the shorter, as with
|
|---|
| 617 | // quadrilaterals
|
|---|
| 618 | // created using the orthogonalise tool the sideness will be about the
|
|---|
| 619 | // same for all sides.
|
|---|
| 620 | if (sideLength(w, side1) > sideLength(w, side1 + 1)
|
|---|
| 621 | && Math.abs(sideness[side1] - sideness[(side1 + 1) % (w.getNodesCount() - 1)]) < 0.001) {
|
|---|
| 622 | side1 = (side1 + 1) % (w.getNodesCount() - 1);
|
|---|
| 623 | side2 = (side2 + 1) % (w.getNodesCount() - 1);
|
|---|
| 624 | }
|
|---|
| 625 |
|
|---|
| 626 | // swap side1 and side2 into sorted order.
|
|---|
| 627 | if (side1 > side2) {
|
|---|
| 628 | int tmp = side2;
|
|---|
| 629 | side2 = side1;
|
|---|
| 630 | side1 = tmp;
|
|---|
| 631 | }
|
|---|
| 632 |
|
|---|
| 633 | Way front = new Way();
|
|---|
| 634 | Way back = new Way();
|
|---|
| 635 | for (int i = side2 + 1; i < w.getNodesCount() - 1; ++i) {
|
|---|
| 636 | front.addNode(w.getNode(i));
|
|---|
| 637 | }
|
|---|
| 638 | for (int i = 0; i <= side1; ++i) {
|
|---|
| 639 | front.addNode(w.getNode(i));
|
|---|
| 640 | }
|
|---|
| 641 | // add the back in reverse order so that the front and back ways point
|
|---|
| 642 | // in the same direction.
|
|---|
| 643 | for (int i = side2; i > side1; --i) {
|
|---|
| 644 | back.addNode(w.getNode(i));
|
|---|
| 645 | }
|
|---|
| 646 |
|
|---|
| 647 | return new Pair<>(front, back);
|
|---|
| 648 | }
|
|---|
| 649 |
|
|---|
| 650 | /**
|
|---|
| 651 | * returns the distance of two segments of a closed polygon
|
|---|
| 652 | */
|
|---|
| 653 | private int indexDistance(int i1, int i2, int n) {
|
|---|
| 654 | return Math.min(positiveModulus(i1 - i2, n), positiveModulus(i2 - i1, n));
|
|---|
| 655 | }
|
|---|
| 656 |
|
|---|
| 657 | /**
|
|---|
| 658 | * return the modulus in the range [0, n)
|
|---|
| 659 | */
|
|---|
| 660 | private int positiveModulus(int a, int n) {
|
|---|
| 661 | if (n <= 0)
|
|---|
| 662 | throw new IllegalArgumentException();
|
|---|
| 663 | int res = a % n;
|
|---|
| 664 | if (res < 0) {
|
|---|
| 665 | res += n;
|
|---|
| 666 | }
|
|---|
| 667 | return res;
|
|---|
| 668 | }
|
|---|
| 669 |
|
|---|
| 670 | /**
|
|---|
| 671 | * Calculate the length of a side (from node i to i+1) in a way. This assumes that
|
|---|
| 672 | * the way is closed, but I only ever call it for buildings.
|
|---|
| 673 | */
|
|---|
| 674 | private double sideLength(Way w, int i) {
|
|---|
| 675 | Node a = w.getNode(i);
|
|---|
| 676 | Node b = w.getNode((i + 1) % (w.getNodesCount() - 1));
|
|---|
| 677 | return a.getCoor().greatCircleDistance(b.getCoor());
|
|---|
| 678 | }
|
|---|
| 679 |
|
|---|
| 680 | /**
|
|---|
| 681 | * Given an array of doubles (but this could made generic very easily) sort
|
|---|
| 682 | * into order and return the array of indexes such that, for a returned array
|
|---|
| 683 | * x, a[x[i]] is sorted for ascending index i.
|
|---|
| 684 | *
|
|---|
| 685 | * This isn't efficient at all, but should be fine for the small arrays we're
|
|---|
| 686 | * expecting. If this gets slow - replace it with some more efficient algorithm.
|
|---|
| 687 | *
|
|---|
| 688 | * @param a The array to sort.
|
|---|
| 689 | * @return An array of indexes, the same size as the input, such that a[x[i]]
|
|---|
| 690 | * is in sorted order.
|
|---|
| 691 | */
|
|---|
| 692 | private int[] sortedIndexes(final double[] a) {
|
|---|
| 693 | class SortWithIndex implements Comparable<SortWithIndex> {
|
|---|
| 694 | public double x;
|
|---|
| 695 | public int i;
|
|---|
| 696 |
|
|---|
| 697 | public SortWithIndex(double a, int b) {
|
|---|
| 698 | x = a;
|
|---|
| 699 | i = b;
|
|---|
| 700 | }
|
|---|
| 701 |
|
|---|
| 702 | @Override
|
|---|
| 703 | public int compareTo(SortWithIndex o) {
|
|---|
| 704 | return Double.compare(x, o.x);
|
|---|
| 705 | }
|
|---|
| 706 | }
|
|---|
| 707 |
|
|---|
| 708 | final int length = a.length;
|
|---|
| 709 | ArrayList<SortWithIndex> sortable = new ArrayList<>(length);
|
|---|
| 710 | for (int i = 0; i < length; ++i) {
|
|---|
| 711 | sortable.add(new SortWithIndex(a[i], i));
|
|---|
| 712 | }
|
|---|
| 713 | Collections.sort(sortable);
|
|---|
| 714 |
|
|---|
| 715 | int[] indexes = new int[length];
|
|---|
| 716 | for (int i = 0; i < length; ++i) {
|
|---|
| 717 | indexes[i] = sortable.get(i).i;
|
|---|
| 718 | }
|
|---|
| 719 |
|
|---|
| 720 | return indexes;
|
|---|
| 721 | }
|
|---|
| 722 |
|
|---|
| 723 | /**
|
|---|
| 724 | * Calculate "sideness" metric for each segment in a way.
|
|---|
| 725 | */
|
|---|
| 726 | private double[] calculateSideness(Way w) {
|
|---|
| 727 | final int length = w.getNodesCount() - 1;
|
|---|
| 728 | double[] sideness = new double[length];
|
|---|
| 729 |
|
|---|
| 730 | sideness[0] = calculateSideness(w.getNode(length - 1), w.getNode(0), w
|
|---|
| 731 | .getNode(1), w.getNode(2));
|
|---|
| 732 | for (int i = 1; i < length - 1; ++i) {
|
|---|
| 733 | sideness[i] = calculateSideness(w.getNode(i - 1), w.getNode(i), w
|
|---|
| 734 | .getNode(i + 1), w.getNode(i + 2));
|
|---|
| 735 | }
|
|---|
| 736 | sideness[length - 1] = calculateSideness(w.getNode(length - 2), w
|
|---|
| 737 | .getNode(length - 1), w.getNode(length), w.getNode(1));
|
|---|
| 738 |
|
|---|
| 739 | return sideness;
|
|---|
| 740 | }
|
|---|
| 741 |
|
|---|
| 742 | /**
|
|---|
| 743 | * Calculate sideness of a single segment given the nodes which make up that
|
|---|
| 744 | * segment and its previous and next segments in order. Sideness is calculated
|
|---|
| 745 | * for the segment b-c.
|
|---|
| 746 | */
|
|---|
| 747 | private double calculateSideness(Node a, Node b, Node c, Node d) {
|
|---|
| 748 | final double ndx = b.getCoor().getX() - a.getCoor().getX();
|
|---|
| 749 | final double pdx = d.getCoor().getX() - c.getCoor().getX();
|
|---|
| 750 | final double ndy = b.getCoor().getY() - a.getCoor().getY();
|
|---|
| 751 | final double pdy = d.getCoor().getY() - c.getCoor().getY();
|
|---|
| 752 |
|
|---|
| 753 | return (ndx * pdx + ndy * pdy)
|
|---|
| 754 | / Math.sqrt((ndx * ndx + ndy * ndy) * (pdx * pdx + pdy * pdy));
|
|---|
| 755 | }
|
|---|
| 756 |
|
|---|
| 757 | /**
|
|---|
| 758 | * Creates a new node at the interpolated position between the argument
|
|---|
| 759 | * nodes. Interpolates linearly in projected coordinates.
|
|---|
| 760 | *
|
|---|
| 761 | * If new node coordinate matches a or b coordinates, a or b is returned.
|
|---|
| 762 | *
|
|---|
| 763 | * @param a First node, at which f=0.
|
|---|
| 764 | * @param b Last node, at which f=1.
|
|---|
| 765 | * @param f Fractional position between first and last nodes.
|
|---|
| 766 | * @return A new node at the interpolated position (or a or b in case if f ≈ 0 or f ≈ 1).
|
|---|
| 767 | */
|
|---|
| 768 | private Node interpolateNode(Node a, Node b, double f) {
|
|---|
| 769 | Node n = new Node(a.getEastNorth().interpolate(b.getEastNorth(), f));
|
|---|
| 770 | if (n.getCoor().equalsEpsilon(a.getCoor()))
|
|---|
| 771 | return a;
|
|---|
| 772 | if (n.getCoor().equalsEpsilon(b.getCoor()))
|
|---|
| 773 | return b;
|
|---|
| 774 | return n;
|
|---|
| 775 | }
|
|---|
| 776 |
|
|---|
| 777 | @Override
|
|---|
| 778 | protected void updateEnabledState() {
|
|---|
| 779 | setEnabled(getCurrentDataSet() != null);
|
|---|
| 780 | }
|
|---|
| 781 | }
|
|---|