/* * Copyright 2010, 2011, 2012 mapsforge.org * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later version. * * 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with * this program. If not, see <http://www.gnu.org/licenses/>. */ package org.mapsforge.map.writer; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import org.mapsforge.map.writer.model.TDNode; import org.mapsforge.map.writer.model.TDWay; import org.mapsforge.map.writer.util.JTSUtils; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.Polygon; //TODO could be implemented more efficiently with graphs: each line string is an edge, use an undirected graph and search for strongly connected components class WayPolygonizer { private static final int MIN_NODES_POLYGON = 4; private final GeometryFactory geometryFactory = new GeometryFactory(); private List<Deque<TDWay>> polygons; private List<TDWay> dangling; private List<TDWay> illegal; private Map<Integer, List<Integer>> outerToInner; /** * Tries to merge ways to closed polygons. The ordering of waynodes is preserved during the merge process. * * @param ways * An array of ways that should be merged. Ways may be given in any order and may already be closed. */ void mergePolygons(TDWay[] ways) { this.polygons = new ArrayList<Deque<TDWay>>(); this.dangling = new ArrayList<TDWay>(); this.illegal = new ArrayList<TDWay>(); Deque<TDWay> ungroupedWays = new ArrayDeque<TDWay>(); // initially all ways are ungrouped for (TDWay tdWay : ways) { // reset reversed flag, may already be set when way is part of another relation tdWay.setReversedInRelation(false); // first extract all way that are closed polygons in their own right if (isClosedPolygon(tdWay)) { if (tdWay.getWayNodes().length < MIN_NODES_POLYGON) { this.illegal.add(tdWay); } else { Deque<TDWay> cluster = new ArrayDeque<TDWay>(); cluster.add(tdWay); this.polygons.add(cluster); } } else { ungroupedWays.add(tdWay); } } // all ways have been polygons, nice! if (ungroupedWays.isEmpty()) { return; } if (ungroupedWays.size() == 1) { this.dangling.add(ungroupedWays.getFirst()); return; } boolean startNewPolygon = true; while (true) { boolean merge = false; if (startNewPolygon) { // we start a new polygon either during first iteration or when // the previous iterations merged ways to a closed polygon and there // are still ungrouped ways left Deque<TDWay> cluster = new ArrayDeque<TDWay>(); // get the first way of the yet ungrouped ways and form a new group cluster.add(ungroupedWays.removeFirst()); this.polygons.add(cluster); startNewPolygon = false; } // test if we can merge the current polygon with an ungrouped way Iterator<TDWay> it = ungroupedWays.iterator(); while (it.hasNext()) { TDWay current = it.next(); Deque<TDWay> currentPolygonSegments = this.polygons.get(this.polygons.size() - 1); // first way in current polygon TDWay c1Start = currentPolygonSegments.getFirst(); // last way in current polygon TDWay c1End = currentPolygonSegments.getLast(); long startFirst = c1Start.isReversedInRelation() ? c1Start.getWayNodes()[c1Start.getWayNodes().length - 1] .getId() : c1Start.getWayNodes()[0].getId(); long endLast = c1End.isReversedInRelation() ? c1End.getWayNodes()[0].getId() : c1End.getWayNodes()[c1End.getWayNodes().length - 1].getId(); long currentFirst = current.getWayNodes()[0].getId(); long currentLast = current.getWayNodes()[current.getWayNodes().length - 1].getId(); // current way end connects to the start of the current polygon (correct direction) if (startFirst == currentLast) { merge = true; it.remove(); // add way to start of current polygon currentPolygonSegments.offerFirst(current); } // // current way start connects to the start of the current polygon (reversed // direction) else if (startFirst == currentFirst) { current.setReversedInRelation(true); merge = true; it.remove(); currentPolygonSegments.offerFirst(current); } // current way start connects to the end of the current polygon (correct direction) else if (endLast == currentFirst) { merge = true; it.remove(); // add way to end of current polygon currentPolygonSegments.offerLast(current); } // // current way end connects to the end of the current polygon (reversed direction) else if (endLast == currentLast) { current.setReversedInRelation(true); merge = true; it.remove(); // add way to end of current polygon currentPolygonSegments.offerLast(current); } } Deque<TDWay> currentCluster = this.polygons.get(this.polygons.size() - 1); boolean closed = isClosedPolygon(currentCluster); // not a closed polygon and no more ways to merge if (!closed) { if (ungroupedWays.isEmpty() || !merge) { this.dangling.addAll(this.polygons.get(this.polygons.size() - 1)); // may be a non operation when ungrouped is empty this.dangling.addAll(ungroupedWays); this.polygons.remove(this.polygons.size() - 1); return; } } else { // built a closed polygon and no more ways left --> we are finished if (ungroupedWays.isEmpty()) { return; } startNewPolygon = true; } // if we are here, the polygon is not yet closed, but there are also some ungrouped ways // which may be merge-able in the next iteration } } void relatePolygons() { this.outerToInner = new HashMap<Integer, List<Integer>>(); if (this.polygons.isEmpty()) { return; } Polygon[] polygonGeometries = new Polygon[this.polygons.size()]; int i = 0; for (Deque<TDWay> polygon : this.polygons) { polygonGeometries[i++] = this.geometryFactory.createPolygon( this.geometryFactory.createLinearRing(toCoordinates(polygon)), null); } this.outerToInner = new HashMap<Integer, List<Integer>>(); HashSet<Integer> inner = new HashSet<Integer>(); for (int k = 0; k < polygonGeometries.length; k++) { if (inner.contains(Integer.valueOf(k))) { continue; } for (int l = k + 1; l < polygonGeometries.length; l++) { if (inner.contains(Integer.valueOf(l))) { continue; } if (polygonGeometries[k].covers(polygonGeometries[l])) { List<Integer> inners = this.outerToInner.get(Integer.valueOf(k)); if (inners == null) { inners = new ArrayList<Integer>(); this.outerToInner.put(Integer.valueOf(k), inners); } inners.add(Integer.valueOf(l)); inner.add(Integer.valueOf(l)); } else if (!this.outerToInner.containsKey(Integer.valueOf(k)) && polygonGeometries[l].covers(polygonGeometries[k])) { List<Integer> inners = this.outerToInner.get(Integer.valueOf(l)); if (inners == null) { inners = new ArrayList<Integer>(); this.outerToInner.put(Integer.valueOf(l), inners); } inners.add(Integer.valueOf(k)); inner.add(Integer.valueOf(k)); } } // single polygon without any inner polygons if (!this.outerToInner.containsKey(Integer.valueOf(k)) && !inner.contains(Integer.valueOf(k))) { this.outerToInner.put(Integer.valueOf(k), null); } } } void polygonizeAndRelate(TDWay[] ways) { mergePolygons(ways); relatePolygons(); } List<Deque<TDWay>> getPolygons() { return this.polygons; } List<TDWay> getDangling() { return this.dangling; } List<TDWay> getIllegal() { return this.illegal; } Map<Integer, List<Integer>> getOuterToInner() { return this.outerToInner; } private static boolean isClosedPolygon(Deque<TDWay> currentPolygonSegments) { TDWay c1Start = currentPolygonSegments.getFirst(); TDWay c1End = currentPolygonSegments.getLast(); long startFirst = c1Start.isReversedInRelation() ? c1Start.getWayNodes()[c1Start.getWayNodes().length - 1] .getId() : c1Start.getWayNodes()[0].getId(); long endLast = c1End.isReversedInRelation() ? c1End.getWayNodes()[0].getId() : c1End.getWayNodes()[c1End .getWayNodes().length - 1].getId(); return startFirst == endLast; } private static boolean isClosedPolygon(TDWay way) { TDNode[] waynodes = way.getWayNodes(); return waynodes[0].getId() == waynodes[waynodes.length - 1].getId(); } private static Coordinate[] toCoordinates(Collection<TDWay> linestrings) { Coordinate[][] temp = new Coordinate[linestrings.size()][]; int i = 0; int n = 0; for (TDWay tdWay : linestrings) { temp[i] = JTSUtils.toCoordinates(tdWay); n += temp[i].length; ++i; } Coordinate[] res = new Coordinate[n]; int pos = 0; for (i = 0; i < temp.length; i++) { System.arraycopy(temp[i], 0, res, pos, temp[i].length); pos += temp[i].length; } return res; } class PolygonMergeException extends Exception { private static final long serialVersionUID = 1L; } }