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}