001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm.visitor;
003
004import java.util.Collection;
005import java.util.function.DoubleUnaryOperator;
006
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.data.projection.ProjectionRegistry;
022import org.openstreetmap.josm.spi.preferences.Config;
023
024/**
025 * Calculates the total bounding rectangle of a series of {@link OsmPrimitive} objects, using the
026 * EastNorth values as reference.
027 * @author imi
028 */
029public class BoundingXYVisitor implements OsmPrimitiveVisitor, PrimitiveVisitor {
030    /** default value for setting "edit.zoom-enlarge-bbox" */
031    private static final double ENLARGE_DEFAULT = 0.0002;
032
033    private ProjectionBounds bounds;
034
035    @Override
036    public void visit(Node n) {
037        visit((ILatLon) n);
038    }
039
040    @Override
041    public void visit(Way w) {
042        visit((IWay<?>) w);
043    }
044
045    @Override
046    public void visit(Relation r) {
047        visit((IRelation<?>) r);
048    }
049
050    @Override
051    public void visit(INode n) {
052        visit((ILatLon) n);
053    }
054
055    @Override
056    public void visit(IWay<?> w) {
057        if (w.isIncomplete()) return;
058        for (INode n : w.getNodes()) {
059            visit(n);
060        }
061    }
062
063    @Override
064    public void visit(IRelation<?> r) {
065        // only use direct members
066        for (IRelationMember<?> m : r.getMembers()) {
067            if (!m.isRelation()) {
068                m.getMember().accept(this);
069            }
070        }
071    }
072
073    /**
074     * Visiting call for bounds.
075     * @param b bounds
076     */
077    public void visit(Bounds b) {
078        if (b != null) {
079            ProjectionRegistry.getProjection().visitOutline(b, this::visit);
080        }
081    }
082
083    /**
084     * Visiting call for projection bounds.
085     * @param b projection bounds
086     */
087    public void visit(ProjectionBounds b) {
088        if (b != null) {
089            visit(b.getMin());
090            visit(b.getMax());
091        }
092    }
093
094    /**
095     * Visiting call for lat/lon.
096     * @param latlon lat/lon
097     * @since 12725 (public for ILatLon parameter)
098     */
099    public void visit(ILatLon latlon) {
100        if (latlon != null) {
101            visit(latlon.getEastNorth(ProjectionRegistry.getProjection()));
102        }
103    }
104
105    /**
106     * Visiting call for lat/lon.
107     * @param latlon lat/lon
108     */
109    public void visit(LatLon latlon) {
110        visit((ILatLon) latlon);
111    }
112
113    /**
114     * Visiting call for east/north.
115     * @param eastNorth east/north
116     */
117    public void visit(EastNorth eastNorth) {
118        if (eastNorth != null) {
119            if (bounds == null) {
120                bounds = new ProjectionBounds(eastNorth);
121            } else {
122                bounds.extend(eastNorth);
123            }
124        }
125    }
126
127    /**
128     * Determines if the visitor has a non null bounds area.
129     * @return {@code true} if the visitor has a non null bounds area
130     * @see ProjectionBounds#hasExtend
131     */
132    public boolean hasExtend() {
133        return bounds != null && bounds.hasExtend();
134    }
135
136    /**
137     * Returns the bounding box.
138     * @return The bounding box or <code>null</code> if no coordinates have passed
139     */
140    public ProjectionBounds getBounds() {
141        return bounds;
142    }
143
144    /**
145     * Enlarges the calculated bounding box by 0.0002 degrees or user value
146     * given in edit.zoom-enlarge-bbox.
147     * If the bounding box has not been set (<code>min</code> or <code>max</code>
148     * equal <code>null</code>) this method does not do anything.
149     */
150    public void enlargeBoundingBox() {
151        final double enlarge = Config.getPref().getDouble("edit.zoom-enlarge-bbox", ENLARGE_DEFAULT);
152        enlargeBoundingBox(enlarge, enlarge);
153    }
154
155    /**
156     * Enlarges the calculated bounding box by the specified number of degrees.
157     * If the bounding box has not been set (<code>min</code> or <code>max</code>
158     * equal <code>null</code>) this method does not do anything.
159     *
160     * @param enlargeDegreeX number of degrees to enlarge on each side along X
161     * @param enlargeDegreeY number of degrees to enlarge on each side along Y
162     */
163    public void enlargeBoundingBox(double enlargeDegreeX, double enlargeDegreeY) {
164        if (bounds == null)
165            return;
166        LatLon minLatlon = ProjectionRegistry.getProjection().eastNorth2latlon(bounds.getMin());
167        LatLon maxLatlon = ProjectionRegistry.getProjection().eastNorth2latlon(bounds.getMax());
168        bounds = new ProjectionBounds(new LatLon(
169                        Math.max(-90, minLatlon.lat() - enlargeDegreeY),
170                        Math.max(-180, minLatlon.lon() - enlargeDegreeX)).getEastNorth(ProjectionRegistry.getProjection()),
171                new LatLon(
172                        Math.min(90, maxLatlon.lat() + enlargeDegreeY),
173                        Math.min(180, maxLatlon.lon() + enlargeDegreeX)).getEastNorth(ProjectionRegistry.getProjection()));
174    }
175
176    /**
177     * Enlarges the bounding box up to 0.0002 degrees, depending on its size and user
178     * settings in edit.zoom-enlarge-bbox. If the bounding box is small, it will be enlarged more in relation
179     * to its beginning size. The larger the bounding box, the smaller the change,
180     * down to 0.0 degrees.
181     *
182     * If the bounding box has not been set (<code>min</code> or <code>max</code>
183     * equal <code>null</code>) this method does not do anything.
184     *
185     * @since 14628
186     */
187    public void enlargeBoundingBoxLogarithmically() {
188        if (bounds == null)
189            return;
190        final LatLon min = ProjectionRegistry.getProjection().eastNorth2latlon(bounds.getMin());
191        final LatLon max = ProjectionRegistry.getProjection().eastNorth2latlon(bounds.getMax());
192        final double deltaLat = max.lat() - min.lat();
193        final double deltaLon = max.lon() - min.lon();
194        final double enlarge = Config.getPref().getDouble("edit.zoom-enlarge-bbox", ENLARGE_DEFAULT);
195
196        final DoubleUnaryOperator enlargement = deltaDegress -> {
197            if (deltaDegress < enlarge) {
198                // delta is very small, use configured minimum value
199                return enlarge;
200            }
201            if (deltaDegress < 0.1) {
202                return enlarge - deltaDegress / 100;
203            }
204            return 0.0;
205        };
206        enlargeBoundingBox(enlargement.applyAsDouble(deltaLon), enlargement.applyAsDouble(deltaLat));
207    }
208
209    @Override
210    public String toString() {
211        return "BoundingXYVisitor["+bounds+']';
212    }
213
214    /**
215     * Compute the bounding box of a collection of primitives.
216     * @param primitives the collection of primitives
217     */
218    public void computeBoundingBox(Collection<? extends IPrimitive> primitives) {
219        if (primitives == null) return;
220        for (IPrimitive p: primitives) {
221            if (p == null) {
222                continue;
223            }
224            p.accept(this);
225        }
226    }
227}