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