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     * @return The bounding box or <code>null</code> if no coordinates have passed
138     */
139    public ProjectionBounds getBounds() {
140        return bounds;
141    }
142
143    /**
144     * Enlarges the calculated bounding box by 0.0002 degrees or user value
145     * given in edit.zoom-enlarge-bbox.
146     * If the bounding box has not been set (<code>min</code> or <code>max</code>
147     * equal <code>null</code>) this method does not do anything.
148     */
149    public void enlargeBoundingBox() {
150        final double enlarge = Config.getPref().getDouble("edit.zoom-enlarge-bbox", ENLARGE_DEFAULT);
151        enlargeBoundingBox(enlarge, enlarge);
152    }
153
154    /**
155     * Enlarges the calculated bounding box by the specified number of degrees.
156     * If the bounding box has not been set (<code>min</code> or <code>max</code>
157     * equal <code>null</code>) this method does not do anything.
158     *
159     * @param enlargeDegreeX number of degrees to enlarge on each side along X
160     * @param enlargeDegreeY number of degrees to enlarge on each side along Y
161     */
162    public void enlargeBoundingBox(double enlargeDegreeX, double enlargeDegreeY) {
163        if (bounds == null)
164            return;
165        LatLon minLatlon = ProjectionRegistry.getProjection().eastNorth2latlon(bounds.getMin());
166        LatLon maxLatlon = ProjectionRegistry.getProjection().eastNorth2latlon(bounds.getMax());
167        bounds = new ProjectionBounds(new LatLon(
168                        Math.max(-90, minLatlon.lat() - enlargeDegreeY),
169                        Math.max(-180, minLatlon.lon() - enlargeDegreeX)).getEastNorth(ProjectionRegistry.getProjection()),
170                new LatLon(
171                        Math.min(90, maxLatlon.lat() + enlargeDegreeY),
172                        Math.min(180, maxLatlon.lon() + enlargeDegreeX)).getEastNorth(ProjectionRegistry.getProjection()));
173    }
174
175    /**
176     * Enlarges the bounding box up to 0.0002 degrees, depending on its size and user
177     * settings in edit.zoom-enlarge-bbox. If the bounding box is small, it will be enlarged more in relation
178     * to its beginning size. The larger the bounding box, the smaller the change,
179     * down to 0.0 degrees.
180     *
181     * If the bounding box has not been set (<code>min</code> or <code>max</code>
182     * equal <code>null</code>) this method does not do anything.
183     *
184     * @since 14628
185     */
186    public void enlargeBoundingBoxLogarithmically() {
187        if (bounds == null)
188            return;
189        final LatLon min = ProjectionRegistry.getProjection().eastNorth2latlon(bounds.getMin());
190        final LatLon max = ProjectionRegistry.getProjection().eastNorth2latlon(bounds.getMax());
191        final double deltaLat = max.lat() - min.lat();
192        final double deltaLon = max.lon() - min.lon();
193        final double enlarge = Config.getPref().getDouble("edit.zoom-enlarge-bbox", ENLARGE_DEFAULT);
194
195        final DoubleUnaryOperator enlargement = deltaDegress -> {
196            if (deltaDegress < enlarge) {
197                // delta is very small, use configured minimum value
198                return enlarge;
199            }
200            if (deltaDegress < 0.1) {
201                return enlarge - deltaDegress / 100;
202            }
203            return 0.0;
204        };
205        enlargeBoundingBox(enlargement.applyAsDouble(deltaLon), enlargement.applyAsDouble(deltaLat));
206    }
207
208    @Override
209    public String toString() {
210        return "BoundingXYVisitor["+bounds+']';
211    }
212
213    /**
214     * Compute the bounding box of a collection of primitives.
215     * @param primitives the collection of primitives
216     */
217    public void computeBoundingBox(Collection<? extends IPrimitive> primitives) {
218        if (primitives == null) return;
219        for (IPrimitive p: primitives) {
220            if (p == null) {
221                continue;
222            }
223            p.accept(this);
224        }
225    }
226}