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}