// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.fastdraw;
import java.awt.Point;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.gui.MapView;
public class DrawnPolyLine {
MapView mv;
private LinkedList<LatLon> points = new LinkedList<>();
private LinkedList<LatLon> simplePoints = new LinkedList<>();
private Set<LatLon> used;
private Set<LatLon> fixed = new HashSet<>();
private int lastIdx;
private boolean closedFlag;
public DrawnPolyLine() {
clear();
}
public void setMv(MapView mv) {
this.mv = mv;
}
boolean isFixed(LatLon pp2) {
return fixed.contains(pp2);
}
double getLength() {
List<LatLon> pts = getPoints();
Iterator<LatLon> it1, it2;
LatLon pp1, pp2;
if (pts.size() < 2) return 0;
it1 = pts.listIterator(0);
it2 = pts.listIterator(1);
double len = 0;
for (int i = 0; i < pts.size() - 1; i++) {
pp1 = it1.next();
pp2 = it2.next();
len += pp1.greatCircleDistance(pp2);
}
return len;
}
LinkedList<LatLon> getPoints() {
if (simplePoints != null) return simplePoints; else return points;
}
boolean wasSimplified() {
return (simplePoints != null && simplePoints.size() > 0);
}
int findClosestPoint(Point p, double d) {
double x = p.x, y = p.y;
int n = points.size();
int idx = -1;
double dist, minD = 1e10;
for (int i = 0; i < n; i++) {
dist = Math.sqrt(getPoint(points.get(i)).distanceSq(x, y));
if (dist < d && dist < minD) {
idx = i;
minD = dist;
}
}
return idx;
}
void clear() {
points.clear();
used = null;
lastIdx = 0;
closedFlag = false;
fixed.clear();
simplePoints = null;
}
void undo() {
//if (points.size() > 0) points.removeLast();
if (lastIdx > 0 && lastIdx < points.size()) {
points.remove(lastIdx);
lastIdx--;
}
}
void fixPoint(LatLon p) {
fixed.add(p);
}
void addFixed(LatLon coor) {
addLast(coor);
fixed.add(coor);
}
public Set<LatLon> getFixedPoints() {
return fixed;
}
void addLast(LatLon coor) {
if (closedFlag && lastIdx > points.size()-1) return;
if (lastIdx >= points.size()-1) {
//
if (points.isEmpty() || !coor.equals(points.getLast())) {
points.addLast(coor);
if (points.size() > 1) lastIdx++;
}
} else {
// insert point into midlle of the line
if (points.isEmpty() || !coor.equals(points.get(lastIdx))) {
points.add(lastIdx+1, coor);
lastIdx++;
}
}
}
Point getLastPoint() {
if (lastIdx < points.size()) return getPoint(points.get(lastIdx));
else return null;
}
Point getPoint(LatLon p) {
return mv.getPoint(p);
}
int getSimplePointsCount() {
if (simplePoints != null) return simplePoints.size(); else return -1;
}
/**
* Increase epsilon to fit points count in maxPKM point per 1 km
*/
double autoSimplify(double initEpsilon, double ekf, int k, double maxPKM) {
double e = initEpsilon;
if (e < 1e-3) e = 1e-3;
if (ekf < 1+1e-2) ekf = 1.01;
simplify(e);
while (getNodesPerKm(k) > maxPKM && e < 1e3) {
e = e*ekf;
simplify(e);
//System.out.printf("eps=%f n=%d\n", e,simplePoints.size());
}
return e;
}
/**
* Simplified drawn line, not touching the nodes includes in "fixed" set.
*/
void simplify(double epsilon) {
//System.out.println("Simplify polyline...");
int n = points.size();
if (n < 3) return;
used = new HashSet<>(n);
int start = 0;
for (int i = 0; i < n; i++) {
LatLon p = points.get(i);
if (fixed.contains(p) || i == n - 1) {
if (start < 0) {
start = i;
} else {
douglasPeucker(start, i, epsilon, 0);
}
}
}
simplePoints = new LinkedList<>();
simplePoints.addAll(points);
simplePoints.retainAll(used);
//Main.map.mapView.repaint();
used = null;
}
/**
* Simplification of the line specified by "points" field.
* Remainin points are included to "used" set.
* @param start - starting index
* @param end - ending index
* @param epsilon - min point-to line distance in pixels (tolerance)
* @param depth - recursion level
*/
private void douglasPeucker(int start, int end, double epsilon, int depth) {
if (depth > 500) return;
if (end - start < 1) return; // incorrect invocation
LatLon first = points.get(start);
LatLon last = points.get(end);
Point firstp = getPoint(first);
Point lastp = getPoint(last);
used.add(first);
used.add(last);
if (end - start < 2) return;
int farthest_node = -1;
double farthest_dist = 0;
double d = 0;
for (int i = start + 1; i < end; i++) {
d = pointLineDistance(getPoint(points.get(i)), firstp, lastp);
if (d > farthest_dist) {
farthest_dist = d;
farthest_node = i;
}
}
if (farthest_dist > epsilon) {
douglasPeucker(start, farthest_node, epsilon, depth + 1);
douglasPeucker(farthest_node, end, epsilon, depth + 1);
}
}
/** Modfified funclion from LakeWalker
* Gets distance from point p1 to line p2-p3
*/
public double pointLineDistance(Point p1, Point p2, Point p3) {
double x0 = p1.x; double y0 = p1.y;
double x1 = p2.x; double y1 = p2.y;
double x2 = p3.x; double y2 = p3.y;
if (x2 == x1 && y2 == y1) {
return Math.hypot(x1 - x0, y1 - y0);
} else {
return Math.abs((x2-x1)*(y1-y0)-(x1-x0)*(y2-y1))/Math.hypot(x2 - x1, y2 - y1);
}
}
void closeLine() {
points.add(points.getFirst());
closedFlag = true;
}
boolean isClosed() {
return closedFlag;
}
void deleteNode(int idx) {
if (idx <= lastIdx) lastIdx--;
fixed.remove(points.get(idx));
points.remove(idx);
}
void tryToDeleteSegment(Point p) {
if (points.size() < 3) return;
LatLon start;
start = findBigSegment(p);
ListIterator<LatLon> it = points.listIterator();
LatLon pp;
boolean f = false;
int i = 0, idx = -1;
while (it.hasNext()) {
pp = it.next();
if (f && (fixed.contains(pp))) {
// if end of line fragment reached
lastIdx = idx;
return;
}
if (f && !it.hasNext()) {
// if end of whole line reached
closedFlag = false;
it.remove();
lastIdx = points.size()-1;
return;
}
// if we are deleting this segment
if (f) it.remove();
if (pp == start) {
f = true;
idx = i;
} // next node should be removed
i++;
}
lastIdx = points.size()-1;
}
/** find starting point of the polyline line fragment close to p
* line fragment = segments between two fixed (green) nodes
*/
LatLon findBigSegment(Point p) {
if (points.size() < 2) return null;
Iterator<LatLon> it1 = points.listIterator(0);
Iterator<LatLon> it2 = points.listIterator(1);
Point p1, p2;
LatLon pp1, pp2, start = null;
start = points.getFirst();
do {
pp1 = it1.next();
pp2 = it2.next();
p1 = getPoint(pp1);
p2 = getPoint(pp2);
// maintain segment start end end
if (fixed.contains(pp1)) {
start = pp1;
}
if (pointSegmentDistance(p, p1, p2) < 5) {
return start;
}
} while (it2.hasNext());
return null;
}
private double pointSegmentDistance(Point p, Point p1, Point p2) {
double a, b, x, y, l, kt, kn, dist;
x = p.x-p1.x;
y = p.y-p1.y;
a = p2.x-p1.x;
b = p2.y-p1.y;
l = Math.hypot(a, b);
if (l == 0) return Math.hypot(x, y); // p1 = p2
kt = (x*a+y*b)/l;
kn = Math.abs((-x*b+y*a)/l);
if (kt >= 0 && kt < l) dist = kn; else {
dist = Math.min(Math.hypot(x, y), Math.hypot(x-a, y-b));
}
return dist;
}
void clearSimplifiedVersion() {
simplePoints = null;
}
boolean isLastPoint(int i) {
return (lastIdx == i);
}
void moveToTheEnd() {
lastIdx = points.size()-1;
}
void toggleFixed(int idx) {
LatLon p = points.get(idx);
if (fixed.contains(p)) fixed.remove(p);
else fixed.add(p);
}
void moveNode(int dragNode, LatLon coor) {
LatLon dragged = points.get(dragNode);
// points.getLast().equals(points.getFirst()
if (closedFlag && points.getFirst().equals(dragged)) {
// move both ends
points.set(0, coor);
points.set(points.size()-1, coor);
} else {
points.set(dragNode, coor);
}
if (fixed.contains(dragged)) {
fixed.remove(dragged);
fixed.add(coor);
}
}
/**
* Returns maximum number of simplified line points divided by line segment length
* max((k-1) / (L(i,i+1)+L(i+1,i+2)+...+L(i+k-1,i+k))) [ i=1..n-k ]
* @param k - window size (number of points to average points per km
*/
public double getNodesPerKm(int k) {
List<LatLon> pts = simplePoints;
if (!wasSimplified()) pts = points;
int n = pts.size();
if (n < 2) return 0;
if (k < 2) k = 2;
if (k > n) k = n;
LatLon pp1, pp2 = null;
Iterator<LatLon> it1, it2;
it1 = pts.listIterator(0);
it2 = pts.listIterator(1);
double[] lens = new double[n];
for (int i = 0; i < n-1; i++) {
pp1 = it1.next();
//p1 = getPoint(pp1);
pp2 = it2.next();
//p2 =sa getPoint(pp2);
lens[i] = pp1.greatCircleDistance(pp2);
}
double pkm = 0, maxpkm = 0;
double len = 0;
int seg = 0; // averaged segments counts
for (int i = 1; i < n; i++) {
len += lens[i-1]; // add next next point
// remove old segment
if (i > k) {
seg = k;
len -= lens[i-k-1];
} else
seg = i;
if (i >= k || i == n-1) {
// len is length of points[i-windowSize] .. points[i]
if (len > 0) pkm = seg / len * 1000;
//System.out.println("i="+i+" pkm="+len+" pkm="+pkm);
if (pkm > maxpkm) maxpkm = pkm;
}
}
return Math.round(maxpkm);
}
int getPointCount() {
return points.size();
}
}