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.io.InputStreamReader;
009import java.text.MessageFormat;
010import java.util.ArrayList;
011import java.util.Collection;
012import java.util.HashMap;
013import java.util.List;
014import java.util.Map;
015import java.util.Map.Entry;
016import java.util.OptionalLong;
017import java.util.function.Consumer;
018
019import org.openstreetmap.josm.data.Bounds;
020import org.openstreetmap.josm.data.DataSource;
021import org.openstreetmap.josm.data.coor.LatLon;
022import org.openstreetmap.josm.data.osm.AbstractPrimitive;
023import org.openstreetmap.josm.data.osm.Changeset;
024import org.openstreetmap.josm.data.osm.DataSet;
025import org.openstreetmap.josm.data.osm.DownloadPolicy;
026import org.openstreetmap.josm.data.osm.Node;
027import org.openstreetmap.josm.data.osm.NodeData;
028import org.openstreetmap.josm.data.osm.OsmPrimitive;
029import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
030import org.openstreetmap.josm.data.osm.PrimitiveData;
031import org.openstreetmap.josm.data.osm.PrimitiveId;
032import org.openstreetmap.josm.data.osm.Relation;
033import org.openstreetmap.josm.data.osm.RelationData;
034import org.openstreetmap.josm.data.osm.RelationMember;
035import org.openstreetmap.josm.data.osm.RelationMemberData;
036import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
037import org.openstreetmap.josm.data.osm.Tagged;
038import org.openstreetmap.josm.data.osm.UploadPolicy;
039import org.openstreetmap.josm.data.osm.User;
040import org.openstreetmap.josm.data.osm.Way;
041import org.openstreetmap.josm.data.osm.WayData;
042import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
043import org.openstreetmap.josm.gui.progress.ProgressMonitor;
044import org.openstreetmap.josm.tools.CheckParameterUtil;
045import org.openstreetmap.josm.tools.Logging;
046import org.openstreetmap.josm.tools.Utils;
047import org.openstreetmap.josm.tools.date.DateUtils;
048
049/**
050 * Abstract Reader, allowing other implementations than OsmReader (PbfReader in PBF plugin for example)
051 * @author Vincent
052 * @since 4490
053 */
054public abstract class AbstractReader {
055
056    /** Used by plugins to register themselves as data postprocessors. */
057    private static volatile List<OsmServerReadPostprocessor> postprocessors;
058
059    protected boolean cancel;
060
061    /**
062     * Register a new postprocessor.
063     * @param pp postprocessor
064     * @see #deregisterPostprocessor
065     * @since 14119 (moved from OsmReader)
066     */
067    public static void registerPostprocessor(OsmServerReadPostprocessor pp) {
068        if (postprocessors == null) {
069            postprocessors = new ArrayList<>();
070        }
071        postprocessors.add(pp);
072    }
073
074    /**
075     * Deregister a postprocessor previously registered with {@link #registerPostprocessor}.
076     * @param pp postprocessor
077     * @see #registerPostprocessor
078     * @since 14119 (moved from OsmReader)
079     */
080    public static void deregisterPostprocessor(OsmServerReadPostprocessor pp) {
081        if (postprocessors != null) {
082            postprocessors.remove(pp);
083        }
084    }
085
086    /**
087     * The dataset to add parsed objects to.
088     */
089    protected DataSet ds = new DataSet();
090
091    protected Changeset uploadChangeset;
092
093    /** the map from external ids to read OsmPrimitives. External ids are
094     * longs too, but in contrast to internal ids negative values are used
095     * to identify primitives unknown to the OSM server
096     */
097    protected final Map<PrimitiveId, OsmPrimitive> externalIdMap = new HashMap<>();
098
099    /**
100     * Data structure for the remaining way objects
101     */
102    protected final Map<Long, Collection<Long>> ways = new HashMap<>();
103
104    /**
105     * Data structure for relation objects
106     */
107    protected final Map<Long, Collection<RelationMemberData>> relations = new HashMap<>();
108
109    /**
110     * Replies the parsed data set
111     *
112     * @return the parsed data set
113     */
114    public DataSet getDataSet() {
115        return ds;
116    }
117
118    /**
119     * Iterate over registered postprocessors and give them each a chance to modify the dataset we have just loaded.
120     * @param progressMonitor Progress monitor
121     */
122    protected void callPostProcessors(ProgressMonitor progressMonitor) {
123        if (postprocessors != null) {
124            for (OsmServerReadPostprocessor pp : postprocessors) {
125                pp.postprocessDataSet(getDataSet(), progressMonitor);
126            }
127        }
128    }
129
130    /**
131     * Processes the parsed nodes after parsing. Just adds them to
132     * the dataset
133     *
134     */
135    protected void processNodesAfterParsing() {
136        for (OsmPrimitive primitive: externalIdMap.values()) {
137            if (primitive instanceof Node) {
138                this.ds.addPrimitive(primitive);
139            }
140        }
141    }
142
143    /**
144     * Processes the ways after parsing. Rebuilds the list of nodes of each way and
145     * adds the way to the dataset
146     *
147     * @throws IllegalDataException if a data integrity problem is detected
148     */
149    protected void processWaysAfterParsing() throws IllegalDataException {
150        for (Entry<Long, Collection<Long>> entry : ways.entrySet()) {
151            Long externalWayId = entry.getKey();
152            Way w = (Way) externalIdMap.get(new SimplePrimitiveId(externalWayId, OsmPrimitiveType.WAY));
153            List<Node> wayNodes = new ArrayList<>();
154            for (long id : entry.getValue()) {
155                Node n = (Node) externalIdMap.get(new SimplePrimitiveId(id, OsmPrimitiveType.NODE));
156                if (n == null) {
157                    if (id <= 0)
158                        throw new IllegalDataException(
159                                tr("Way with external ID ''{0}'' includes missing node with external ID ''{1}''.",
160                                        Long.toString(externalWayId),
161                                        Long.toString(id)));
162                    // create an incomplete node if necessary
163                    n = (Node) ds.getPrimitiveById(id, OsmPrimitiveType.NODE);
164                    if (n == null) {
165                        n = new Node(id);
166                        ds.addPrimitive(n);
167                    }
168                }
169                if (n.isDeleted()) {
170                    Logging.info(tr("Deleted node {0} is part of way {1}", Long.toString(id), Long.toString(w.getId())));
171                } else {
172                    wayNodes.add(n);
173                }
174            }
175            w.setNodes(wayNodes);
176            if (w.hasIncompleteNodes()) {
177                Logging.info(tr("Way {0} with {1} nodes is incomplete because at least one node was missing in the loaded data.",
178                        Long.toString(externalWayId), w.getNodesCount()));
179            }
180            ds.addPrimitive(w);
181        }
182    }
183
184    /**
185     * Completes the parsed relations with its members.
186     *
187     * @throws IllegalDataException if a data integrity problem is detected, i.e. if a
188     * relation member refers to a local primitive which wasn't available in the data
189     */
190    protected void processRelationsAfterParsing() throws IllegalDataException {
191
192        // First add all relations to make sure that when relation reference other relation, the referenced will be already in dataset
193        for (Long externalRelationId : relations.keySet()) {
194            Relation relation = (Relation) externalIdMap.get(
195                    new SimplePrimitiveId(externalRelationId, OsmPrimitiveType.RELATION)
196            );
197            ds.addPrimitive(relation);
198        }
199
200        for (Entry<Long, Collection<RelationMemberData>> entry : relations.entrySet()) {
201            Long externalRelationId = entry.getKey();
202            Relation relation = (Relation) externalIdMap.get(
203                    new SimplePrimitiveId(externalRelationId, OsmPrimitiveType.RELATION)
204            );
205            List<RelationMember> relationMembers = new ArrayList<>();
206            for (RelationMemberData rm : entry.getValue()) {
207                // lookup the member from the map of already created primitives
208                OsmPrimitive primitive = externalIdMap.get(new SimplePrimitiveId(rm.getMemberId(), rm.getMemberType()));
209
210                if (primitive == null) {
211                    if (rm.getMemberId() <= 0)
212                        // relation member refers to a primitive with a negative id which was not
213                        // found in the data. This is always a data integrity problem and we abort
214                        // with an exception
215                        //
216                        throw new IllegalDataException(
217                                tr("Relation with external id ''{0}'' refers to a missing primitive with external id ''{1}''.",
218                                        Long.toString(externalRelationId),
219                                        Long.toString(rm.getMemberId())));
220
221                    // member refers to OSM primitive which was not present in the parsed data
222                    // -> create a new incomplete primitive and add it to the dataset
223                    //
224                    primitive = ds.getPrimitiveById(rm.getMemberId(), rm.getMemberType());
225                    if (primitive == null) {
226                        switch (rm.getMemberType()) {
227                        case NODE:
228                            primitive = new Node(rm.getMemberId()); break;
229                        case WAY:
230                            primitive = new Way(rm.getMemberId()); break;
231                        case RELATION:
232                            primitive = new Relation(rm.getMemberId()); break;
233                        default: throw new AssertionError(); // can't happen
234                        }
235
236                        ds.addPrimitive(primitive);
237                        externalIdMap.put(new SimplePrimitiveId(rm.getMemberId(), rm.getMemberType()), primitive);
238                    }
239                }
240                if (primitive.isDeleted()) {
241                    Logging.info(tr("Deleted member {0} is used by relation {1}",
242                            Long.toString(primitive.getId()), Long.toString(relation.getId())));
243                } else {
244                    relationMembers.add(new RelationMember(rm.getRole(), primitive));
245                }
246            }
247            relation.setMembers(relationMembers);
248        }
249    }
250
251    protected void processChangesetAfterParsing() {
252        if (uploadChangeset != null) {
253            for (Map.Entry<String, String> e : uploadChangeset.getKeys().entrySet()) {
254                ds.addChangeSetTag(e.getKey(), e.getValue());
255            }
256        }
257    }
258
259    protected final void prepareDataSet() throws IllegalDataException {
260        ds.beginUpdate();
261        try {
262            processNodesAfterParsing();
263            processWaysAfterParsing();
264            processRelationsAfterParsing();
265            processChangesetAfterParsing();
266        } finally {
267            ds.endUpdate();
268        }
269    }
270
271    protected abstract DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException;
272
273    @FunctionalInterface
274    protected interface ParserWorker {
275        /**
276         * Effectively parses the file, depending on the format (XML, JSON, etc.)
277         * @param ir input stream reader
278         * @throws IllegalDataException in case of invalid data
279         * @throws IOException in case of I/O error
280         */
281        void accept(InputStreamReader ir) throws IllegalDataException, IOException;
282    }
283
284    protected final DataSet doParseDataSet(InputStream source, ProgressMonitor progressMonitor, ParserWorker parserWorker)
285            throws IllegalDataException {
286        if (progressMonitor == null) {
287            progressMonitor = NullProgressMonitor.INSTANCE;
288        }
289        ProgressMonitor.CancelListener cancelListener = () -> cancel = true;
290        progressMonitor.addCancelListener(cancelListener);
291        CheckParameterUtil.ensureParameterNotNull(source, "source");
292        try {
293            progressMonitor.beginTask(tr("Prepare OSM data..."), 4); // read, prepare, post-process, render
294            progressMonitor.indeterminateSubTask(tr("Parsing OSM data..."));
295
296            try (InputStreamReader ir = UTFInputStreamReader.create(source)) {
297                parserWorker.accept(ir);
298            }
299            progressMonitor.worked(1);
300
301            boolean readOnly = getDataSet().isLocked();
302
303            progressMonitor.indeterminateSubTask(tr("Preparing data set..."));
304            if (readOnly) {
305                getDataSet().unlock();
306            }
307            prepareDataSet();
308            if (readOnly) {
309                getDataSet().lock();
310            }
311            progressMonitor.worked(1);
312            progressMonitor.indeterminateSubTask(tr("Post-processing data set..."));
313            // iterate over registered postprocessors and give them each a chance
314            // to modify the dataset we have just loaded.
315            callPostProcessors(progressMonitor);
316            progressMonitor.worked(1);
317            progressMonitor.indeterminateSubTask(tr("Rendering data set..."));
318            // Make sure postprocessors did not change the read-only state
319            if (readOnly && !getDataSet().isLocked()) {
320                getDataSet().lock();
321            }
322            return getDataSet();
323        } catch (IllegalDataException e) {
324            throw e;
325        } catch (IOException e) {
326            throw new IllegalDataException(e);
327        } finally {
328            OptionalLong minId = externalIdMap.values().stream().mapToLong(AbstractPrimitive::getUniqueId).min();
329            if (minId.isPresent() && minId.getAsLong() < AbstractPrimitive.currentUniqueId()) {
330                AbstractPrimitive.advanceUniqueId(minId.getAsLong());
331            }
332            progressMonitor.finishTask();
333            progressMonitor.removeCancelListener(cancelListener);
334        }
335    }
336
337    protected final long getLong(String name, String value) throws IllegalDataException {
338        if (value == null) {
339            throw new IllegalDataException(tr("Missing required attribute ''{0}''.", name));
340        }
341        try {
342            return Long.parseLong(value);
343        } catch (NumberFormatException e) {
344            throw new IllegalDataException(tr("Illegal long value for attribute ''{0}''. Got ''{1}''.", name, value), e);
345        }
346    }
347
348    protected final void parseVersion(String version) throws IllegalDataException {
349        validateVersion(version);
350        ds.setVersion(version);
351    }
352
353    private static void validateVersion(String version) throws IllegalDataException {
354        if (version == null) {
355            throw new IllegalDataException(tr("Missing mandatory attribute ''{0}''.", "version"));
356        }
357        if (!"0.6".equals(version)) {
358            throw new IllegalDataException(tr("Unsupported version: {0}", version));
359        }
360    }
361
362    protected final void parseDownloadPolicy(String key, String downloadPolicy) throws IllegalDataException {
363        parsePolicy(key, downloadPolicy, policy -> ds.setDownloadPolicy(DownloadPolicy.of(policy)));
364    }
365
366    protected final void parseUploadPolicy(String key, String uploadPolicy) throws IllegalDataException {
367        parsePolicy(key, uploadPolicy, policy -> ds.setUploadPolicy(UploadPolicy.of(policy)));
368    }
369
370    private static void parsePolicy(String key, String policy, Consumer<String> consumer) throws IllegalDataException {
371        if (policy != null) {
372            try {
373                consumer.accept(policy);
374            } catch (IllegalArgumentException e) {
375                throw new IllegalDataException(MessageFormat.format(
376                        "Illegal value for attribute ''{0}''. Got ''{1}''.", key, policy), e);
377            }
378        }
379    }
380
381    protected final void parseLocked(String locked) {
382        if ("true".equalsIgnoreCase(locked)) {
383            ds.lock();
384        }
385    }
386
387    protected final void parseBounds(String generator, String minlon, String minlat, String maxlon, String maxlat, String origin)
388            throws IllegalDataException {
389        if (minlon != null && maxlon != null && minlat != null && maxlat != null) {
390            if (origin == null) {
391                origin = generator;
392            }
393            Bounds bounds = new Bounds(
394                    Double.parseDouble(minlat), Double.parseDouble(minlon),
395                    Double.parseDouble(maxlat), Double.parseDouble(maxlon));
396            if (bounds.isOutOfTheWorld()) {
397                Bounds copy = new Bounds(bounds);
398                bounds.normalize();
399                Logging.info("Bbox " + copy + " is out of the world, normalized to " + bounds);
400            }
401            ds.addDataSource(new DataSource(bounds, origin));
402        } else {
403            throw new IllegalDataException(tr("Missing mandatory attributes on element ''bounds''. " +
404                    "Got minlon=''{0}'',minlat=''{1}'',maxlon=''{2}'',maxlat=''{3}'', origin=''{4}''.",
405                    minlon, minlat, maxlon, maxlat, origin
406            ));
407        }
408    }
409
410    protected final void parseId(PrimitiveData current, long id) throws IllegalDataException {
411        current.setId(id);
412        if (current.getUniqueId() == 0) {
413            throw new IllegalDataException(tr("Illegal object with ID=0."));
414        }
415    }
416
417    protected final void parseTimestamp(PrimitiveData current, String time) {
418        if (time != null && !time.isEmpty()) {
419            current.setRawTimestamp((int) (DateUtils.tsFromString(time)/1000));
420        }
421    }
422
423    private static User createUser(String uid, String name) throws IllegalDataException {
424        if (uid == null) {
425            if (name == null)
426                return null;
427            return User.createLocalUser(name);
428        }
429        try {
430            return User.createOsmUser(Long.parseLong(uid), name);
431        } catch (NumberFormatException e) {
432            throw new IllegalDataException(MessageFormat.format("Illegal value for attribute ''uid''. Got ''{0}''.", uid), e);
433        }
434    }
435
436    protected final void parseUser(PrimitiveData current, String user, long uid) {
437        current.setUser(User.createOsmUser(uid, user));
438    }
439
440    protected final void parseUser(PrimitiveData current, String user, String uid) throws IllegalDataException {
441        current.setUser(createUser(uid, user));
442    }
443
444    protected final void parseVisible(PrimitiveData current, String visible) {
445        if (visible != null) {
446            current.setVisible(Boolean.parseBoolean(visible));
447        }
448    }
449
450    protected final void parseVersion(PrimitiveData current, String versionString) throws IllegalDataException {
451        int version = 0;
452        if (versionString != null) {
453            try {
454                version = Integer.parseInt(versionString);
455            } catch (NumberFormatException e) {
456                throw new IllegalDataException(
457                        tr("Illegal value for attribute ''version'' on OSM primitive with ID {0}. Got {1}.",
458                        Long.toString(current.getUniqueId()), versionString), e);
459            }
460            parseVersion(current, version);
461        } else {
462            // version expected for OSM primitives with an id assigned by the server (id > 0), since API 0.6
463            if (!current.isNew() && ds.getVersion() != null && "0.6".equals(ds.getVersion())) {
464                throw new IllegalDataException(
465                        tr("Missing attribute ''version'' on OSM primitive with ID {0}.", Long.toString(current.getUniqueId())));
466            }
467        }
468    }
469
470    protected final void parseVersion(PrimitiveData current, int version) throws IllegalDataException {
471        switch (ds.getVersion()) {
472        case "0.6":
473            if (version <= 0 && !current.isNew()) {
474                throw new IllegalDataException(
475                        tr("Illegal value for attribute ''version'' on OSM primitive with ID {0}. Got {1}.",
476                        Long.toString(current.getUniqueId()), version));
477            } else if (version < 0 && current.isNew()) {
478                Logging.warn(tr("Normalizing value of attribute ''version'' of element {0} to {2}, API version is ''{3}''. Got {1}.",
479                        current.getUniqueId(), version, 0, "0.6"));
480                version = 0;
481            }
482            break;
483        default:
484            // should not happen. API version has been checked before
485            throw new IllegalDataException(tr("Unknown or unsupported API version. Got {0}.", ds.getVersion()));
486        }
487        current.setVersion(version);
488    }
489
490    protected final void parseAction(PrimitiveData current, String action) {
491        if (action == null) {
492            // do nothing
493        } else if ("delete".equals(action)) {
494            current.setDeleted(true);
495            current.setModified(current.isVisible());
496        } else if ("modify".equals(action)) {
497            current.setModified(true);
498        }
499    }
500
501    private static void handleIllegalChangeset(PrimitiveData current, IllegalArgumentException e, Object v)
502            throws IllegalDataException {
503        Logging.debug(e.getMessage());
504        if (current.isNew()) {
505            // for a new primitive we just log a warning
506            Logging.info(tr("Illegal value for attribute ''changeset'' on new object {1}. Got {0}. Resetting to 0.",
507                    v, current.getUniqueId()));
508            current.setChangesetId(0);
509        } else {
510            // for an existing primitive this is a problem
511            throw new IllegalDataException(tr("Illegal value for attribute ''changeset''. Got {0}.", v), e);
512        }
513    }
514
515    protected final void parseChangeset(PrimitiveData current, String v) throws IllegalDataException {
516        if (v == null) {
517            current.setChangesetId(0);
518        } else {
519            try {
520                parseChangeset(current, Integer.parseInt(v));
521            } catch (NumberFormatException e) {
522                handleIllegalChangeset(current, e, v);
523            }
524        }
525    }
526
527    protected final void parseChangeset(PrimitiveData current, int v) throws IllegalDataException {
528        try {
529            current.setChangesetId(v);
530        } catch (IllegalArgumentException e) {
531            handleIllegalChangeset(current, e, v);
532        } catch (IllegalStateException e) {
533            // thrown for positive changeset id on new primitives
534            Logging.debug(e);
535            Logging.info(e.getMessage());
536            current.setChangesetId(0);
537        }
538        if (current.getChangesetId() <= 0) {
539            if (current.isNew()) {
540                // for a new primitive we just log a warning
541                Logging.info(tr("Illegal value for attribute ''changeset'' on new object {1}. Got {0}. Resetting to 0.",
542                        v, current.getUniqueId()));
543                current.setChangesetId(0);
544            } else if (current.getChangesetId() < 0) {
545                // for an existing primitive this is a problem only for negative ids (GDPR extracts are set to 0)
546                throw new IllegalDataException(tr("Illegal value for attribute ''changeset''. Got {0}.", v));
547            }
548        }
549    }
550
551    protected final void parseTag(Tagged t, String key, String value) throws IllegalDataException {
552        if (key == null || value == null) {
553            throw new IllegalDataException(tr("Missing key or value attribute in tag."));
554        } else if (Utils.isStripEmpty(key) && t instanceof AbstractPrimitive) {
555            // #14199: Empty keys as ignored by AbstractPrimitive#put, but it causes problems to fix existing data
556            // Drop the tag on import, but flag the primitive as modified
557            ((AbstractPrimitive) t).setModified(true);
558        } else {
559            t.put(key.intern(), value.intern());
560        }
561    }
562
563    @FunctionalInterface
564    protected interface CommonReader {
565        /**
566         * Reads the common primitive attributes and sets them in {@code pd}
567         * @param pd primitive data to update
568         * @throws IllegalDataException in case of invalid data
569         */
570        void accept(PrimitiveData pd) throws IllegalDataException;
571    }
572
573    @FunctionalInterface
574    protected interface NodeReader {
575        /**
576         * Reads the node tags.
577         * @param n node
578         * @throws IllegalDataException in case of invalid data
579         */
580        void accept(Node n) throws IllegalDataException;
581    }
582
583    @FunctionalInterface
584    protected interface WayReader {
585        /**
586         * Reads the way nodes and tags.
587         * @param w way
588         * @param nodeIds collection of resulting node ids
589         * @throws IllegalDataException in case of invalid data
590         */
591        void accept(Way w, Collection<Long> nodeIds) throws IllegalDataException;
592    }
593
594    @FunctionalInterface
595    protected interface RelationReader {
596        /**
597         * Reads the relation members and tags.
598         * @param r relation
599         * @param members collection of resulting members
600         * @throws IllegalDataException in case of invalid data
601         */
602        void accept(Relation r, Collection<RelationMemberData> members) throws IllegalDataException;
603    }
604
605    private static boolean areLatLonDefined(String lat, String lon) {
606        return lat != null && lon != null;
607    }
608
609    private static boolean areLatLonDefined(double lat, double lon) {
610        return !Double.isNaN(lat) && !Double.isNaN(lon);
611    }
612
613    protected OsmPrimitive buildPrimitive(PrimitiveData pd) {
614        OsmPrimitive p;
615        if (pd.getUniqueId() < AbstractPrimitive.currentUniqueId()) {
616            p = pd.getType().newInstance(pd.getUniqueId(), true);
617            AbstractPrimitive.advanceUniqueId(pd.getUniqueId());
618        } else {
619            p = pd.getType().newVersionedInstance(pd.getId(), pd.getVersion());
620        }
621        p.setVisible(pd.isVisible());
622        p.load(pd);
623        externalIdMap.put(pd.getPrimitiveId(), p);
624        return p;
625    }
626
627    private Node addNode(NodeData nd, NodeReader nodeReader) throws IllegalDataException {
628        Node n = (Node) buildPrimitive(nd);
629        nodeReader.accept(n);
630        return n;
631    }
632
633    protected final Node parseNode(double lat, double lon, CommonReader commonReader, NodeReader nodeReader)
634            throws IllegalDataException {
635        NodeData nd = new NodeData(0);
636        LatLon ll = null;
637        if (areLatLonDefined(lat, lon)) {
638            try {
639                ll = new LatLon(lat, lon);
640                nd.setCoor(ll);
641            } catch (NumberFormatException e) {
642                Logging.trace(e);
643            }
644        }
645        commonReader.accept(nd);
646        if (areLatLonDefined(lat, lon) && (ll == null || !ll.isValid())) {
647            throw new IllegalDataException(tr("Illegal value for attributes ''lat'', ''lon'' on node with ID {0}. Got ''{1}'', ''{2}''.",
648                    Long.toString(nd.getId()), lat, lon));
649        }
650        return addNode(nd, nodeReader);
651    }
652
653    protected final Node parseNode(String lat, String lon, CommonReader commonReader, NodeReader nodeReader)
654            throws IllegalDataException {
655        NodeData nd = new NodeData();
656        LatLon ll = null;
657        if (areLatLonDefined(lat, lon)) {
658            try {
659                ll = new LatLon(Double.parseDouble(lat), Double.parseDouble(lon));
660                nd.setCoor(ll);
661            } catch (NumberFormatException e) {
662                Logging.trace(e);
663            }
664        }
665        commonReader.accept(nd);
666        if (areLatLonDefined(lat, lon) && (ll == null || !ll.isValid())) {
667            throw new IllegalDataException(tr("Illegal value for attributes ''lat'', ''lon'' on node with ID {0}. Got ''{1}'', ''{2}''.",
668                    Long.toString(nd.getId()), lat, lon));
669        }
670        return addNode(nd, nodeReader);
671    }
672
673    protected final Way parseWay(CommonReader commonReader, WayReader wayReader) throws IllegalDataException {
674        WayData wd = new WayData(0);
675        commonReader.accept(wd);
676        Way w = (Way) buildPrimitive(wd);
677
678        Collection<Long> nodeIds = new ArrayList<>();
679        wayReader.accept(w, nodeIds);
680        if (w.isDeleted() && !nodeIds.isEmpty()) {
681            Logging.info(tr("Deleted way {0} contains nodes", Long.toString(w.getUniqueId())));
682            nodeIds = new ArrayList<>();
683        }
684        ways.put(wd.getUniqueId(), nodeIds);
685        return w;
686    }
687
688    protected final Relation parseRelation(CommonReader commonReader, RelationReader relationReader) throws IllegalDataException {
689        RelationData rd = new RelationData(0);
690        commonReader.accept(rd);
691        Relation r = (Relation) buildPrimitive(rd);
692
693        Collection<RelationMemberData> members = new ArrayList<>();
694        relationReader.accept(r, members);
695        if (r.isDeleted() && !members.isEmpty()) {
696            Logging.info(tr("Deleted relation {0} contains members", Long.toString(r.getUniqueId())));
697            members = new ArrayList<>();
698        }
699        relations.put(rd.getUniqueId(), members);
700        return r;
701    }
702
703    protected final RelationMemberData parseRelationMember(Relation r, String ref, String type, String role) throws IllegalDataException {
704        if (ref == null) {
705            throw new IllegalDataException(tr("Missing attribute ''ref'' on member in relation {0}.",
706                    Long.toString(r.getUniqueId())));
707        }
708        try {
709            return parseRelationMember(r, Long.parseLong(ref), type, role);
710        } catch (NumberFormatException e) {
711            throw new IllegalDataException(tr("Illegal value for attribute ''ref'' on member in relation {0}. Got {1}",
712                    Long.toString(r.getUniqueId()), ref), e);
713        }
714    }
715
716    protected final RelationMemberData parseRelationMember(Relation r, long id, String type, String role) throws IllegalDataException {
717        if (id == 0) {
718            throw new IllegalDataException(tr("Incomplete <member> specification with ref=0"));
719        }
720        if (type == null) {
721            throw new IllegalDataException(tr("Missing attribute ''type'' on member {0} in relation {1}.",
722                    Long.toString(id), Long.toString(r.getUniqueId())));
723        }
724        try {
725            return new RelationMemberData(role, OsmPrimitiveType.fromApiTypeName(type), id);
726        } catch (IllegalArgumentException e) {
727            throw new IllegalDataException(tr("Illegal value for attribute ''type'' on member {0} in relation {1}. Got {2}.",
728                    Long.toString(id), Long.toString(r.getUniqueId()), type), e);
729        }
730    }
731}