001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm.visitor; 003 004import java.util.Collection; 005import java.util.function.DoubleUnaryOperator; 006 007import org.openstreetmap.josm.data.Bounds; 008import org.openstreetmap.josm.data.ProjectionBounds; 009import org.openstreetmap.josm.data.coor.EastNorth; 010import org.openstreetmap.josm.data.coor.ILatLon; 011import org.openstreetmap.josm.data.coor.LatLon; 012import org.openstreetmap.josm.data.osm.INode; 013import org.openstreetmap.josm.data.osm.IPrimitive; 014import org.openstreetmap.josm.data.osm.IRelation; 015import org.openstreetmap.josm.data.osm.IRelationMember; 016import org.openstreetmap.josm.data.osm.IWay; 017import org.openstreetmap.josm.data.osm.Node; 018import org.openstreetmap.josm.data.osm.OsmPrimitive; 019import org.openstreetmap.josm.data.osm.Relation; 020import org.openstreetmap.josm.data.osm.Way; 021import org.openstreetmap.josm.data.projection.ProjectionRegistry; 022import org.openstreetmap.josm.spi.preferences.Config; 023 024/** 025 * Calculates the total bounding rectangle of a series of {@link OsmPrimitive} objects, using the 026 * EastNorth values as reference. 027 * @author imi 028 */ 029public class BoundingXYVisitor implements OsmPrimitiveVisitor, PrimitiveVisitor { 030 /** default value for setting "edit.zoom-enlarge-bbox" */ 031 private static final double ENLARGE_DEFAULT = 0.0002; 032 033 private ProjectionBounds bounds; 034 035 @Override 036 public void visit(Node n) { 037 visit((ILatLon) n); 038 } 039 040 @Override 041 public void visit(Way w) { 042 visit((IWay<?>) w); 043 } 044 045 @Override 046 public void visit(Relation r) { 047 visit((IRelation<?>) r); 048 } 049 050 @Override 051 public void visit(INode n) { 052 visit((ILatLon) n); 053 } 054 055 @Override 056 public void visit(IWay<?> w) { 057 if (w.isIncomplete()) return; 058 for (INode n : w.getNodes()) { 059 visit(n); 060 } 061 } 062 063 @Override 064 public void visit(IRelation<?> r) { 065 // only use direct members 066 for (IRelationMember<?> m : r.getMembers()) { 067 if (!m.isRelation()) { 068 m.getMember().accept(this); 069 } 070 } 071 } 072 073 /** 074 * Visiting call for bounds. 075 * @param b bounds 076 */ 077 public void visit(Bounds b) { 078 if (b != null) { 079 ProjectionRegistry.getProjection().visitOutline(b, this::visit); 080 } 081 } 082 083 /** 084 * Visiting call for projection bounds. 085 * @param b projection bounds 086 */ 087 public void visit(ProjectionBounds b) { 088 if (b != null) { 089 visit(b.getMin()); 090 visit(b.getMax()); 091 } 092 } 093 094 /** 095 * Visiting call for lat/lon. 096 * @param latlon lat/lon 097 * @since 12725 (public for ILatLon parameter) 098 */ 099 public void visit(ILatLon latlon) { 100 if (latlon != null) { 101 visit(latlon.getEastNorth(ProjectionRegistry.getProjection())); 102 } 103 } 104 105 /** 106 * Visiting call for lat/lon. 107 * @param latlon lat/lon 108 */ 109 public void visit(LatLon latlon) { 110 visit((ILatLon) latlon); 111 } 112 113 /** 114 * Visiting call for east/north. 115 * @param eastNorth east/north 116 */ 117 public void visit(EastNorth eastNorth) { 118 if (eastNorth != null) { 119 if (bounds == null) { 120 bounds = new ProjectionBounds(eastNorth); 121 } else { 122 bounds.extend(eastNorth); 123 } 124 } 125 } 126 127 /** 128 * Determines if the visitor has a non null bounds area. 129 * @return {@code true} if the visitor has a non null bounds area 130 * @see ProjectionBounds#hasExtend 131 */ 132 public boolean hasExtend() { 133 return bounds != null && bounds.hasExtend(); 134 } 135 136 /** 137 * Returns the bounding box. 138 * @return The bounding box or <code>null</code> if no coordinates have passed 139 */ 140 public ProjectionBounds getBounds() { 141 return bounds; 142 } 143 144 /** 145 * Enlarges the calculated bounding box by 0.0002 degrees or user value 146 * given in edit.zoom-enlarge-bbox. 147 * If the bounding box has not been set (<code>min</code> or <code>max</code> 148 * equal <code>null</code>) this method does not do anything. 149 */ 150 public void enlargeBoundingBox() { 151 final double enlarge = Config.getPref().getDouble("edit.zoom-enlarge-bbox", ENLARGE_DEFAULT); 152 enlargeBoundingBox(enlarge, enlarge); 153 } 154 155 /** 156 * Enlarges the calculated bounding box by the specified number of degrees. 157 * If the bounding box has not been set (<code>min</code> or <code>max</code> 158 * equal <code>null</code>) this method does not do anything. 159 * 160 * @param enlargeDegreeX number of degrees to enlarge on each side along X 161 * @param enlargeDegreeY number of degrees to enlarge on each side along Y 162 */ 163 public void enlargeBoundingBox(double enlargeDegreeX, double enlargeDegreeY) { 164 if (bounds == null) 165 return; 166 LatLon minLatlon = ProjectionRegistry.getProjection().eastNorth2latlon(bounds.getMin()); 167 LatLon maxLatlon = ProjectionRegistry.getProjection().eastNorth2latlon(bounds.getMax()); 168 bounds = new ProjectionBounds(new LatLon( 169 Math.max(-90, minLatlon.lat() - enlargeDegreeY), 170 Math.max(-180, minLatlon.lon() - enlargeDegreeX)).getEastNorth(ProjectionRegistry.getProjection()), 171 new LatLon( 172 Math.min(90, maxLatlon.lat() + enlargeDegreeY), 173 Math.min(180, maxLatlon.lon() + enlargeDegreeX)).getEastNorth(ProjectionRegistry.getProjection())); 174 } 175 176 /** 177 * Enlarges the bounding box up to 0.0002 degrees, depending on its size and user 178 * settings in edit.zoom-enlarge-bbox. If the bounding box is small, it will be enlarged more in relation 179 * to its beginning size. The larger the bounding box, the smaller the change, 180 * down to 0.0 degrees. 181 * 182 * If the bounding box has not been set (<code>min</code> or <code>max</code> 183 * equal <code>null</code>) this method does not do anything. 184 * 185 * @since 14628 186 */ 187 public void enlargeBoundingBoxLogarithmically() { 188 if (bounds == null) 189 return; 190 final LatLon min = ProjectionRegistry.getProjection().eastNorth2latlon(bounds.getMin()); 191 final LatLon max = ProjectionRegistry.getProjection().eastNorth2latlon(bounds.getMax()); 192 final double deltaLat = max.lat() - min.lat(); 193 final double deltaLon = max.lon() - min.lon(); 194 final double enlarge = Config.getPref().getDouble("edit.zoom-enlarge-bbox", ENLARGE_DEFAULT); 195 196 final DoubleUnaryOperator enlargement = deltaDegress -> { 197 if (deltaDegress < enlarge) { 198 // delta is very small, use configured minimum value 199 return enlarge; 200 } 201 if (deltaDegress < 0.1) { 202 return enlarge - deltaDegress / 100; 203 } 204 return 0.0; 205 }; 206 enlargeBoundingBox(enlargement.applyAsDouble(deltaLon), enlargement.applyAsDouble(deltaLat)); 207 } 208 209 @Override 210 public String toString() { 211 return "BoundingXYVisitor["+bounds+']'; 212 } 213 214 /** 215 * Compute the bounding box of a collection of primitives. 216 * @param primitives the collection of primitives 217 */ 218 public void computeBoundingBox(Collection<? extends IPrimitive> primitives) { 219 if (primitives == null) return; 220 for (IPrimitive p: primitives) { 221 if (p == null) { 222 continue; 223 } 224 p.accept(this); 225 } 226 } 227}