001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.IOException;
007import java.io.InputStream;
008import java.text.MessageFormat;
009import java.util.ArrayList;
010import java.util.Collection;
011
012import org.openstreetmap.josm.data.osm.DataSet;
013import org.openstreetmap.josm.data.osm.DataSetMerger;
014import org.openstreetmap.josm.data.osm.OsmPrimitive;
015import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
016import org.openstreetmap.josm.data.osm.Relation;
017import org.openstreetmap.josm.data.osm.Way;
018import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
019import org.openstreetmap.josm.gui.progress.ProgressMonitor;
020import org.openstreetmap.josm.tools.CheckParameterUtil;
021
022/**
023 * OsmServerBackreferenceReader fetches the primitives from the OSM server which
024 * refer to a specific primitive. For a {@link org.openstreetmap.josm.data.osm.Node Node}, ways and relations are retrieved
025 * which refer to the node. For a {@link Way} or a {@link Relation}, only relations are read.
026 *
027 * OsmServerBackreferenceReader uses the API calls <code>[node|way|relation]/#id/relations</code>
028 * and  <code>node/#id/ways</code> to retrieve the referring primitives. The default behaviour
029 * of these calls is to reply incomplete primitives only.
030 *
031 * If you set {@link #setReadFull(boolean)} to true this reader uses a {@link MultiFetchServerObjectReader}
032 * to complete incomplete primitives.
033 *
034 * @since 1806
035 */
036public class OsmServerBackreferenceReader extends OsmServerReader {
037
038    /** the id of the primitive whose referrers are to be read */
039    private long id;
040    /** the type of the primitive */
041    private OsmPrimitiveType primitiveType;
042    /** true if this reader should complete incomplete primitives */
043    private boolean readFull;
044    /** true if this reader should allow incomplete parent ways */
045    private boolean allowIncompleteParentWays;
046
047    /**
048     * constructor
049     *
050     * @param primitive  the primitive to be read. Must not be null. primitive.id &gt; 0 expected
051     *
052     * @throws IllegalArgumentException if primitive is null
053     * @throws IllegalArgumentException if primitive.id &lt;= 0
054     */
055    public OsmServerBackreferenceReader(OsmPrimitive primitive) {
056        CheckParameterUtil.ensure(primitive, "primitive", "id > 0", prim -> prim.getUniqueId() > 0);
057        this.id = primitive.getId();
058        this.primitiveType = OsmPrimitiveType.from(primitive);
059        this.readFull = false;
060    }
061
062    /**
063     * constructor
064     *
065     * @param id  the id of the primitive. &gt; 0 expected
066     * @param type the type of the primitive. Must not be null.
067     *
068     * @throws IllegalArgumentException if id &lt;= 0
069     * @throws IllegalArgumentException if type is null
070     */
071    public OsmServerBackreferenceReader(long id, OsmPrimitiveType type) {
072        if (id <= 0)
073            throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0 expected. Got ''{1}''.", "id", id));
074        CheckParameterUtil.ensureParameterNotNull(type, "type");
075        this.id = id;
076        this.primitiveType = type;
077        this.readFull = false;
078    }
079
080    /**
081     * Creates a back reference reader for given primitive
082     *
083     * @param primitive the primitive
084     * @param readFull <code>true</code>, if referers should be read fully (i.e. including their immediate children)
085     *
086     */
087    public OsmServerBackreferenceReader(OsmPrimitive primitive, boolean readFull) {
088        this(primitive);
089        this.readFull = readFull;
090    }
091
092    /**
093     * Creates a back reference reader for given primitive id
094     *
095     * @param id the id of the primitive whose referers are to be read
096     * @param type the type of the primitive
097     * @param readFull true, if referers should be read fully (i.e. including their immediate children)
098     *
099     * @throws IllegalArgumentException if id &lt;= 0
100     * @throws IllegalArgumentException if type is null
101     */
102    public OsmServerBackreferenceReader(long id, OsmPrimitiveType type, boolean readFull) {
103        this(id, type);
104        this.readFull = readFull;
105    }
106
107    /**
108     * Replies true if this reader also reads immediate children of referring primitives
109     *
110     * @return true if this reader also reads immediate children of referring primitives
111     */
112    public boolean isReadFull() {
113        return readFull;
114    }
115
116    /**
117     * Set true if this reader should reads immediate children of referring primitives too. False, otherwise.
118     *
119     * @param readFull true if this reader should reads immediate children of referring primitives too. False, otherwise.
120     * @return {@code this}, for easy chaining
121     * @since 15426
122     */
123    public OsmServerBackreferenceReader setReadFull(boolean readFull) {
124        this.readFull = readFull;
125        return this;
126    }
127
128    /**
129     * Determines if this reader allows to return incomplete parent ways of a node.
130     * @return {@code true} if this reader allows to return incomplete parent ways of a node
131     * @since 15426
132     */
133    public boolean isAllowIncompleteParentWays() {
134        return allowIncompleteParentWays;
135    }
136
137    /**
138     * Sets whether this reader allows to return incomplete parent ways of a node.
139     * @param allowIncompleteWays {@code true} if this reader allows to return incomplete parent ways of a node
140     * @return {@code this}, for easy chaining
141     * @since 15426
142     */
143    public OsmServerBackreferenceReader setAllowIncompleteParentWays(boolean allowIncompleteWays) {
144        this.allowIncompleteParentWays = allowIncompleteWays;
145        return this;
146    }
147
148    private DataSet getReferringPrimitives(ProgressMonitor progressMonitor, String type, String message) throws OsmTransferException {
149        progressMonitor.beginTask(null, 2);
150        try {
151            progressMonitor.subTask(tr("Contacting OSM Server..."));
152            StringBuilder sb = new StringBuilder();
153            sb.append(primitiveType.getAPIName()).append('/').append(id).append(type);
154
155            try (InputStream in = getInputStream(sb.toString(), progressMonitor.createSubTaskMonitor(1, true))) {
156                if (in == null)
157                    return null;
158                progressMonitor.subTask(message);
159                return OsmReader.parseDataSet(in, progressMonitor.createSubTaskMonitor(1, true));
160            }
161        } catch (OsmTransferException e) {
162            throw e;
163        } catch (IOException | IllegalDataException e) {
164            if (cancel)
165                return null;
166            throw new OsmTransferException(e);
167        } finally {
168            progressMonitor.finishTask();
169            activeConnection = null;
170        }
171    }
172
173    /**
174     * Reads referring ways from the API server and replies them in a {@link DataSet}
175     *
176     * @param progressMonitor progress monitor
177     * @return the data set
178     * @throws OsmTransferException if any error occurs during dialog with OSM API
179     */
180    protected DataSet getReferringWays(ProgressMonitor progressMonitor) throws OsmTransferException {
181        return getReferringPrimitives(progressMonitor, "/ways", tr("Downloading referring ways ..."));
182    }
183
184    /**
185     * Reads referring relations from the API server and replies them in a {@link DataSet}
186     *
187     * @param progressMonitor the progress monitor
188     * @return the data set
189     * @throws OsmTransferException if any error occurs during dialog with OSM API
190     */
191    protected DataSet getReferringRelations(ProgressMonitor progressMonitor) throws OsmTransferException {
192        return getReferringPrimitives(progressMonitor, "/relations", tr("Downloading referring relations ..."));
193    }
194
195    /**
196     * Scans a dataset for incomplete primitives. Depending on the configuration of this reader
197     * incomplete primitives are read from the server with an individual <code>/api/0.6/[way,relation]/#id/full</code>
198     * request.
199     *
200     * <ul>
201     *   <li>if this reader reads referers for a {@link org.openstreetmap.josm.data.osm.Node}, referring ways are always
202     *     read fully, unless {@link #setAllowIncompleteParentWays(boolean)} is set to true.</li>
203     *   <li>if this reader reads referers for an {@link Way} or a {@link Relation}, referring relations
204     *    are only read fully if {@link #setReadFull(boolean)} is set to true.</li>
205     * </ul>
206     *
207     * The method replies the modified dataset.
208     *
209     * @param ds the original dataset
210     * @param progressMonitor  the progress monitor
211     * @return the modified dataset
212     * @throws OsmTransferException if an exception occurs.
213     */
214    protected DataSet readIncompletePrimitives(DataSet ds, ProgressMonitor progressMonitor) throws OsmTransferException {
215        progressMonitor.beginTask(null, 2);
216        try {
217            Collection<Way> waysToCheck = new ArrayList<>(ds.getWays());
218            if (isReadFull() || (primitiveType == OsmPrimitiveType.NODE && !isAllowIncompleteParentWays())) {
219                for (Way way: waysToCheck) {
220                    if (!way.isNew() && way.hasIncompleteNodes()) {
221                        OsmServerObjectReader reader = new OsmServerObjectReader(way.getId(), OsmPrimitiveType.from(way), true /* read full */);
222                        DataSet wayDs = reader.parseOsm(progressMonitor.createSubTaskMonitor(1, false));
223                        DataSetMerger visitor = new DataSetMerger(ds, wayDs);
224                        visitor.merge();
225                    }
226                }
227            }
228            if (isReadFull()) {
229                Collection<Relation> relationsToCheck = new ArrayList<>(ds.getRelations());
230                for (Relation relation: relationsToCheck) {
231                    if (!relation.isNew() && relation.hasIncompleteMembers()) {
232                        OsmServerObjectReader reader = new OsmServerObjectReader(relation.getId(), OsmPrimitiveType.from(relation), true);
233                        DataSet wayDs = reader.parseOsm(progressMonitor.createSubTaskMonitor(1, false));
234                        DataSetMerger visitor = new DataSetMerger(ds, wayDs);
235                        visitor.merge();
236                    }
237                }
238            }
239            return ds;
240        } finally {
241            progressMonitor.finishTask();
242        }
243    }
244
245    /**
246     * Reads the referring primitives from the OSM server, parses them and
247     * replies them as {@link DataSet}
248     *
249     * @param progressMonitor the progress monitor. Set to {@link NullProgressMonitor#INSTANCE} if null.
250     * @return the dataset with the referring primitives
251     * @throws OsmTransferException if an error occurs while communicating with the server
252     */
253    @Override
254    public DataSet parseOsm(ProgressMonitor progressMonitor) throws OsmTransferException {
255        if (progressMonitor == null) {
256            progressMonitor = NullProgressMonitor.INSTANCE;
257        }
258        try {
259            progressMonitor.beginTask(null, 3);
260            DataSet ret = new DataSet();
261            if (primitiveType == OsmPrimitiveType.NODE) {
262                DataSet ds = getReferringWays(progressMonitor.createSubTaskMonitor(1, false));
263                DataSetMerger visitor = new DataSetMerger(ret, ds);
264                visitor.merge();
265                ret = visitor.getTargetDataSet();
266            }
267            DataSet ds = getReferringRelations(progressMonitor.createSubTaskMonitor(1, false));
268            DataSetMerger visitor = new DataSetMerger(ret, ds);
269            visitor.merge();
270            ret = visitor.getTargetDataSet();
271            if (ret != null) {
272                readIncompletePrimitives(ret, progressMonitor.createSubTaskMonitor(1, false));
273                ret.deleteInvisible();
274            }
275            return ret;
276        } finally {
277            progressMonitor.finishTask();
278        }
279    }
280}