001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm.visitor; 003 004import java.util.Collection; 005 006import org.openstreetmap.josm.Main; 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.gui.MainApplication; 022import org.openstreetmap.josm.gui.MapFrame; 023import org.openstreetmap.josm.spi.preferences.Config; 024 025/** 026 * Calculates the total bounding rectangle of a series of {@link OsmPrimitive} objects, using the 027 * EastNorth values as reference. 028 * @author imi 029 */ 030public class BoundingXYVisitor implements OsmPrimitiveVisitor, PrimitiveVisitor { 031 032 private ProjectionBounds bounds; 033 034 @Override 035 public void visit(Node n) { 036 visit((ILatLon) n); 037 } 038 039 @Override 040 public void visit(Way w) { 041 visit((IWay<?>) w); 042 } 043 044 @Override 045 public void visit(Relation r) { 046 visit((IRelation<?>) r); 047 } 048 049 @Override 050 public void visit(INode n) { 051 visit((ILatLon) n); 052 } 053 054 @Override 055 public void visit(IWay<?> w) { 056 if (w.isIncomplete()) return; 057 for (INode n : w.getNodes()) { 058 visit(n); 059 } 060 } 061 062 @Override 063 public void visit(IRelation<?> r) { 064 // only use direct members 065 for (IRelationMember<?> m : r.getMembers()) { 066 if (!m.isRelation()) { 067 m.getMember().accept(this); 068 } 069 } 070 } 071 072 /** 073 * Visiting call for bounds. 074 * @param b bounds 075 */ 076 public void visit(Bounds b) { 077 if (b != null) { 078 Main.getProjection().visitOutline(b, this::visit); 079 } 080 } 081 082 /** 083 * Visiting call for projection bounds. 084 * @param b projection bounds 085 */ 086 public void visit(ProjectionBounds b) { 087 if (b != null) { 088 visit(b.getMin()); 089 visit(b.getMax()); 090 } 091 } 092 093 /** 094 * Visiting call for lat/lon. 095 * @param latlon lat/lon 096 * @since 12725 (public for ILatLon parameter) 097 */ 098 public void visit(ILatLon latlon) { 099 if (latlon != null) { 100 visit(latlon.getEastNorth(Main.getProjection())); 101 } 102 } 103 104 /** 105 * Visiting call for lat/lon. 106 * @param latlon lat/lon 107 */ 108 public void visit(LatLon latlon) { 109 visit((ILatLon) latlon); 110 } 111 112 /** 113 * Visiting call for east/north. 114 * @param eastNorth east/north 115 */ 116 public void visit(EastNorth eastNorth) { 117 if (eastNorth != null) { 118 if (bounds == null) { 119 bounds = new ProjectionBounds(eastNorth); 120 } else { 121 bounds.extend(eastNorth); 122 } 123 } 124 } 125 126 /** 127 * Determines if the visitor has a non null bounds area. 128 * @return {@code true} if the visitor has a non null bounds area 129 * @see ProjectionBounds#hasExtend 130 */ 131 public boolean hasExtend() { 132 return bounds != null && bounds.hasExtend(); 133 } 134 135 /** 136 * @return The bounding box or <code>null</code> if no coordinates have passed 137 */ 138 public ProjectionBounds getBounds() { 139 return bounds; 140 } 141 142 /** 143 * Enlarges the calculated bounding box by 0.002 degrees. 144 * If the bounding box has not been set (<code>min</code> or <code>max</code> 145 * equal <code>null</code>) this method does not do anything. 146 */ 147 public void enlargeBoundingBox() { 148 enlargeBoundingBox(Config.getPref().getDouble("edit.zoom-enlarge-bbox", 0.002)); 149 } 150 151 /** 152 * Enlarges the calculated bounding box by the specified number of degrees. 153 * If the bounding box has not been set (<code>min</code> or <code>max</code> 154 * equal <code>null</code>) this method does not do anything. 155 * 156 * @param enlargeDegree number of degrees to enlarge on each side 157 */ 158 public void enlargeBoundingBox(double enlargeDegree) { 159 if (bounds == null) 160 return; 161 LatLon minLatlon = Main.getProjection().eastNorth2latlon(bounds.getMin()); 162 LatLon maxLatlon = Main.getProjection().eastNorth2latlon(bounds.getMax()); 163 bounds = new ProjectionBounds(new LatLon( 164 Math.max(-90, minLatlon.lat() - enlargeDegree), 165 Math.max(-180, minLatlon.lon() - enlargeDegree)).getEastNorth(Main.getProjection()), 166 new LatLon( 167 Math.min(90, maxLatlon.lat() + enlargeDegree), 168 Math.min(180, maxLatlon.lon() + enlargeDegree)).getEastNorth(Main.getProjection())); 169 } 170 171 /** 172 * Enlarges the bounding box up to <code>maxEnlargePercent</code>, depending on 173 * its size. If the bounding box is small, it will be enlarged more in relation 174 * to its beginning size. The larger the bounding box, the smaller the change, 175 * down to the minimum of 1% enlargement. 176 * 177 * Warning: if the bounding box only contains a single node, no expansion takes 178 * place because a node has no width/height. Use <code>enlargeToMinDegrees</code> 179 * instead. 180 * 181 * Example: You specify enlargement to be up to 100%. 182 * 183 * Bounding box is a small house: enlargement will be 95–100%, i.e. 184 * making enough space so that the house fits twice on the screen in 185 * each direction. 186 * 187 * Bounding box is a large landuse, like a forest: Enlargement will 188 * be 1–10%, i.e. just add a little border around the landuse. 189 * 190 * If the bounding box has not been set (<code>min</code> or <code>max</code> 191 * equal <code>null</code>) this method does not do anything. 192 * 193 * @param maxEnlargePercent maximum enlargement in percentage (100.0 for 100%) 194 */ 195 public void enlargeBoundingBoxLogarithmically(double maxEnlargePercent) { 196 if (bounds == null) 197 return; 198 199 double diffEast = bounds.getMax().east() - bounds.getMin().east(); 200 double diffNorth = bounds.getMax().north() - bounds.getMin().north(); 201 202 double enlargeEast = Math.min(maxEnlargePercent - 10*Math.log(diffEast), 1)/100; 203 double enlargeNorth = Math.min(maxEnlargePercent - 10*Math.log(diffNorth), 1)/100; 204 205 visit(bounds.getMin().add(-enlargeEast/2, -enlargeNorth/2)); 206 visit(bounds.getMax().add(+enlargeEast/2, +enlargeNorth/2)); 207 } 208 209 /** 210 * Specify a degree larger than 0 in order to make the bounding box at least 211 * the specified size in width and height. The value is ignored if the 212 * bounding box is already larger than the specified amount. 213 * 214 * If the bounding box has not been set (<code>min</code> or <code>max</code> 215 * equal <code>null</code>) this method does not do anything. 216 * 217 * If the bounding box contains objects and is to be enlarged, the objects 218 * will be centered within the new bounding box. 219 * 220 * @param size minimum width and height in meter 221 */ 222 public void enlargeToMinSize(double size) { 223 if (bounds == null) 224 return; 225 // convert size from meters to east/north units 226 MapFrame map = MainApplication.getMap(); 227 double enSize = size * map.mapView.getScale() / map.mapView.getDist100Pixel() * 100; 228 visit(bounds.getMin().add(-enSize/2, -enSize/2)); 229 visit(bounds.getMax().add(+enSize/2, +enSize/2)); 230 } 231 232 @Override 233 public String toString() { 234 return "BoundingXYVisitor["+bounds+']'; 235 } 236 237 /** 238 * Compute the bounding box of a collection of primitives. 239 * @param primitives the collection of primitives 240 */ 241 public void computeBoundingBox(Collection<? extends IPrimitive> primitives) { 242 if (primitives == null) return; 243 for (IPrimitive p: primitives) { 244 if (p == null) { 245 continue; 246 } 247 p.accept(this); 248 } 249 } 250}