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}