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