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 = null;
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    public void visit(Bounds b) {
051        if(b != null)
052        {
053            visit(b.getMin());
054            visit(b.getMax());
055        }
056    }
057
058    public void visit(ProjectionBounds b) {
059        if(b != null)
060        {
061            visit(b.getMin());
062            visit(b.getMax());
063        }
064    }
065
066    public void visit(LatLon latlon) {
067        if(latlon != null)
068        {
069            if(latlon instanceof CachedLatLon) {
070                visit(((CachedLatLon)latlon).getEastNorth());
071            } else {
072                visit(Main.getProjection().latlon2eastNorth(latlon));
073            }
074        }
075    }
076
077    public void visit(EastNorth eastNorth) {
078        if (eastNorth != null) {
079            if (bounds == null) {
080                bounds = new ProjectionBounds(eastNorth);
081            } else {
082                bounds.extend(eastNorth);
083            }
084        }
085    }
086
087    public boolean hasExtend()
088    {
089        return bounds != null && !bounds.getMin().equals(bounds.getMax());
090    }
091
092    /**
093     * @return The bounding box or <code>null</code> if no coordinates have passed
094     */
095    public ProjectionBounds getBounds() {
096        return bounds;
097    }
098
099    /**
100     * Enlarges the calculated bounding box by 0.002 degrees.
101     * If the bounding box has not been set (<code>min</code> or <code>max</code>
102     * equal <code>null</code>) this method does not do anything.
103     */
104    public void enlargeBoundingBox() {
105        enlargeBoundingBox(Main.pref.getDouble("edit.zoom-enlarge-bbox", 0.002));
106    }
107
108    /**
109     * Enlarges the calculated bounding box by the specified number of degrees.
110     * If the bounding box has not been set (<code>min</code> or <code>max</code>
111     * equal <code>null</code>) this method does not do anything.
112     *
113     * @param enlargeDegree
114     */
115    public void enlargeBoundingBox(double enlargeDegree) {
116        if (bounds == null)
117            return;
118        LatLon minLatlon = Main.getProjection().eastNorth2latlon(bounds.getMin());
119        LatLon maxLatlon = Main.getProjection().eastNorth2latlon(bounds.getMax());
120        bounds = new ProjectionBounds(
121                Main.getProjection().latlon2eastNorth(new LatLon(minLatlon.lat() - enlargeDegree, minLatlon.lon() - enlargeDegree)),
122                Main.getProjection().latlon2eastNorth(new LatLon(maxLatlon.lat() + enlargeDegree, maxLatlon.lon() + enlargeDegree)));
123    }
124
125    /**
126     * Enlarges the bounding box up to <code>maxEnlargePercent</code>, depending on
127     * its size. If the bounding box is small, it will be enlarged more in relation
128     * to its beginning size. The larger the bounding box, the smaller the change,
129     * down to the minimum of 1% enlargement.
130     * 
131     * Warning: if the bounding box only contains a single node, no expansion takes
132     * place because a node has no width/height. Use <code>enlargeToMinDegrees</code>
133     * instead.
134     * 
135     * Example: You specify enlargement to be up to 100%.
136     * 
137     *          Bounding box is a small house: enlargement will be 95–100%, i.e.
138     *          making enough space so that the house fits twice on the screen in
139     *          each direction.
140     * 
141     *          Bounding box is a large landuse, like a forest: Enlargement will
142     *          be 1–10%, i.e. just add a little border around the landuse.
143     * 
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     * @param maxEnlargePercent
148     */
149    public void enlargeBoundingBoxLogarithmically(double maxEnlargePercent) {
150        if (bounds == null)
151            return;
152
153        double diffEast = bounds.getMax().east() - bounds.getMin().east();
154        double diffNorth = bounds.getMax().north() - bounds.getMin().north();
155
156        double enlargeEast = Math.min(maxEnlargePercent - 10*Math.log(diffEast), 1)/100;
157        double enlargeNorth = Math.min(maxEnlargePercent - 10*Math.log(diffNorth), 1)/100;
158
159        visit(bounds.getMin().add(-enlargeEast/2, -enlargeNorth/2));
160        visit(bounds.getMax().add(+enlargeEast/2, +enlargeNorth/2));
161    }
162
163    /**
164     * Specify a degree larger than 0 in order to make the bounding box at least
165     * the specified size in width and height. The value is ignored if the
166     * bounding box is already larger than the specified amount.
167     * 
168     * If the bounding box has not been set (<code>min</code> or <code>max</code>
169     * equal <code>null</code>) this method does not do anything.
170     * 
171     * If the bounding box contains objects and is to be enlarged, the objects
172     * will be centered within the new bounding box.
173     * 
174     * @param size minimum width and height in meter
175     */
176    public void enlargeToMinSize(double size) {
177        if (bounds == null)
178            return;
179        // convert size from meters to east/north units
180        double en_size = size * Main.map.mapView.getScale() / Main.map.mapView.getDist100Pixel() * 100;
181        visit(bounds.getMin().add(-en_size/2, -en_size/2));
182        visit(bounds.getMax().add(+en_size/2, +en_size/2));
183    }
184
185
186    @Override public String toString() {
187        return "BoundingXYVisitor["+bounds+"]";
188    }
189
190    public void computeBoundingBox(Collection<? extends OsmPrimitive> primitives) {
191        if (primitives == null) return;
192        for (OsmPrimitive p: primitives) {
193            if (p == null) {
194                continue;
195            }
196            p.accept(this);
197        }
198    }
199}