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