/* * Copyright (C) 2007 Steve Ratcliffe * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * */ package uk.me.parabola.mkgmap.filters; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.mkgmap.general.MapElement; import uk.me.parabola.mkgmap.general.MapLine; /** * This is a filter that smooths out lines at low resolutions. If the element * has no size at all at the given resolution, then it is not passed on down * the chain at all is excluded from the map at that resolution. */ public class DouglasPeuckerFilter implements MapFilter { //private static final double ERROR_DISTANCE = 5.4 / 2; //One unit is 5.4 m, so error dist is 2.6m //Can be increased more, but may lead to artifacts on T-crossings private final double filterDistance; private double maxErrorDistance; private int resolution; public DouglasPeuckerFilter(double filterDistance) { this.filterDistance = filterDistance; } public void init(FilterConfig config) { this.resolution = config.getResolution(); this.maxErrorDistance = filterDistance * (1<< config.getShift()); } /** * This applies to both lines and polygons. We are going to smooth out * the points in the line so that you do not get jaggies. * * @param element A map element that will be a line or a polygon. * @param next This is used to pass the possibly transformed element onward. */ public void doFilter(MapElement element, MapFilterChain next) { // First off we don't touch things if at the highest level of detail if (resolution == 24) { // XXX 24 is not necessarily the highest level. next.doFilter(element); return; } MapLine line = (MapLine) element; List<Coord> points = line.getPoints(); // Create a new list to rewrite the points into. Don't alter the original one List<Coord> coords = new ArrayList<>(points.size()); coords.addAll(points); // Loop runs downwards, as the list length gets modified while running int endIndex = coords.size()-1; for(int i = endIndex-1; i > 0; i--) { Coord p = coords.get(i); //int highwayCount = p.getHighwayCount(); // If a node in the line use the douglas peucker algorithm for upper segment // TODO: Should consider only nodes connected to roads visible at current resolution. if (p.preserved()) { // point is "preserved", don't remove it douglasPeucker(coords, i, endIndex, maxErrorDistance); endIndex = i; } } // Simplify the rest douglasPeucker(coords, 0, endIndex, maxErrorDistance); if (coords.size() == points.size()) next.doFilter(line); // nothing changed, no need to copy else { MapLine newline = line.copy(); newline.setPoints(coords); next.doFilter(newline); } } /** * Reduces point density by Douglas-Peucker algorithm * * @param points The list of points to simplify. * @param startIndex First index of segment. The point with this index will not be changed * @param endIndex Last index of segment. The point with this index will not be changed * @param allowedError Maximal allowed error to be introduced by simplification. * returns number of removed points. */ protected void douglasPeucker(List<Coord> points, int startIndex, int endIndex, double allowedError) { if (startIndex >= endIndex) return; double maxDistance = 0; //Highest distance int maxIndex = endIndex; //Index of highest distance Coord a = points.get(startIndex); Coord b = points.get(endIndex); // Find point with highest distance to line between start- and end-point. // handle also closed or nearly closed lines and spikes on straight lines for(int i = endIndex-1; i > startIndex; i--) { Coord p = points.get(i); double distance = p.shortestDistToLineSegment(a, b); if (distance > maxDistance) { maxDistance = distance; maxIndex = i; } } if (maxDistance > allowedError) { // Call recursive for both parts douglasPeucker(points, maxIndex, endIndex, allowedError); douglasPeucker(points, startIndex, maxIndex, allowedError); } else { // All points in tolerance, delete all of them. // Remove the end-point if it is the same as the start point if (a.highPrecEquals(b) && points.get(endIndex).preserved() == false) endIndex++; if (endIndex - startIndex > 4){ // faster than many repeated remove actions points.subList(startIndex+1, endIndex).clear(); return; } // Remove the points in between for (int i = endIndex - 1; i > startIndex; i--) { points.remove(i); } } } }