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            synchronized (AbstractPrimitive.class) {
330                if (minId.isPresent() && minId.getAsLong() < AbstractPrimitive.currentUniqueId()) {
331                    AbstractPrimitive.advanceUniqueId(minId.getAsLong());
332                }
333            }
334            progressMonitor.finishTask();
335            progressMonitor.removeCancelListener(cancelListener);
336        }
337    }
338
339    protected final long getLong(String name, String value) throws IllegalDataException {
340        if (value == null) {
341            throw new IllegalDataException(tr("Missing required attribute ''{0}''.", name));
342        }
343        try {
344            return Long.parseLong(value);
345        } catch (NumberFormatException e) {
346            throw new IllegalDataException(tr("Illegal long value for attribute ''{0}''. Got ''{1}''.", name, value), e);
347        }
348    }
349
350    protected final void parseVersion(String version) throws IllegalDataException {
351        validateVersion(version);
352        ds.setVersion(version);
353    }
354
355    private static void validateVersion(String version) throws IllegalDataException {
356        if (version == null) {
357            throw new IllegalDataException(tr("Missing mandatory attribute ''{0}''.", "version"));
358        }
359        if (!"0.6".equals(version)) {
360            throw new IllegalDataException(tr("Unsupported version: {0}", version));
361        }
362    }
363
364    protected final void parseDownloadPolicy(String key, String downloadPolicy) throws IllegalDataException {
365        parsePolicy(key, downloadPolicy, policy -> ds.setDownloadPolicy(DownloadPolicy.of(policy)));
366    }
367
368    protected final void parseUploadPolicy(String key, String uploadPolicy) throws IllegalDataException {
369        parsePolicy(key, uploadPolicy, policy -> ds.setUploadPolicy(UploadPolicy.of(policy)));
370    }
371
372    private static void parsePolicy(String key, String policy, Consumer<String> consumer) throws IllegalDataException {
373        if (policy != null) {
374            try {
375                consumer.accept(policy);
376            } catch (IllegalArgumentException e) {
377                throw new IllegalDataException(MessageFormat.format(
378                        "Illegal value for attribute ''{0}''. Got ''{1}''.", key, policy), e);
379            }
380        }
381    }
382
383    protected final void parseLocked(String locked) {
384        if ("true".equalsIgnoreCase(locked)) {
385            ds.lock();
386        }
387    }
388
389    protected final void parseBounds(String generator, String minlon, String minlat, String maxlon, String maxlat, String origin)
390            throws IllegalDataException {
391        if (minlon != null && maxlon != null && minlat != null && maxlat != null) {
392            if (origin == null) {
393                origin = generator;
394            }
395            Bounds bounds = new Bounds(
396                    Double.parseDouble(minlat), Double.parseDouble(minlon),
397                    Double.parseDouble(maxlat), Double.parseDouble(maxlon));
398            if (bounds.isOutOfTheWorld()) {
399                Bounds copy = new Bounds(bounds);
400                bounds.normalize();
401                Logging.info("Bbox " + copy + " is out of the world, normalized to " + bounds);
402            }
403            ds.addDataSource(new DataSource(bounds, origin));
404        } else {
405            throw new IllegalDataException(tr("Missing mandatory attributes on element ''bounds''. " +
406                    "Got minlon=''{0}'',minlat=''{1}'',maxlon=''{2}'',maxlat=''{3}'', origin=''{4}''.",
407                    minlon, minlat, maxlon, maxlat, origin
408            ));
409        }
410    }
411
412    protected final void parseId(PrimitiveData current, long id) throws IllegalDataException {
413        current.setId(id);
414        if (current.getUniqueId() == 0) {
415            throw new IllegalDataException(tr("Illegal object with ID=0."));
416        }
417    }
418
419    protected final void parseTimestamp(PrimitiveData current, String time) {
420        if (time != null && !time.isEmpty()) {
421            current.setRawTimestamp((int) (DateUtils.tsFromString(time)/1000));
422        }
423    }
424
425    private static User createUser(String uid, String name) throws IllegalDataException {
426        if (uid == null) {
427            if (name == null)
428                return null;
429            return User.createLocalUser(name);
430        }
431        try {
432            return User.createOsmUser(Long.parseLong(uid), name);
433        } catch (NumberFormatException e) {
434            throw new IllegalDataException(MessageFormat.format("Illegal value for attribute ''uid''. Got ''{0}''.", uid), e);
435        }
436    }
437
438    protected final void parseUser(PrimitiveData current, String user, long uid) {
439        current.setUser(User.createOsmUser(uid, user));
440    }
441
442    protected final void parseUser(PrimitiveData current, String user, String uid) throws IllegalDataException {
443        current.setUser(createUser(uid, user));
444    }
445
446    protected final void parseVisible(PrimitiveData current, String visible) {
447        if (visible != null) {
448            current.setVisible(Boolean.parseBoolean(visible));
449        }
450    }
451
452    protected final void parseVersion(PrimitiveData current, String versionString) throws IllegalDataException {
453        int version = 0;
454        if (versionString != null) {
455            try {
456                version = Integer.parseInt(versionString);
457            } catch (NumberFormatException e) {
458                throw new IllegalDataException(
459                        tr("Illegal value for attribute ''version'' on OSM primitive with ID {0}. Got {1}.",
460                        Long.toString(current.getUniqueId()), versionString), e);
461            }
462            parseVersion(current, version);
463        } else {
464            // version expected for OSM primitives with an id assigned by the server (id > 0), since API 0.6
465            if (!current.isNew() && ds.getVersion() != null && "0.6".equals(ds.getVersion())) {
466                throw new IllegalDataException(
467                        tr("Missing attribute ''version'' on OSM primitive with ID {0}.", Long.toString(current.getUniqueId())));
468            }
469        }
470    }
471
472    protected final void parseVersion(PrimitiveData current, int version) throws IllegalDataException {
473        switch (ds.getVersion()) {
474        case "0.6":
475            if (version <= 0 && !current.isNew()) {
476                throw new IllegalDataException(
477                        tr("Illegal value for attribute ''version'' on OSM primitive with ID {0}. Got {1}.",
478                        Long.toString(current.getUniqueId()), version));
479            } else if (version < 0 && current.isNew()) {
480                Logging.warn(tr("Normalizing value of attribute ''version'' of element {0} to {2}, API version is ''{3}''. Got {1}.",
481                        current.getUniqueId(), version, 0, "0.6"));
482                version = 0;
483            }
484            break;
485        default:
486            // should not happen. API version has been checked before
487            throw new IllegalDataException(tr("Unknown or unsupported API version. Got {0}.", ds.getVersion()));
488        }
489        current.setVersion(version);
490    }
491
492    protected final void parseAction(PrimitiveData current, String action) {
493        if (action == null) {
494            // do nothing
495        } else if ("delete".equals(action)) {
496            current.setDeleted(true);
497            current.setModified(current.isVisible());
498        } else if ("modify".equals(action)) {
499            current.setModified(true);
500        }
501    }
502
503    private static void handleIllegalChangeset(PrimitiveData current, IllegalArgumentException e, Object v)
504            throws IllegalDataException {
505        Logging.debug(e.getMessage());
506        if (current.isNew()) {
507            // for a new primitive we just log a warning
508            Logging.info(tr("Illegal value for attribute ''changeset'' on new object {1}. Got {0}. Resetting to 0.",
509                    v, current.getUniqueId()));
510            current.setChangesetId(0);
511        } else {
512            // for an existing primitive this is a problem
513            throw new IllegalDataException(tr("Illegal value for attribute ''changeset''. Got {0}.", v), e);
514        }
515    }
516
517    protected final void parseChangeset(PrimitiveData current, String v) throws IllegalDataException {
518        if (v == null) {
519            current.setChangesetId(0);
520        } else {
521            try {
522                parseChangeset(current, Integer.parseInt(v));
523            } catch (NumberFormatException e) {
524                handleIllegalChangeset(current, e, v);
525            }
526        }
527    }
528
529    protected final void parseChangeset(PrimitiveData current, int v) throws IllegalDataException {
530        try {
531            current.setChangesetId(v);
532        } catch (IllegalArgumentException e) {
533            handleIllegalChangeset(current, e, v);
534        } catch (IllegalStateException e) {
535            // thrown for positive changeset id on new primitives
536            Logging.debug(e);
537            Logging.info(e.getMessage());
538            current.setChangesetId(0);
539        }
540        if (current.getChangesetId() <= 0) {
541            if (current.isNew()) {
542                // for a new primitive we just log a warning
543                Logging.info(tr("Illegal value for attribute ''changeset'' on new object {1}. Got {0}. Resetting to 0.",
544                        v, current.getUniqueId()));
545                current.setChangesetId(0);
546            } else if (current.getChangesetId() < 0) {
547                // for an existing primitive this is a problem only for negative ids (GDPR extracts are set to 0)
548                throw new IllegalDataException(tr("Illegal value for attribute ''changeset''. Got {0}.", v));
549            }
550        }
551    }
552
553    protected final void parseTag(Tagged t, String key, String value) throws IllegalDataException {
554        if (key == null || value == null) {
555            throw new IllegalDataException(tr("Missing key or value attribute in tag."));
556        } else if (Utils.isStripEmpty(key) && t instanceof AbstractPrimitive) {
557            // #14199: Empty keys as ignored by AbstractPrimitive#put, but it causes problems to fix existing data
558            // Drop the tag on import, but flag the primitive as modified
559            ((AbstractPrimitive) t).setModified(true);
560        } else {
561            t.put(key.intern(), value.intern());
562        }
563    }
564
565    @FunctionalInterface
566    protected interface CommonReader {
567        /**
568         * Reads the common primitive attributes and sets them in {@code pd}
569         * @param pd primitive data to update
570         * @throws IllegalDataException in case of invalid data
571         */
572        void accept(PrimitiveData pd) throws IllegalDataException;
573    }
574
575    @FunctionalInterface
576    protected interface NodeReader {
577        /**
578         * Reads the node tags.
579         * @param n node
580         * @throws IllegalDataException in case of invalid data
581         */
582        void accept(Node n) throws IllegalDataException;
583    }
584
585    @FunctionalInterface
586    protected interface WayReader {
587        /**
588         * Reads the way nodes and tags.
589         * @param w way
590         * @param nodeIds collection of resulting node ids
591         * @throws IllegalDataException in case of invalid data
592         */
593        void accept(Way w, Collection<Long> nodeIds) throws IllegalDataException;
594    }
595
596    @FunctionalInterface
597    protected interface RelationReader {
598        /**
599         * Reads the relation members and tags.
600         * @param r relation
601         * @param members collection of resulting members
602         * @throws IllegalDataException in case of invalid data
603         */
604        void accept(Relation r, Collection<RelationMemberData> members) throws IllegalDataException;
605    }
606
607    private static boolean areLatLonDefined(String lat, String lon) {
608        return lat != null && lon != null;
609    }
610
611    private static boolean areLatLonDefined(double lat, double lon) {
612        return !Double.isNaN(lat) && !Double.isNaN(lon);
613    }
614
615    protected OsmPrimitive buildPrimitive(PrimitiveData pd) {
616        OsmPrimitive p;
617        if (pd.getUniqueId() < AbstractPrimitive.currentUniqueId()) {
618            p = pd.getType().newInstance(pd.getUniqueId(), true);
619            AbstractPrimitive.advanceUniqueId(pd.getUniqueId());
620        } else {
621            p = pd.getType().newVersionedInstance(pd.getId(), pd.getVersion());
622        }
623        p.setVisible(pd.isVisible());
624        p.load(pd);
625        externalIdMap.put(pd.getPrimitiveId(), p);
626        return p;
627    }
628
629    private Node addNode(NodeData nd, NodeReader nodeReader) throws IllegalDataException {
630        Node n = (Node) buildPrimitive(nd);
631        nodeReader.accept(n);
632        return n;
633    }
634
635    protected final Node parseNode(double lat, double lon, CommonReader commonReader, NodeReader nodeReader)
636            throws IllegalDataException {
637        NodeData nd = new NodeData(0);
638        LatLon ll = null;
639        if (areLatLonDefined(lat, lon)) {
640            try {
641                ll = new LatLon(lat, lon);
642                nd.setCoor(ll);
643            } catch (NumberFormatException e) {
644                Logging.trace(e);
645            }
646        }
647        commonReader.accept(nd);
648        if (areLatLonDefined(lat, lon) && (ll == null || !ll.isValid())) {
649            throw new IllegalDataException(tr("Illegal value for attributes ''lat'', ''lon'' on node with ID {0}. Got ''{1}'', ''{2}''.",
650                    Long.toString(nd.getId()), lat, lon));
651        }
652        return addNode(nd, nodeReader);
653    }
654
655    protected final Node parseNode(String lat, String lon, CommonReader commonReader, NodeReader nodeReader)
656            throws IllegalDataException {
657        NodeData nd = new NodeData();
658        LatLon ll = null;
659        if (areLatLonDefined(lat, lon)) {
660            try {
661                ll = new LatLon(Double.parseDouble(lat), Double.parseDouble(lon));
662                nd.setCoor(ll);
663            } catch (NumberFormatException e) {
664                Logging.trace(e);
665            }
666        }
667        commonReader.accept(nd);
668        if (areLatLonDefined(lat, lon) && (ll == null || !ll.isValid())) {
669            throw new IllegalDataException(tr("Illegal value for attributes ''lat'', ''lon'' on node with ID {0}. Got ''{1}'', ''{2}''.",
670                    Long.toString(nd.getId()), lat, lon));
671        }
672        return addNode(nd, nodeReader);
673    }
674
675    protected final Way parseWay(CommonReader commonReader, WayReader wayReader) throws IllegalDataException {
676        WayData wd = new WayData(0);
677        commonReader.accept(wd);
678        Way w = (Way) buildPrimitive(wd);
679
680        Collection<Long> nodeIds = new ArrayList<>();
681        wayReader.accept(w, nodeIds);
682        if (w.isDeleted() && !nodeIds.isEmpty()) {
683            Logging.info(tr("Deleted way {0} contains nodes", Long.toString(w.getUniqueId())));
684            nodeIds = new ArrayList<>();
685        }
686        ways.put(wd.getUniqueId(), nodeIds);
687        return w;
688    }
689
690    protected final Relation parseRelation(CommonReader commonReader, RelationReader relationReader) throws IllegalDataException {
691        RelationData rd = new RelationData(0);
692        commonReader.accept(rd);
693        Relation r = (Relation) buildPrimitive(rd);
694
695        Collection<RelationMemberData> members = new ArrayList<>();
696        relationReader.accept(r, members);
697        if (r.isDeleted() && !members.isEmpty()) {
698            Logging.info(tr("Deleted relation {0} contains members", Long.toString(r.getUniqueId())));
699            members = new ArrayList<>();
700        }
701        relations.put(rd.getUniqueId(), members);
702        return r;
703    }
704
705    protected final RelationMemberData parseRelationMember(Relation r, String ref, String type, String role) throws IllegalDataException {
706        if (ref == null) {
707            throw new IllegalDataException(tr("Missing attribute ''ref'' on member in relation {0}.",
708                    Long.toString(r.getUniqueId())));
709        }
710        try {
711            return parseRelationMember(r, Long.parseLong(ref), type, role);
712        } catch (NumberFormatException e) {
713            throw new IllegalDataException(tr("Illegal value for attribute ''ref'' on member in relation {0}. Got {1}",
714                    Long.toString(r.getUniqueId()), ref), e);
715        }
716    }
717
718    protected final RelationMemberData parseRelationMember(Relation r, long id, String type, String role) throws IllegalDataException {
719        if (id == 0) {
720            throw new IllegalDataException(tr("Incomplete <member> specification with ref=0"));
721        }
722        if (type == null) {
723            throw new IllegalDataException(tr("Missing attribute ''type'' on member {0} in relation {1}.",
724                    Long.toString(id), Long.toString(r.getUniqueId())));
725        }
726        try {
727            return new RelationMemberData(role, OsmPrimitiveType.fromApiTypeName(type), id);
728        } catch (IllegalArgumentException e) {
729            throw new IllegalDataException(tr("Illegal value for attribute ''type'' on member {0} in relation {1}. Got {2}.",
730                    Long.toString(id), Long.toString(r.getUniqueId()), type), e);
731        }
732    }
733}