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
045    /**
046     * constructor
047     *
048     * @param primitive  the primitive to be read. Must not be null. primitive.id &gt; 0 expected
049     *
050     * @throws IllegalArgumentException if primitive is null
051     * @throws IllegalArgumentException if primitive.id &lt;= 0
052     */
053    public OsmServerBackreferenceReader(OsmPrimitive primitive) {
054        CheckParameterUtil.ensure(primitive, "primitive", "id > 0", prim -> prim.getUniqueId() > 0);
055        this.id = primitive.getId();
056        this.primitiveType = OsmPrimitiveType.from(primitive);
057        this.readFull = false;
058    }
059
060    /**
061     * constructor
062     *
063     * @param id  the id of the primitive. &gt; 0 expected
064     * @param type the type of the primitive. Must not be null.
065     *
066     * @throws IllegalArgumentException if id &lt;= 0
067     * @throws IllegalArgumentException if type is null
068     */
069    public OsmServerBackreferenceReader(long id, OsmPrimitiveType type) {
070        if (id <= 0)
071            throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0 expected. Got ''{1}''.", "id", id));
072        CheckParameterUtil.ensureParameterNotNull(type, "type");
073        this.id = id;
074        this.primitiveType = type;
075        this.readFull = false;
076    }
077
078    /**
079     * Creates a back reference reader for given primitive
080     *
081     * @param primitive the primitive
082     * @param readFull <code>true</code>, if referers should be read fully (i.e. including their immediate children)
083     *
084     */
085    public OsmServerBackreferenceReader(OsmPrimitive primitive, boolean readFull) {
086        this(primitive);
087        this.readFull = readFull;
088    }
089
090    /**
091     * Creates a back reference reader for given primitive id
092     *
093     * @param id the id of the primitive whose referers are to be read
094     * @param type the type of the primitive
095     * @param readFull true, if referers should be read fully (i.e. including their immediate children)
096     *
097     * @throws IllegalArgumentException if id &lt;= 0
098     * @throws IllegalArgumentException if type is null
099     */
100    public OsmServerBackreferenceReader(long id, OsmPrimitiveType type, boolean readFull) {
101        this(id, type);
102        this.readFull = readFull;
103    }
104
105    /**
106     * Replies true if this reader also reads immediate children of referring primitives
107     *
108     * @return true if this reader also reads immediate children of referring primitives
109     */
110    public boolean isReadFull() {
111        return readFull;
112    }
113
114    /**
115     * Set true if this reader should reads immediate children of referring primitives too. False, otherweise.
116     *
117     * @param readFull true if this reader should reads immediate children of referring primitives too. False, otherweise.
118     */
119    public void setReadFull(boolean readFull) {
120        this.readFull = readFull;
121    }
122
123    private DataSet getReferringPrimitives(ProgressMonitor progressMonitor, String type, String message) throws OsmTransferException {
124        progressMonitor.beginTask(null, 2);
125        try {
126            progressMonitor.subTask(tr("Contacting OSM Server..."));
127            StringBuilder sb = new StringBuilder();
128            sb.append(primitiveType.getAPIName()).append('/').append(id).append(type);
129
130            try (InputStream in = getInputStream(sb.toString(), progressMonitor.createSubTaskMonitor(1, true))) {
131                if (in == null)
132                    return null;
133                progressMonitor.subTask(message);
134                return OsmReader.parseDataSet(in, progressMonitor.createSubTaskMonitor(1, true));
135            }
136        } catch (OsmTransferException e) {
137            throw e;
138        } catch (IOException | IllegalDataException e) {
139            if (cancel)
140                return null;
141            throw new OsmTransferException(e);
142        } finally {
143            progressMonitor.finishTask();
144            activeConnection = null;
145        }
146    }
147
148    /**
149     * Reads referring ways from the API server and replies them in a {@link DataSet}
150     *
151     * @param progressMonitor progress monitor
152     * @return the data set
153     * @throws OsmTransferException if any error occurs during dialog with OSM API
154     */
155    protected DataSet getReferringWays(ProgressMonitor progressMonitor) throws OsmTransferException {
156        return getReferringPrimitives(progressMonitor, "/ways", tr("Downloading referring ways ..."));
157    }
158
159    /**
160     * Reads referring relations from the API server and replies them in a {@link DataSet}
161     *
162     * @param progressMonitor the progress monitor
163     * @return the data set
164     * @throws OsmTransferException if any error occurs during dialog with OSM API
165     */
166    protected DataSet getReferringRelations(ProgressMonitor progressMonitor) throws OsmTransferException {
167        return getReferringPrimitives(progressMonitor, "/relations", tr("Downloading referring relations ..."));
168    }
169
170    /**
171     * Scans a dataset for incomplete primitives. Depending on the configuration of this reader
172     * incomplete primitives are read from the server with an individual <code>/api/0.6/[way,relation]/#id/full</code>
173     * request.
174     *
175     * <ul>
176     *   <li>if this reader reads referers for a {@link org.openstreetmap.josm.data.osm.Node}, referring ways are always
177     *     read individually from the server</li>
178     *   <li>if this reader reads referers for an {@link Way} or a {@link Relation}, referring relations
179     *    are only read fully if {@link #setReadFull(boolean)} is set to true.</li>
180     * </ul>
181     *
182     * The method replies the modified dataset.
183     *
184     * @param ds the original dataset
185     * @param progressMonitor  the progress monitor
186     * @return the modified dataset
187     * @throws OsmTransferException if an exception occurs.
188     */
189    protected DataSet readIncompletePrimitives(DataSet ds, ProgressMonitor progressMonitor) throws OsmTransferException {
190        progressMonitor.beginTask(null, 2);
191        try {
192            Collection<Way> waysToCheck = new ArrayList<>(ds.getWays());
193            if (isReadFull() || primitiveType.equals(OsmPrimitiveType.NODE)) {
194                for (Way way: waysToCheck) {
195                    if (!way.isNew() && way.hasIncompleteNodes()) {
196                        OsmServerObjectReader reader = new OsmServerObjectReader(way.getId(), OsmPrimitiveType.from(way), true /* read full */);
197                        DataSet wayDs = reader.parseOsm(progressMonitor.createSubTaskMonitor(1, false));
198                        DataSetMerger visitor = new DataSetMerger(ds, wayDs);
199                        visitor.merge();
200                    }
201                }
202            }
203            if (isReadFull()) {
204                Collection<Relation> relationsToCheck = new ArrayList<>(ds.getRelations());
205                for (Relation relation: relationsToCheck) {
206                    if (!relation.isNew() && relation.hasIncompleteMembers()) {
207                        OsmServerObjectReader reader = new OsmServerObjectReader(relation.getId(), OsmPrimitiveType.from(relation), true);
208                        DataSet wayDs = reader.parseOsm(progressMonitor.createSubTaskMonitor(1, false));
209                        DataSetMerger visitor = new DataSetMerger(ds, wayDs);
210                        visitor.merge();
211                    }
212                }
213            }
214            return ds;
215        } finally {
216            progressMonitor.finishTask();
217        }
218    }
219
220    /**
221     * Reads the referring primitives from the OSM server, parses them and
222     * replies them as {@link DataSet}
223     *
224     * @param progressMonitor the progress monitor. Set to {@link NullProgressMonitor#INSTANCE} if null.
225     * @return the dataset with the referring primitives
226     * @throws OsmTransferException if an error occurs while communicating with the server
227     */
228    @Override
229    public DataSet parseOsm(ProgressMonitor progressMonitor) throws OsmTransferException {
230        if (progressMonitor == null) {
231            progressMonitor = NullProgressMonitor.INSTANCE;
232        }
233        try {
234            progressMonitor.beginTask(null, 3);
235            DataSet ret = new DataSet();
236            if (primitiveType.equals(OsmPrimitiveType.NODE)) {
237                DataSet ds = getReferringWays(progressMonitor.createSubTaskMonitor(1, false));
238                DataSetMerger visitor = new DataSetMerger(ret, ds);
239                visitor.merge();
240                ret = visitor.getTargetDataSet();
241            }
242            DataSet ds = getReferringRelations(progressMonitor.createSubTaskMonitor(1, false));
243            DataSetMerger visitor = new DataSetMerger(ret, ds);
244            visitor.merge();
245            ret = visitor.getTargetDataSet();
246            if (ret != null) {
247                readIncompletePrimitives(ret, progressMonitor.createSubTaskMonitor(1, false));
248                ret.deleteInvisible();
249            }
250            return ret;
251        } finally {
252            progressMonitor.finishTask();
253        }
254    }
255}