diff --git a/OsmAnd-java/src/main/java/net/osmand/data/Multipolygon.java b/OsmAnd-java/src/main/java/net/osmand/data/Multipolygon.java new file mode 100644 index 0000000000..bf8c0a4964 --- /dev/null +++ b/OsmAnd-java/src/main/java/net/osmand/data/Multipolygon.java @@ -0,0 +1,242 @@ +package net.osmand.data; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jts.geom.LinearRing; +import com.vividsolutions.jts.geom.MultiPolygon; +import com.vividsolutions.jts.geom.Polygon; +import net.osmand.osm.edit.Node; +import net.osmand.osm.edit.OsmMapUtils; +import net.osmand.util.Algorithms; + +public class Multipolygon { + private List innerRings, outerRings; + private Map> containedInnerInOuter = new LinkedHashMap>(); + + private float maxLat = -90; + private float minLat = 90; + private float maxLon = -180; + private float minLon = 180; + private long id; + + + public Multipolygon(List outer, List inner, long id) { + outerRings = outer; + innerRings = inner; + this.id = id; + updateRings(); + } + + public Multipolygon(Ring outer, List inner, long id, boolean checkedIsIn) { + outerRings = new ArrayList(); + outerRings.add(outer); + innerRings = inner; + this.id = id; + updateRings(checkedIsIn); + } + + public MultiPolygon toMultiPolygon() { + GeometryFactory geometryFactory = new GeometryFactory(); + MultiPolygon emptyMultiPolygon = geometryFactory.createMultiPolygon(new Polygon[0]); + List polygons = new ArrayList<>(); + for (Ring outerRing : outerRings) { + if (!outerRing.isClosed()) { + return emptyMultiPolygon; + } + List innerLinearRings = new ArrayList<>(); + Set innerRings = containedInnerInOuter.get(outerRing); + if (!Algorithms.isEmpty(innerRings)) { + for (Ring innerRing : innerRings) { + if (!innerRing.isClosed()) { + return emptyMultiPolygon; + } + innerLinearRings.add(innerRing.toLinearRing()); + } + } + polygons.add(geometryFactory.createPolygon(outerRing.toLinearRing(), innerLinearRings.toArray(new LinearRing[innerLinearRings.size()]))); + } + return geometryFactory.createMultiPolygon(polygons.toArray(new Polygon[polygons.size()])); + } + + public long getId() { + return id; + } + + private void updateRings() { + updateRings(false); + } + + private void updateRings(boolean checkedIsIn) { + maxLat = -90; + minLat = 90; + maxLon = -180; + minLon = 180; + for (Ring r : outerRings) { + for (Node n : r.getBorder()) { + maxLat = (float) Math.max(maxLat, n.getLatitude()); + minLat = (float) Math.min(minLat, n.getLatitude()); + maxLon = (float) Math.max(maxLon, n.getLongitude()); + minLon = (float) Math.min(minLon, n.getLongitude()); + } + } + // keep sorted + Collections.sort(outerRings); + for (Ring inner : innerRings) { + HashSet outContainingRings = new HashSet(); + if (checkedIsIn && outerRings.size() == 1) { + outContainingRings.add(outerRings.get(0)); + } else { + for (Ring out : outerRings) { + if (inner.isIn(out)) { + outContainingRings.add(out); + } + } + } + containedInnerInOuter.put(inner, outContainingRings); + } + // keep sorted + Collections.sort(innerRings); + } + + /** + * check if this multipolygon contains a point + * + * @param latitude lat to check + * @param longitude lon to check + * @return true if this multipolygon is correct and contains the point + */ + public boolean containsPoint(double latitude, double longitude) { + // fast check + if (maxLat + 0.3 < latitude || minLat - 0.3 > latitude || + maxLon + 0.3 < longitude || minLon - 0.3 > longitude) { + return false; + } + + Ring containedInOuter = null; + // use a sortedset to get the smallest outer containing the point + for (Ring outer : outerRings) { + if (outer.containsPoint(latitude, longitude)) { + containedInOuter = outer; + break; + } + } + + if (containedInOuter == null) { + return false; + } + + //use a sortedSet to get the smallest inner Ring + Ring containedInInner = null; + for (Ring inner : innerRings) { + if (inner.containsPoint(latitude, longitude)) { + containedInInner = inner; + break; + } + } + + if (containedInInner == null) return true; + if (outerRings.size() == 1) { + // return immediately false + return false; + } + + // if it is both, in an inner and in an outer, check if the inner is indeed the smallest one + Set s = containedInnerInOuter.get(containedInInner); + if (s == null) { + throw new IllegalStateException(); + } + return !s.contains(containedInOuter); + } + + /** + * check if this multipolygon contains a point + * + * @param point point to check + * @return true if this multipolygon is correct and contains the point + */ + public boolean containsPoint(LatLon point) { + + return containsPoint(point.getLatitude(), point.getLongitude()); + + } + + public int countOuterPolygons() { + return zeroSizeIfNull(outerRings); + } + + private int zeroSizeIfNull(Collection l) { + return l != null ? l.size() : 0; + } + + /** + * Get the weighted center of all nodes in this multiPolygon
+ * This only works when the ways have initialized nodes + * + * @return the weighted center + */ + public LatLon getCenterPoint() { + List points = new ArrayList(); + for (Ring w : outerRings) { + points.addAll(w.getBorder()); + } + if (points.isEmpty()) { + for (Ring w : innerRings) { + points.addAll(w.getBorder()); + } + } + + return OsmMapUtils.getWeightCenterForNodes(points); + } + + public void mergeWith(Multipolygon multipolygon) { + innerRings.addAll(multipolygon.innerRings); + outerRings.addAll(multipolygon.outerRings); + updateRings(); + } + + public boolean hasOpenedPolygons() { + return !areRingsComplete(); + } + + public boolean areRingsComplete() { + List l = outerRings; + for (Ring r : l) { + if (!r.isClosed()) { + return false; + } + } + l = innerRings; + for (Ring r : l) { + if (!r.isClosed()) { + return false; + } + } + return true; + } + + public QuadRect getLatLonBbox() { + if(minLat == 90) { + return new QuadRect(); + } + return new QuadRect(minLon, maxLat, maxLon, minLat); + } + + + public List getInnerRings() { + return innerRings; + } + + public List getOuterRings() { + return outerRings; + } + + +} diff --git a/OsmAnd-java/src/main/java/net/osmand/data/MultipolygonBuilder.java b/OsmAnd-java/src/main/java/net/osmand/data/MultipolygonBuilder.java new file mode 100644 index 0000000000..f7b741bc82 --- /dev/null +++ b/OsmAnd-java/src/main/java/net/osmand/data/MultipolygonBuilder.java @@ -0,0 +1,278 @@ +package net.osmand.data; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.commons.logging.Log; + +import gnu.trove.map.hash.TLongObjectHashMap; +import net.osmand.osm.edit.Node; +import net.osmand.osm.edit.Way; +import net.osmand.util.MapUtils; + +/** + * The idea of multipolygon: + * - we treat each outer way as closed polygon + * - multipolygon is always closed! + * - each way we try to assign to existing way and form + * so a more complex polygon + * - number of outer ways, is number of polygons + * + * @author Pavol Zibrita + */ +public class MultipolygonBuilder { + + /* package */ List outerWays = new ArrayList(); + /* package */ List innerWays = new ArrayList(); + + long id; + + /** + * Create a multipolygon with initialized outer and inner ways + * + * @param outers a list of outer ways + * @param inners a list of inner ways + */ + public MultipolygonBuilder(List outers, List inners) { + this(); + outerWays.addAll(outers); + innerWays.addAll(inners); + } + + public MultipolygonBuilder() { + id = -1L; + } + + public void setId(long newId) { + id = newId; + } + + public long getId() { + return id; + } + + public MultipolygonBuilder addInnerWay(Way w) { + innerWays.add(w); + return this; + } + + public List getOuterWays() { + return outerWays; + } + + public List getInnerWays() { + return innerWays; + } + + public MultipolygonBuilder addOuterWay(Way w) { + outerWays.add(w); + return this; + } + + /** + * Split this multipolygon in several separate multipolygons with one outer ring each + * + * @param log the stream to log problems to, if log = null, nothing will be logged + * @return a list with multipolygons which have exactly one outer ring + */ + public List splitPerOuterRing(Log log) { + SortedSet inners = new TreeSet(combineToRings(innerWays)); + ArrayList outers = combineToRings(outerWays); + ArrayList multipolygons = new ArrayList(); + // loop; start with the smallest outer ring + for (Ring outer : outers) { + ArrayList innersInsideOuter = new ArrayList(); + Iterator innerIt = inners.iterator(); + while (innerIt.hasNext()) { + Ring inner = innerIt.next(); + if (inner.isIn(outer)) { + innersInsideOuter.add(inner); + innerIt.remove(); + } + } + multipolygons.add(new Multipolygon(outer, innersInsideOuter, id, true)); + } + + if (inners.size() != 0 && log != null) { + log.warn("Multipolygon " + getId() + " has a mismatch in outer and inner rings"); + } + + return multipolygons; + } + + public Multipolygon build() { + return new Multipolygon(combineToRings(outerWays), combineToRings(innerWays), id); + } + + public ArrayList combineToRings(List ways) { + // make a list of multiLines (connecter pieces of way) + TLongObjectHashMap> multilineStartPoint = new TLongObjectHashMap>(); + TLongObjectHashMap> multilineEndPoint = new TLongObjectHashMap>(); + for (Way toAdd : ways) { + if (toAdd.getNodeIds().size() < 2) { + continue; + } + // iterate over the multiLines, and add the way to the correct one + Way changedWay = toAdd; + Way newWay; + do { + newWay = merge(multilineStartPoint, getLastId(changedWay), changedWay, + multilineEndPoint, getFirstId(changedWay)); + if(newWay == null) { + newWay = merge(multilineEndPoint, getFirstId(changedWay), changedWay, + multilineStartPoint, getLastId(changedWay)); + } + if(newWay == null) { + newWay = merge(multilineStartPoint, getFirstId(changedWay), changedWay, + multilineEndPoint, getLastId(changedWay)); + } + if(newWay == null) { + newWay = merge(multilineEndPoint, getLastId(changedWay), changedWay, + multilineStartPoint, getFirstId(changedWay)); + } + if(newWay != null) { + changedWay = newWay; + } + } while (newWay != null); + + addToMap(multilineStartPoint, getFirstId(changedWay), changedWay); + addToMap(multilineEndPoint, getLastId(changedWay), changedWay); + + } + + List multiLines = new ArrayList(); + for(List lst : multilineStartPoint.valueCollection()) { + multiLines.addAll(lst); + } + ArrayList result = new ArrayList(); + for (Way multiLine : multiLines) { + Ring r = new Ring(multiLine); + result.add(r); + } + return result; + } + + private Way merge(TLongObjectHashMap> endMap, long stNodeId, Way changedWay, + TLongObjectHashMap> startMap, long endNodeId) { + List lst = endMap.get(stNodeId); + if(lst != null && lst.size() > 0) { + Way candToMerge = lst.get(0); + Way newWay = combineTwoWaysIfHasPoints(candToMerge, changedWay); + List otherLst = startMap.get( + getLastId(candToMerge) == stNodeId ? getFirstId(candToMerge) : getLastId(candToMerge)); + boolean removed1 = lst.remove(candToMerge) ; + boolean removed2 = otherLst != null && otherLst.remove(candToMerge); + if(newWay == null || !removed1 || !removed2) { + throw new UnsupportedOperationException("Can't merge way: " + changedWay.getId() + " " + stNodeId + " -> " + endNodeId); + } + return newWay; + } + + return null; + } + + private void addToMap(TLongObjectHashMap> mp, long id, Way changedWay) { + List lst = mp.get(id); + if(lst == null) { + lst = new ArrayList<>(); + mp.put(id, lst); + } + lst.add(changedWay); + } + + + private long getId(Node n) { + if(n == null ) { + return - nextRandId(); + } + long l = MapUtils.get31TileNumberY(n.getLatitude()); + l = (l << 31) | MapUtils.get31TileNumberX(n.getLongitude()); + return l; + } + + /** + * make a new Way with the nodes from two other ways + * + * @param w1 the first way + * @param w2 the second way + * @return null if it is not possible + */ + private Way combineTwoWaysIfHasPoints(Way w1, Way w2) { + boolean combine = true; + boolean firstReverse = false; + boolean secondReverse = false; + long w1f = getFirstId(w1); + long w2f = getFirstId(w2); + long w1l = getLastId(w1); + long w2l = getLastId(w2); + if (w1f == w2f) { + firstReverse = true; + secondReverse = false; + } else if (w1l == w2f) { + firstReverse = false; + secondReverse = false; + } else if (w1l == w2l) { + firstReverse = false; + secondReverse = true; + } else if (w1f == w2l) { + firstReverse = true; + secondReverse = true; + } else { + combine = false; + } + if (combine) { + Way newWay = new Way(nextRandId()); + boolean nodePresent = w1.getNodes() != null || w1.getNodes().size() != 0; + int w1size = nodePresent ? w1.getNodes().size() : w1.getNodeIds().size(); + for (int i = 0; i < w1size; i++) { + int ind = firstReverse ? (w1size - 1 - i) : i; + if (nodePresent) { + newWay.addNode(w1.getNodes().get(ind)); + } else { + newWay.addNode(w1.getNodeIds().get(ind)); + } + } + int w2size = nodePresent ? w2.getNodes().size() : w2.getNodeIds().size(); + for (int i = 1; i < w2size; i++) { + int ind = secondReverse ? (w2size - 1 - i) : i; + if (nodePresent) { + newWay.addNode(w2.getNodes().get(ind)); + } else { + newWay.addNode(w2.getNodeIds().get(ind)); + } + } + return newWay; + } + return null; + + } + + private long getLastId(Way w1) { + return w1.getLastNodeId() > 0 ? w1.getLastNodeId(): getId(w1.getLastNode()); + } + + private long getFirstId(Way w1) { + return w1.getFirstNodeId() > 0 ? w1.getFirstNodeId(): getId(w1.getFirstNode()); + } + + + + private static long initialValue = -1000; + private final static long randomInterval = 5000; + + /** + * get a random long number + * + * @return + */ + private static long nextRandId() { + // exclude duplicates in one session (!) and be quazirandom every run + long val = initialValue - Math.round(Math.random() * randomInterval); + initialValue = val; + return val; + } + +} diff --git a/OsmAnd-java/src/main/java/net/osmand/data/Ring.java b/OsmAnd-java/src/main/java/net/osmand/data/Ring.java new file mode 100644 index 0000000000..4a51b007cd --- /dev/null +++ b/OsmAnd-java/src/main/java/net/osmand/data/Ring.java @@ -0,0 +1,369 @@ +package net.osmand.data; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.CoordinateList; +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jts.geom.LinearRing; +import net.osmand.osm.edit.Node; +import net.osmand.osm.edit.OsmMapUtils; +import net.osmand.osm.edit.Way; +import net.osmand.util.MapAlgorithms; + +/** + * A ring is a list of CONTIGUOUS ways that form a simple boundary or an area.

+ * + * @author sander + */ +public class Ring implements Comparable { + /** + * This is a list of the ways added by the user + * The order can be changed with methods from this class + */ +// private final ArrayList ways; + + private static final int INDEX_RING_NODES_FAST_CHECK = 100; + private static final int INDEX_SIZE = 100; + private double[] indexedRingIntervals = null; + private List[] indexedRingNodes = null; + + /** + * a concatenation of the ways to form the border + * this is NOT necessarily a CLOSED way + * The id is random, so this may never leave the Ring object + */ + private Way border; + + /** + * area can be asked a lot of times when comparing rings, cache it + */ + private double area = -1; + + + /** + * Construct a Ring with a list of ways + * + * @param ways the ways that make up the Ring + */ + Ring(Way w) { + border = w; + indexForFastCheck(); + } + + @SuppressWarnings("unchecked") + private void indexForFastCheck() { + if(border.getNodes().size() > INDEX_RING_NODES_FAST_CHECK) { + // calculate min/max lat + double maxLat = Double.MIN_VALUE; + double minLat = Double.MAX_VALUE; + Node lastNode = null; + for(Node n : border.getNodes()) { + if(n == null) { + continue; + } + lastNode = n; + if(n.getLatitude() > maxLat) { + maxLat = n.getLatitude(); + } else if(n.getLatitude() < minLat) { + minLat = n.getLatitude(); + } + } + maxLat += 0.0001; + minLat -= 0.0001; + // create interval array [minLat, minLat+interval, ..., maxLat] + double interval = (maxLat - minLat) / (INDEX_SIZE - 1); + indexedRingIntervals = new double[INDEX_SIZE]; + indexedRingNodes = new List[INDEX_SIZE]; + for(int i = 0; i < INDEX_SIZE; i++) { + indexedRingIntervals[i] = minLat + i * interval; + indexedRingNodes[i] = new ArrayList(); + } + // split nodes by intervals + Node prev = lastNode; + for(int i = 0; i < border.getNodes().size(); i++) { + Node current = border.getNodes().get(i); + if(current == null) { + continue; + } + int i1 = getIndexedLessOrEq(current.getLatitude()); + int i2 = getIndexedLessOrEq(prev.getLatitude()); + int min, max; + if(i1 > i2) { + min = i2; + max = i1; + } else { + min = i1; + max = i2; + } + for (int j = min; j <= max; j++) { + indexedRingNodes[j].add(prev); + indexedRingNodes[j].add(current); + } + prev = current; + } + } + + } + + private int getIndexedLessOrEq(double latitude) { + int ind1 = Arrays.binarySearch(indexedRingIntervals, latitude); + if(ind1 < 0) { + ind1 = -(ind1 + 1); + } + return ind1; + } + + /** + * check if this ring is closed by nature + * + * @return true if this ring is closed, false otherwise + */ + public boolean isClosed() { + return border.getFirstNodeId() == border.getLastNodeId(); + } + + public List getBorder() { + return border.getNodes(); + } + + + /** + * check if this Ring contains the node + * + * @param n the Node to check + * @return yes if the node is inside the ring + */ + public boolean containsNode(Node n) { + return containsPoint(n.getLatitude(), n.getLongitude()); + } + + /** + * check if this Ring contains the point + * + * @param latitude lat of the point + * @param longitude lon of the point + * @return yes if the point is inside the ring + */ + public boolean containsPoint(double latitude, double longitude) { + if(indexedRingIntervals != null) { + int intersections = 0; + int indx = getIndexedLessOrEq(latitude); + if(indx == 0 || indx >= indexedRingNodes.length) { + return false; + } + List lst = indexedRingNodes[indx]; + for (int k = 0; k < lst.size(); k += 2) { + Node first = lst.get(k); + Node last = lst.get(k + 1); + if (OsmMapUtils.ray_intersect_lon(first, last, latitude, longitude) != -360.0d) { + intersections++; + } + } + return intersections % 2 == 1; + } + return MapAlgorithms.containsPoint(getBorder(), latitude, longitude); + } + + /** + * Check if this is in Ring r + * @param r the ring to check + * @return true if this Ring is inside Ring r (false if it is undetermined) + */ + public boolean speedIsIn(Ring r) { + /* + * bi-directional check is needed because some concave rings can intersect + * and would only fail on one of the checks + */ + List points = this.getBorder(); + if(points.size() < 2) { + return false; + } + double minlat = points.get(0).getLatitude(); + double maxlat = points.get(0).getLatitude(); + double minlon = points.get(0).getLongitude(); + double maxlon = points.get(0).getLongitude(); + // r should contain all nodes of this + for (Node n : points) { + minlat = Math.min(n.getLatitude(), minlat); + maxlat = Math.max(n.getLatitude(), maxlat); + minlon = Math.min(n.getLongitude(), minlon); + maxlon = Math.max(n.getLongitude(), maxlon); + } + + // r should contain all nodes of this + if (!r.containsPoint(minlat, minlon)) { + return false; + } + if (!r.containsPoint(maxlat, minlon)) { + return false; + } + if (!r.containsPoint(minlat, maxlon)) { + return false; + } + if (!r.containsPoint(maxlat, maxlon)) { + return false; + } + // this should not contain a node from r + for (Node n : r.getBorder()) { + if(n.getLatitude() > minlat && n.getLatitude() < maxlat && + n.getLongitude() > minlon && n.getLongitude() < maxlon) { + return false; + } + } + + return true; + + } + + /** + * Check if this is in Ring r + * + * @param r the ring to check + * @return true if this Ring is inside Ring r + */ + public boolean isIn(Ring r) { + if(speedIsIn(r)) { + return true; + } + /* + * bi-directional check is needed because some concave rings can intersect + * and would only fail on one of the checks + */ + List points = this.getBorder(); + + // r should contain all nodes of this + for (Node n : points) { + if (!r.containsNode(n)) { + return false; + } + } + + points = r.getBorder(); + + // this should not contain a node from r + for (Node n : points) { + if (this.containsNode(n)) { + return false; + } + } + + return true; + + } + + + /** + * If this Ring is not complete + * (some ways are not initialized + * because they are not included in the OSM file)

+ * + * We are trying to close this Ring by using the other Ring.

+ * + * The other Ring must be complete, and the part of this Ring + * inside the other Ring must also be complete. + * + * @param other the other Ring (which is complete) used to close this one + */ + public void closeWithOtherRing(Ring other) { + List thisBorder = getBorder(); + List thisSwitchPoints = new ArrayList(); + + boolean insideOther = other.containsNode(thisBorder.get(0)); + + // Search the node pairs for which the ring goes inside or out the other + for (int i = 0; i < thisBorder.size(); i++) { + Node n = thisBorder.get(i); + if (other.containsNode(n) != insideOther) { + // we are getting out or in the boundary now. + // toggle switch + insideOther = !insideOther; + + thisSwitchPoints.add(i); + } + } + + List otherSwitchPoints = new ArrayList(); + + // Search the according node pairs in the other ring + for (int i : thisSwitchPoints) { + LatLon a = thisBorder.get(i - 1).getLatLon(); + LatLon b = thisBorder.get(i).getLatLon(); + otherSwitchPoints.add(getTheSegmentRingIntersectsSegment(a, b)); + } + + + + /* + * TODO: + * + * * Split the other Ring into ways from splitPoint to splitPoint + * + * * Split this ring into ways from splitPoint to splitPoint + * + * * Filter out the parts of way from this that are inside the other Ring + * Use the insideOther var and the switchPoints list for this. + * + * * For each two parts of way from this, search a part of way connecting the two. + * If there are two, take the shortest. + */ + } + + /** + * Get the segment of the Ring that intersects a segment + * going from point a to point b + * + * @param a the begin point of the segment + * @param b the end point of the segment + * @return an integer i which is the index so that the segment + * from getBorder().get(i-1) to getBorder().get(i) intersects with + * the segment from parameters a to b.

+ *

+ * 0 if the segment from a to b doesn't intersect with the Ring. + */ + private int getTheSegmentRingIntersectsSegment(LatLon a, LatLon b) { + List border = getBorder(); + for (int i = 1; i < border.size(); i++) { + LatLon c = border.get(i - 1).getLatLon(); + LatLon d = border.get(i).getLatLon(); + if (MapAlgorithms.linesIntersect( + a.getLatitude(), a.getLongitude(), + b.getLatitude(), b.getLongitude(), + c.getLatitude(), c.getLongitude(), + d.getLatitude(), d.getLongitude())) { + return i; + } + } + return 0; + + } + + public double getArea() { + if (area == -1) { + //cache the area + area = OsmMapUtils.getArea(getBorder()); + } + return area; + } + + public LinearRing toLinearRing() { + GeometryFactory geometryFactory = new GeometryFactory(); + CoordinateList coordinates = new CoordinateList(); + for (Node node : border.getNodes()) { + coordinates.add(new Coordinate(node.getLatitude(), node.getLongitude()), true); + } + coordinates.closeRing(); + return geometryFactory.createLinearRing(coordinates.toCoordinateArray()); + } + + /** + * Use area size as comparable metric + */ + @Override + public int compareTo(Ring r) { + return Double.compare(getArea(), r.getArea()); + } +} diff --git a/OsmAnd-java/src/main/java/net/osmand/osm/edit/OsmMapUtils.java b/OsmAnd-java/src/main/java/net/osmand/osm/edit/OsmMapUtils.java index b318953115..b083e88fd2 100644 --- a/OsmAnd-java/src/main/java/net/osmand/osm/edit/OsmMapUtils.java +++ b/OsmAnd-java/src/main/java/net/osmand/osm/edit/OsmMapUtils.java @@ -8,11 +8,17 @@ import java.util.List; import java.util.PriorityQueue; import net.osmand.data.LatLon; +import net.osmand.data.Multipolygon; +import net.osmand.data.MultipolygonBuilder; +import net.osmand.data.Ring; import net.osmand.osm.edit.Relation.RelationMember; +import net.osmand.util.Algorithms; import net.osmand.util.MapAlgorithms; import net.osmand.util.MapUtils; public class OsmMapUtils { + + private static final double POLY_CENTER_PRECISION= 1e-6; public static double getDistance(Node e1, Node e2) { return MapUtils.getDistance(e1.getLatitude(), e1.getLongitude(), e2.getLatitude(), e2.getLongitude()); @@ -33,6 +39,38 @@ public class OsmMapUtils { return getWeightCenterForWay(((Way) e)); } else if (e instanceof Relation) { List list = new ArrayList(); + if (e.getTag("type").equals("multipolygon")) { + MultipolygonBuilder original = new MultipolygonBuilder(); + original.setId(e.getId()); + + // fill the multipolygon with all ways from the Relation + for (RelationMember es : ((Relation) e).getMembers()) { + if (es.getEntity() instanceof Way) { + boolean inner = "inner".equals(es.getRole()); //$NON-NLS-1$ + if (inner) { + original.addInnerWay((Way) es.getEntity()); + } else if("outer".equals(es.getRole())){ + original.addOuterWay((Way) es.getEntity()); + } + } + } + + List multipolygons = original.splitPerOuterRing(null); + if (!Algorithms.isEmpty(multipolygons)){ + Multipolygon m = multipolygons.get(0); + List out = m.getOuterRings().get(0).getBorder(); + List> inner = new ArrayList>(); + if(!Algorithms.isEmpty(out)) { + for (Ring r : m.getInnerRings()) { + inner.add(r.getBorder()); + } + } + if (!Algorithms.isEmpty(out)) { + return getComplexPolyCenter(out, inner); + } + } + } + for (RelationMember fe : ((Relation) e).getMembers()) { LatLon c = null; // skip relations to avoid circular dependencies @@ -48,16 +86,26 @@ public class OsmMapUtils { return null; } - public static LatLon getComplexPolyCenter(Collection nodes) { - double precision = 1e-5; //where to set precision constant? - + public static LatLon getComplexPolyCenter(Collection outer, List> inner) { final List> rings = new ArrayList<>(); List outerRing = new ArrayList<>(); - for (Node n : nodes) { + + for (Node n : outer) { outerRing.add(new LatLon(n.getLatitude(), n.getLongitude())); } rings.add(outerRing); - return getPolylabelPoint(rings, precision); + if (!Algorithms.isEmpty(inner)) { + for (List ring: inner) { + if (!Algorithms.isEmpty(ring)) { + List ringll = new ArrayList(); + for (Node n : ring) { + ringll.add(n.getLatLon()); + } + rings.add(ringll); + } + } + } + return getPolylabelPoint(rings); } public static LatLon getWeightCenter(Collection nodes) { @@ -109,7 +157,7 @@ public class OsmMapUtils { area = false; } } - LatLon ll = area ? getComplexPolyCenter(nodes) : getWeightCenterForNodes(nodes); + LatLon ll = area ? getComplexPolyCenter(nodes, null) : getWeightCenterForNodes(nodes); if(ll == null) { return null; } @@ -464,11 +512,10 @@ public class OsmMapUtils { /** * Calculate "visual" center point of polygons (based on Mapbox' polylabel algorithm) * @param rings - list of lists of nodes - * @param precision - precision of calculation, should be small, like 1e-4 or less. * @return coordinates of calculated center */ - private static LatLon getPolylabelPoint(List> rings, double precision) { + public static LatLon getPolylabelPoint(List> rings) { // find the bounding box of the outer ring double minX = Double.MAX_VALUE; double minY = Double.MAX_VALUE; @@ -520,7 +567,7 @@ public class OsmMapUtils { // do not drill down further if there's no chance of a better solution // System.out.println(String.format("check for precision: cell.max - bestCell.d = %f Precision: %f", cell.max, precision)); - if (cell.max - bestCell.d <= precision) continue; + if (cell.max - bestCell.d <= POLY_CENTER_PRECISION) continue; // split the cell into four cells h = cell.h / 2; diff --git a/OsmAnd/res/layout/fragment_import.xml b/OsmAnd/res/layout/fragment_import.xml index c04a748780..e3a8e18bfa 100644 --- a/OsmAnd/res/layout/fragment_import.xml +++ b/OsmAnd/res/layout/fragment_import.xml @@ -53,9 +53,7 @@ android:gravity="start|center_vertical" android:maxLines="1" android:paddingLeft="@dimen/content_padding_small" - android:paddingTop="@dimen/content_padding_half" android:paddingRight="@dimen/content_padding_small" - android:paddingBottom="@dimen/content_padding_half" android:text="@string/shared_string_select_all" android:textColor="?attr/active_color_basic" android:textSize="@dimen/default_desc_text_size" @@ -80,12 +78,10 @@ android:layout_height="match_parent" android:background="?attr/selectableItemBackground" android:ellipsize="end" - android:gravity="start|center_vertical" + android:gravity="center" android:maxLines="1" android:paddingLeft="@dimen/content_padding_small" - android:paddingTop="@dimen/content_padding_half" android:paddingRight="@dimen/content_padding_small" - android:paddingBottom="@dimen/content_padding_half" android:text="@string/shared_string_continue" android:textColor="?attr/dlg_btn_primary_text" android:textSize="@dimen/default_desc_text_size" @@ -109,22 +105,24 @@ android:layout_width="match_parent" android:layout_height="@dimen/toolbar_height_expanded" android:background="?attr/colorPrimary" + osmand:expandedTitleMarginBottom="@dimen/content_padding_small" + osmand:expandedTitleMarginEnd="@dimen/content_padding" + osmand:expandedTitleMarginStart="@dimen/content_padding" osmand:collapsedTitleTextAppearance="@style/AppBarTitle" - osmand:expandedTitleMarginStart="16dp" - osmand:expandedTitleMarginBottom="12dp" osmand:expandedTitleGravity="start|bottom" osmand:expandedTitleTextAppearance="@style/AppBarTitle" - tools:title="@string/shared_string_import" osmand:layout_scrollFlags="scroll|exitUntilCollapsed"> + osmand:title="@string/shared_string_import"> diff --git a/OsmAnd/res/layout/fragment_import_duplicates.xml b/OsmAnd/res/layout/fragment_import_duplicates.xml index c72a37309e..62e4d7d235 100644 --- a/OsmAnd/res/layout/fragment_import_duplicates.xml +++ b/OsmAnd/res/layout/fragment_import_duplicates.xml @@ -112,6 +112,9 @@ android:layout_width="match_parent" android:layout_height="@dimen/toolbar_height_expanded" android:background="?attr/colorPrimary" + osmand:expandedTitleMarginBottom="@dimen/content_padding_small" + osmand:expandedTitleMarginEnd="@dimen/content_padding" + osmand:expandedTitleMarginStart="@dimen/content_padding" osmand:collapsedTitleTextAppearance="@style/AppBarTitle" osmand:expandedTitleGravity="start|bottom" osmand:expandedTitleTextAppearance="@style/AppBarTitle" @@ -122,6 +125,8 @@ android:layout_width="match_parent" android:layout_height="@dimen/toolbar_height" android:minHeight="@dimen/toolbar_height" + osmand:titleMarginEnd="0dp" + osmand:titleMarginStart="0dp" osmand:layout_collapseMode="pin" osmand:layout_scrollFlags="scroll|enterAlways|exitUntilCollapsed" tools:title="@string/import_duplicates_title"> diff --git a/OsmAnd/res/layout/list_item_header_import.xml b/OsmAnd/res/layout/list_item_header_import.xml index 381f2df543..a1915cf2fe 100644 --- a/OsmAnd/res/layout/list_item_header_import.xml +++ b/OsmAnd/res/layout/list_item_header_import.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + xmlns:osmand="http://schemas.android.com/apk/res-auto" android:orientation="vertical">