// License: GPL. For details, see LICENSE file.
package terracer;
import static org.openstreetmap.josm.tools.I18n.tr;
import static org.openstreetmap.josm.tools.I18n.trn;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JOptionPane;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.JosmAction;
import org.openstreetmap.josm.command.AddCommand;
import org.openstreetmap.josm.command.ChangeCommand;
import org.openstreetmap.josm.command.ChangePropertyCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.DeleteCommand;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Tag;
import org.openstreetmap.josm.data.osm.TagCollection;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.ExtendedDialog;
import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
import org.openstreetmap.josm.tools.Pair;
import org.openstreetmap.josm.tools.Shortcut;
import org.openstreetmap.josm.tools.UserCancelException;
/**
* Terraces a quadrilateral, closed way into a series of quadrilateral,
* closed ways. If two ways are selected and one of them can be identified as
* a street (highway=*, name=*) then the given street will be added
* to the 'associatedStreet' relation.
*
*
* At present it only works on quadrilaterals, but there is no reason
* why it couldn't be extended to work with other shapes too. The
* algorithm employed is naive, but it works in the simple case.
*
* @author zere - Copyright 2009 CloudMade Ltd
*/
public final class TerracerAction extends JosmAction {
// smsms1 asked for the last value to be remembered to make it easier to do
// repeated terraces. this is the easiest, but not necessarily nicest, way.
// private static String lastSelectedValue = "";
Collection<Command> commands;
private Collection<OsmPrimitive> primitives;
private TagCollection tagsInConflict;
public TerracerAction() {
super(tr("Terrace a building"), "terrace",
tr("Creates individual buildings from a long building."),
Shortcut.registerShortcut("tools:Terracer", tr("Tool: {0}",
tr("Terrace a building")), KeyEvent.VK_T,
Shortcut.SHIFT), true);
}
protected static Set<Relation> findAssociatedStreets(Collection<OsmPrimitive> objects) {
Set<Relation> result = new HashSet<>();
if (objects != null) {
for (OsmPrimitive c : objects) {
if (c != null) {
for (OsmPrimitive p : c.getReferrers()) {
if (p instanceof Relation && "associatedStreet".equals(p.get("type"))) {
result.add((Relation) p);
}
}
}
}
}
return result;
}
private static final class InvalidUserInputException extends Exception {
InvalidUserInputException(String message) {
super(message);
}
}
/**
* Checks that the selection is OK. If not, displays error message. If so
* calls to terraceBuilding(), which does all the real work.
*/
@Override
public void actionPerformed(ActionEvent e) {
Collection<OsmPrimitive> sel = getLayerManager().getEditDataSet().getSelected();
Way outline = null;
Way street = null;
String streetname = null;
ArrayList<Node> housenumbers = new ArrayList<>();
Node init = null;
try {
if (sel.size() == 1) {
OsmPrimitive prim = sel.iterator().next();
if (!(prim instanceof Way))
throw new InvalidUserInputException(prim+" is not a way");
outline = (Way) prim;
} else if (sel.size() > 1) {
List<Way> ways = OsmPrimitive.getFilteredList(sel, Way.class);
Iterator<Way> wit = ways.iterator();
while (wit.hasNext()) {
Way way = wit.next();
if (way.hasKey("building")) {
if (outline != null)
// already have a building
throw new InvalidUserInputException("already have a building");
outline = way;
} else if (way.hasKey("highway")) {
if (street != null)
// already have a street
throw new InvalidUserInputException("already have a street");
street = way;
streetname = street.get("name");
if (streetname == null)
throw new InvalidUserInputException("street does not have any name");
} else
throw new InvalidUserInputException(way+" is neither a building nor a highway");
}
if (outline == null)
throw new InvalidUserInputException("no outline way found");
List<Node> nodes = OsmPrimitive.getFilteredList(sel, Node.class);
Iterator<Node> nit = nodes.iterator();
// Actually this should test if the selected address nodes lie
// within the selected outline. Any ideas how to do this?
while (nit.hasNext()) {
Node node = nit.next();
if (node.hasKey("addr:housenumber")) {
String nodesStreetName = node.get("addr:street");
// if a node has a street name if must be equal
// to the one of the other address nodes
if (nodesStreetName != null) {
if (streetname == null)
streetname = nodesStreetName;
else if (!nodesStreetName.equals(streetname))
throw new InvalidUserInputException("addr:street does not match street name");
}
housenumbers.add(node);
} else {
// A given node might not be an address node but then
// it has to be part of the building to help getting
// the number direction right.
if (!outline.containsNode(node) || init != null)
throw new InvalidUserInputException("node problem");
init = node;
}
}
Collections.sort(housenumbers, new HousenumberNodeComparator());
}
if (outline == null || !outline.isClosed() || outline.getNodesCount() < 5)
throw new InvalidUserInputException("wrong or missing outline");
} catch (InvalidUserInputException ex) {
Main.warn("Terracer: "+ex.getMessage());
new ExtendedDialog(Main.parent, tr("Invalid selection"), new String[] {"OK"})
.setButtonIcons(new String[] {"ok"}).setIcon(JOptionPane.INFORMATION_MESSAGE)
.setContent(tr("Select a single, closed way of at least four nodes. " +
"(Optionally you can also select a street for the addr:street tag " +
"and a node to mark the start of numbering.)"))
.showDialog();
return;
}
Relation associatedStreet = null;
// Try to find an associatedStreet relation that could be reused from housenumbers, outline and street.
Set<OsmPrimitive> candidates = new HashSet<OsmPrimitive>(housenumbers);
candidates.add(outline);
if (street != null) {
candidates.add(street);
}
Set<Relation> associatedStreets = findAssociatedStreets(candidates);
if (!associatedStreets.isEmpty()) {
associatedStreet = associatedStreets.iterator().next();
if (associatedStreets.size() > 1) {
// TODO: Deal with multiple associated Streets
Main.warn("Terracer: Found "+associatedStreets.size()+" associatedStreet relations. Considering the first one only.");
}
}
if (streetname == null && associatedStreet != null && associatedStreet.hasKey("name")) {
streetname = associatedStreet.get("name");
}
if (housenumbers.size() == 1) {
// Special case of one outline and one address node.
// Don't open the dialog
try {
terraceBuilding(outline, init, street, associatedStreet, 0, null, null, 0,
housenumbers, streetname, associatedStreet != null, false, "yes");
} catch (UserCancelException ex) {
Main.trace(ex);
} finally {
this.commands.clear();
this.commands = null;
}
} else {
String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size());
// show input dialog.
new HouseNumberInputHandler(this, outline, init, street, streetname, outline.get("building"),
associatedStreet, housenumbers, title).dialog.showDialog();
}
}
public Integer getNumber(String number) {
try {
return Integer.parseInt(number);
} catch (NumberFormatException ex) {
return null;
}
}
/**
* Sorts the house number nodes according their numbers only
*
* @param house
* number nodes
*/
class HousenumberNodeComparator implements Comparator<Node> {
private final Pattern pat = Pattern.compile("^(\\d+)\\s*(.*)");
@Override
public int compare(Node node1, Node node2) {
// It's necessary to strip off trailing non-numbers so we can
// compare the numbers itself numerically since string comparison
// doesn't work for numbers with different number of digits,
// e.g. 9 is higher than 11
String node1String = node1.get("addr:housenumber");
String node2String = node2.get("addr:housenumber");
Matcher mat = pat.matcher(node1String);
if (mat.find()) {
Integer node1Int = Integer.valueOf(mat.group(1));
String node1Rest = mat.group(2);
mat = pat.matcher(node2String);
if (mat.find()) {
Integer node2Int = Integer.valueOf(mat.group(1));
// If the numbers are the same, the rest has to make the decision,
// e.g. when comparing 23, 23a and 23b.
if (node1Int.equals(node2Int)) {
String node2Rest = mat.group(2);
return node1Rest.compareTo(node2Rest);
}
return node1Int.compareTo(node2Int);
}
}
return node1String.compareTo(node2String);
}
}
/**
* Terraces a single, closed, quadrilateral way.
*
* Any node must be adjacent to both a short and long edge, we naively
* choose the longest edge and its opposite and interpolate along them
* linearly to produce new nodes. Those nodes are then assembled into
* closed, quadrilateral ways and left in the selection.
*
* @param outline The closed, quadrilateral way to terrace.
* @param init The node that hints at which side to start the numbering
* @param street The street, the buildings belong to (may be null)
* @param associatedStreet associated street relation
* @param segments The number of segments to generate
* @param start Starting housenumber
* @param end Ending housenumber
* @param step The step width to use
* @param housenumbers List of housenumbers to use. From and To are ignored
* if this is set.
* @param streetName the name of the street, derived from the street line
* or the house numbers (may be null)
* @param handleRelations If the user likes to add a relation or extend an
* existing relation
* @param keepOutline If the outline way should be kept
* @param buildingValue The value for {@code building} key to add
* @throws UserCancelException if user cancels the operation
*/
public void terraceBuilding(final Way outline, Node init, Way street, Relation associatedStreet, Integer segments,
String start, String end, int step, List<Node> housenumbers, String streetName, boolean handleRelations,
boolean keepOutline, String buildingValue) throws UserCancelException {
final int nb;
Integer to = null, from = null;
if (housenumbers == null || housenumbers.isEmpty()) {
to = getNumber(end);
from = getNumber(start);
if (to != null && from != null) {
nb = 1 + (to.intValue() - from.intValue()) / step;
} else if (segments != null) {
nb = segments.intValue();
} else {
// if we get here, there is is a bug in the input validation.
throw new TerracerRuntimeException(
"Could not determine segments from parameters, this is a bug. "
+ "Parameters were: segments " + segments
+ " from " + from + " to " + to + " step " + step);
}
} else {
nb = housenumbers.size();
}
// now find which is the longest side connecting the first node
Pair<Way, Way> interp = findFrontAndBack(outline);
final boolean swap = init != null && (interp.a.lastNode().equals(init) || interp.b.lastNode().equals(init));
final double frontLength = wayLength(interp.a);
final double backLength = wayLength(interp.b);
// new nodes array to hold all intermediate nodes
// This set will contain at least 4 existing nodes from the original outline
// (those, which coordinates match coordinates of outline nodes)
Node[][] newNodes = new Node[2][nb + 1];
// This list will contain nodes of the outline that are used in new lines.
// These nodes will not be deleted with the outline (if deleting was prompted).
List<Node> reusedNodes = new ArrayList<>();
this.commands = new LinkedList<>();
Collection<Way> ways = new LinkedList<>();
if (nb > 1) {
// add required new nodes and build list of nodes to reuse
for (int i = 0; i <= nb; ++i) {
int iDir = swap ? nb - i : i;
newNodes[0][i] = interpolateAlong(interp.a, frontLength * iDir / nb);
newNodes[1][i] = interpolateAlong(interp.b, backLength * iDir / nb);
if (!outline.containsNode(newNodes[0][i]))
this.commands.add(new AddCommand(newNodes[0][i]));
else
reusedNodes.add(newNodes[0][i]);
if (!outline.containsNode(newNodes[1][i]))
this.commands.add(new AddCommand(newNodes[1][i]));
else
reusedNodes.add(newNodes[1][i]);
}
// assemble new quadrilateral, closed ways
for (int i = 0; i < nb; ++i) {
final Way terr;
boolean createNewWay = i > 0 || keepOutline;
if (createNewWay) {
terr = new Way();
// add the tags of the outline to each building (e.g. source=*)
TagCollection.from(outline).applyTo(terr);
} else {
terr = new Way(outline);
terr.setNodes(null);
}
terr.addNode(newNodes[0][i]);
terr.addNode(newNodes[0][i + 1]);
terr.addNode(newNodes[1][i + 1]);
terr.addNode(newNodes[1][i]);
terr.addNode(newNodes[0][i]);
addressBuilding(terr, street, streetName, associatedStreet, housenumbers, i,
from != null ? Integer.toString(from + i * step) : null, buildingValue);
if (createNewWay) {
ways.add(terr);
this.commands.add(new AddCommand(terr));
} else {
ways.add(outline);
this.commands.add(new ChangeCommand(outline, terr));
}
}
if (!keepOutline) {
// Delete outline nodes having no tags and referrers but the outline itself
List<Node> nodes = outline.getNodes();
ArrayList<Node> nodesToDelete = new ArrayList<>();
for (Node n : nodes) {
if (!n.hasKeys() && n.getReferrers().size() == 1 && !reusedNodes.contains(n))
nodesToDelete.add(n);
}
if (!nodesToDelete.isEmpty())
this.commands.add(DeleteCommand.delete(Main.getLayerManager().getEditLayer(), nodesToDelete));
}
} else {
// Single building, just add the address details
addressBuilding(outline, street, streetName, associatedStreet, housenumbers, 0, start, buildingValue);
ways.add(outline);
}
// Remove the address nodes since their tags have been incorporated into the terraces.
// Or should removing them also be an option?
if (!housenumbers.isEmpty()) {
commands.add(DeleteCommand.delete(Main.getLayerManager().getEditLayer(),
housenumbers, true, true));
}
if (handleRelations) { // create a new relation or merge with existing
if (associatedStreet == null) { // create a new relation
addNewAssociatedStreetRelation(street, streetName, ways);
} else { // relation exists already - add new members
updateAssociatedStreetRelation(associatedStreet, housenumbers, ways);
}
}
Main.main.undoRedo.add(createTerracingCommand(outline));
if (nb <= 1 && street != null) {
// Select the way (for quick selection of a new house (with the same way))
Main.getLayerManager().getEditDataSet().setSelected(street);
} else {
// Select the new building outlines (for quick reversing)
Main.getLayerManager().getEditDataSet().setSelected(ways);
}
}
private void updateAssociatedStreetRelation(Relation associatedStreet, List<Node> housenumbers, Collection<Way> ways) {
Relation newAssociatedStreet = new Relation(associatedStreet);
// remove housenumbers as they have been deleted
newAssociatedStreet.removeMembersFor(housenumbers);
for (Way w : ways) {
newAssociatedStreet.addMember(new RelationMember("house", w));
}
/*if (!keepOutline) {
newAssociatedStreet.removeMembersFor(outline);
}*/
this.commands.add(new ChangeCommand(associatedStreet, newAssociatedStreet));
}
private void addNewAssociatedStreetRelation(Way street, String streetName, Collection<Way> ways) {
Relation associatedStreet = new Relation();
associatedStreet.put("type", "associatedStreet");
if (street != null) { // a street was part of the selection
associatedStreet.put("name", street.get("name"));
associatedStreet.addMember(new RelationMember("street", street));
} else {
associatedStreet.put("name", streetName);
}
for (Way w : ways) {
associatedStreet.addMember(new RelationMember("house", w));
}
this.commands.add(new AddCommand(associatedStreet));
}
private Command createTerracingCommand(final Way outline) {
return new SequenceCommand(tr("Terrace"), commands) {
@Override
public boolean executeCommand() {
boolean result = super.executeCommand();
if (result && tagsInConflict != null) {
try {
// Build conflicts commands only after all primitives have been added to dataset to fix #8942
List<Command> conflictCommands = CombinePrimitiveResolverDialog.launchIfNecessary(
tagsInConflict, primitives, Collections.singleton(outline));
if (!conflictCommands.isEmpty()) {
List<Command> newCommands = new ArrayList<>(commands);
newCommands.addAll(conflictCommands);
setSequence(newCommands.toArray(new Command[0]));
// Run conflicts commands
for (int i = 0; i < conflictCommands.size(); i++) {
result = conflictCommands.get(i).executeCommand();
if (!result && !continueOnError) {
setSequenceComplete(false);
undoCommands(commands.size()+i-1);
return false;
}
}
}
} catch (UserCancelException e) {
Main.trace(e);
}
}
return result;
}
};
}
/**
* Adds address details to a single building
*
* @param outline The closed, quadrilateral way to add the address to.
* @param street The street, the buildings belong to (may be null)
* @param streetName the name of a street (may be null). Used if not null and street is null.
* @param associatedStreet The associated street. Used to determine if addr:street should be set or not.
* @param buildingValue The value for {@code building} key to add
* @return {@code outline}
* @throws UserCancelException if user cancels the operation
*/
private void addressBuilding(Way outline, Way street, String streetName, Relation associatedStreet,
List<Node> housenumbers, int i, String defaultNumber, String buildingValue) throws UserCancelException {
Node houseNum = (housenumbers != null && i >= 0 && i < housenumbers.size()) ? housenumbers.get(i) : null;
boolean buildingAdded = false;
boolean numberAdded = false;
if (houseNum != null) {
primitives = Arrays.asList(new OsmPrimitive[]{houseNum, outline});
TagCollection tagsToCopy = TagCollection.unionOfAllPrimitives(primitives).getTagsFor(houseNum.keySet());
tagsInConflict = tagsToCopy.getTagsFor(tagsToCopy.getKeysWithMultipleValues());
tagsToCopy = tagsToCopy.minus(tagsInConflict).minus(TagCollection.from(outline));
for (Tag tag : tagsToCopy) {
this.commands.add(new ChangePropertyCommand(outline, tag.getKey(), tag.getValue()));
}
buildingAdded = houseNum.hasKey("building");
numberAdded = houseNum.hasKey("addr:housenumber");
}
if (!buildingAdded && buildingValue != null && !buildingValue.isEmpty()) {
this.commands.add(new ChangePropertyCommand(outline, "building", buildingValue));
}
if (defaultNumber != null && !numberAdded) {
this.commands.add(new ChangePropertyCommand(outline, "addr:housenumber", defaultNumber));
}
// Only put addr:street if no relation exists or if it has no name
if (associatedStreet == null || !associatedStreet.hasKey("name")) {
if (street != null) {
this.commands.add(new ChangePropertyCommand(outline, "addr:street", street.get("name")));
} else if (streetName != null && !streetName.trim().isEmpty()) {
this.commands.add(new ChangePropertyCommand(outline, "addr:street", streetName.trim()));
}
}
}
/**
* Creates a node at a certain distance along a way, as calculated by the
* great circle distance.
*
* Note that this really isn't an efficient way to do this and leads to
* O(N^2) running time for the main algorithm, but its simple and easy
* to understand, and probably won't matter for reasonable-sized ways.
*
* @param w The way to interpolate.
* @param l The length at which to place the node.
* @return A node at a distance l along w from the first point.
*/
private Node interpolateAlong(Way w, double l) {
List<Pair<Node, Node>> pairs = w.getNodePairs(false);
for (int i = 0; i < pairs.size(); ++i) {
Pair<Node, Node> p = pairs.get(i);
final double seg_length = p.a.getCoor().greatCircleDistance(p.b.getCoor());
if (l <= seg_length || i == pairs.size() - 1) {
// be generous on the last segment (numerical roudoff can lead to a small overshoot)
return interpolateNode(p.a, p.b, l / seg_length);
} else {
l -= seg_length;
}
}
// we shouldn't get here
throw new IllegalStateException();
}
/**
* Calculates the great circle length of a way by summing the great circle
* distance of each pair of nodes.
*
* @param w The way to calculate length of.
* @return The length of the way.
*/
private double wayLength(Way w) {
double length = 0.0;
for (Pair<Node, Node> p : w.getNodePairs(false)) {
length += p.a.getCoor().greatCircleDistance(p.b.getCoor());
}
return length;
}
/**
* Given a way, try and find a definite front and back by looking at the
* segments to find the "sides". Sides are assumed to be single segments
* which cannot be contiguous.
*
* @param w The way to analyse.
* @return A pair of ways (front, back) pointing in the same directions.
*/
private Pair<Way, Way> findFrontAndBack(Way w) {
// calculate the "side-ness" score for each segment of the way
double[] sideness = calculateSideness(w);
// find the largest two sidenesses which are not contiguous
int[] indexes = sortedIndexes(sideness);
int side1 = indexes[0];
int side2 = indexes[1];
// if side2 is contiguous with side1 then look further down the
// list. we know there are at least 4 sides, as anything smaller
// than a quadrilateral would have been rejected at an earlier stage.
if (indexDistance(side1, side2, indexes.length) < 2) {
side2 = indexes[2];
}
if (indexDistance(side1, side2, indexes.length) < 2) {
side2 = indexes[3];
}
// if the second side has a shorter length and an approximately equal
// sideness then its better to choose the shorter, as with
// quadrilaterals
// created using the orthogonalise tool the sideness will be about the
// same for all sides.
if (sideLength(w, side1) > sideLength(w, side1 + 1)
&& Math.abs(sideness[side1] - sideness[(side1 + 1) % (w.getNodesCount() - 1)]) < 0.001) {
side1 = (side1 + 1) % (w.getNodesCount() - 1);
side2 = (side2 + 1) % (w.getNodesCount() - 1);
}
// swap side1 and side2 into sorted order.
if (side1 > side2) {
int tmp = side2;
side2 = side1;
side1 = tmp;
}
Way front = new Way();
Way back = new Way();
for (int i = side2 + 1; i < w.getNodesCount() - 1; ++i) {
front.addNode(w.getNode(i));
}
for (int i = 0; i <= side1; ++i) {
front.addNode(w.getNode(i));
}
// add the back in reverse order so that the front and back ways point
// in the same direction.
for (int i = side2; i > side1; --i) {
back.addNode(w.getNode(i));
}
return new Pair<>(front, back);
}
/**
* returns the distance of two segments of a closed polygon
*/
private int indexDistance(int i1, int i2, int n) {
return Math.min(positiveModulus(i1 - i2, n), positiveModulus(i2 - i1, n));
}
/**
* return the modulus in the range [0, n)
*/
private int positiveModulus(int a, int n) {
if (n <= 0)
throw new IllegalArgumentException();
int res = a % n;
if (res < 0) {
res += n;
}
return res;
}
/**
* Calculate the length of a side (from node i to i+1) in a way. This assumes that
* the way is closed, but I only ever call it for buildings.
*/
private double sideLength(Way w, int i) {
Node a = w.getNode(i);
Node b = w.getNode((i + 1) % (w.getNodesCount() - 1));
return a.getCoor().greatCircleDistance(b.getCoor());
}
/**
* Given an array of doubles (but this could made generic very easily) sort
* into order and return the array of indexes such that, for a returned array
* x, a[x[i]] is sorted for ascending index i.
*
* This isn't efficient at all, but should be fine for the small arrays we're
* expecting. If this gets slow - replace it with some more efficient algorithm.
*
* @param a The array to sort.
* @return An array of indexes, the same size as the input, such that a[x[i]]
* is in sorted order.
*/
private int[] sortedIndexes(final double[] a) {
class SortWithIndex implements Comparable<SortWithIndex> {
public double x;
public int i;
SortWithIndex(double a, int b) {
x = a;
i = b;
}
@Override
public int compareTo(SortWithIndex o) {
return Double.compare(x, o.x);
}
}
final int length = a.length;
ArrayList<SortWithIndex> sortable = new ArrayList<>(length);
for (int i = 0; i < length; ++i) {
sortable.add(new SortWithIndex(a[i], i));
}
Collections.sort(sortable);
int[] indexes = new int[length];
for (int i = 0; i < length; ++i) {
indexes[i] = sortable.get(i).i;
}
return indexes;
}
/**
* Calculate "sideness" metric for each segment in a way.
*/
private double[] calculateSideness(Way w) {
final int length = w.getNodesCount() - 1;
double[] sideness = new double[length];
sideness[0] = calculateSideness(w.getNode(length - 1), w.getNode(0), w
.getNode(1), w.getNode(2));
for (int i = 1; i < length - 1; ++i) {
sideness[i] = calculateSideness(w.getNode(i - 1), w.getNode(i), w
.getNode(i + 1), w.getNode(i + 2));
}
sideness[length - 1] = calculateSideness(w.getNode(length - 2), w
.getNode(length - 1), w.getNode(length), w.getNode(1));
return sideness;
}
/**
* Calculate sideness of a single segment given the nodes which make up that
* segment and its previous and next segments in order. Sideness is calculated
* for the segment b-c.
*/
private double calculateSideness(Node a, Node b, Node c, Node d) {
final double ndx = b.getCoor().getX() - a.getCoor().getX();
final double pdx = d.getCoor().getX() - c.getCoor().getX();
final double ndy = b.getCoor().getY() - a.getCoor().getY();
final double pdy = d.getCoor().getY() - c.getCoor().getY();
return (ndx * pdx + ndy * pdy)
/ Math.sqrt((ndx * ndx + ndy * ndy) * (pdx * pdx + pdy * pdy));
}
/**
* Creates a new node at the interpolated position between the argument
* nodes. Interpolates linearly in projected coordinates.
*
* If new node coordinate matches a or b coordinates, a or b is returned.
*
* @param a First node, at which f=0.
* @param b Last node, at which f=1.
* @param f Fractional position between first and last nodes.
* @return A new node at the interpolated position (or a or b in case if f ≈ 0 or f ≈ 1).
*/
private Node interpolateNode(Node a, Node b, double f) {
Node n = new Node(a.getEastNorth().interpolate(b.getEastNorth(), f));
if (n.getCoor().equalsEpsilon(a.getCoor()))
return a;
if (n.getCoor().equalsEpsilon(b.getCoor()))
return b;
return n;
}
@Override
protected void updateEnabledState() {
setEnabled(getLayerManager().getEditDataSet() != null);
}
}