source: josm/trunk/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java@ 19078

Last change on this file since 19078 was 19078, checked in by taylor.smock, 4 months ago

Fix #4142: Track fully downloaded objects (patch by stoecker, GerdP, and myself)

The serialization move from PrimitiveData to AbstractPrimitive should be
reverted prior to 24.05 (see #23677).

The serialization move was required since we want to ensure that all downstream
users of AbstractPrimitive were not using the flags field, which was done by
making the field private instead of protected. They may still be using that
field (via updateFlags) which would not be caught by compile-time or runtime
errors.

Additionally, a good chunk of common functionality was moved up from
OsmPrimitive, even though much of it wasn't useful for PrimitiveData.

  • Property svn:eol-style set to native
File size: 33.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.osm;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.IOException;
7import java.io.ObjectInputStream;
8import java.io.ObjectOutputStream;
9import java.text.MessageFormat;
10import java.time.Instant;
11import java.util.ArrayList;
12import java.util.Arrays;
13import java.util.Collection;
14import java.util.Collections;
15import java.util.Date;
16import java.util.HashMap;
17import java.util.HashSet;
18import java.util.LinkedList;
19import java.util.List;
20import java.util.Map;
21import java.util.Map.Entry;
22import java.util.Objects;
23import java.util.Set;
24import java.util.concurrent.TimeUnit;
25import java.util.function.BiPredicate;
26import java.util.stream.IntStream;
27import java.util.stream.Stream;
28
29import org.openstreetmap.josm.data.gpx.GpxConstants;
30import org.openstreetmap.josm.spi.preferences.Config;
31import org.openstreetmap.josm.tools.Utils;
32
33/**
34 * Abstract class to represent common features of the datatypes primitives.
35 *
36 * @since 4099
37 */
38public abstract class AbstractPrimitive implements IPrimitive, IFilterablePrimitive {
39
40 /**
41 * This flag shows, that the properties have been changed by the user
42 * and on upload the object will be send to the server.
43 */
44 protected static final short FLAG_MODIFIED = 1 << 0;
45
46 /**
47 * This flag is false, if the object is marked
48 * as deleted on the server.
49 */
50 protected static final short FLAG_VISIBLE = 1 << 1;
51
52 /**
53 * An object that was deleted by the user.
54 * Deleted objects are usually hidden on the map and a request
55 * for deletion will be send to the server on upload.
56 * An object usually cannot be deleted if it has non-deleted
57 * objects still referring to it.
58 */
59 protected static final short FLAG_DELETED = 1 << 2;
60
61 /**
62 * A primitive is incomplete if we know its id and type, but nothing more.
63 * Typically some members of a relation are incomplete until they are
64 * fetched from the server.
65 */
66 protected static final short FLAG_INCOMPLETE = 1 << 3;
67
68 /**
69 * An object can be disabled by the filter mechanism.
70 * Then it will show in a shade of gray on the map or it is completely
71 * hidden from the view.
72 * Disabled objects usually cannot be selected or modified
73 * while the filter is active.
74 */
75 protected static final short FLAG_DISABLED = 1 << 4;
76
77 /**
78 * This flag is only relevant if an object is disabled by the
79 * filter mechanism (i.e.&nbsp;FLAG_DISABLED is set).
80 * Then it indicates, whether it is completely hidden or
81 * just shown in gray color.
82 * <p>
83 * When the primitive is not disabled, this flag should be
84 * unset as well (for efficient access).
85 */
86 protected static final short FLAG_HIDE_IF_DISABLED = 1 << 5;
87
88 /**
89 * Flag used internally by the filter mechanism.
90 */
91 protected static final short FLAG_DISABLED_TYPE = 1 << 6;
92
93 /**
94 * Flag used internally by the filter mechanism.
95 */
96 protected static final short FLAG_HIDDEN_TYPE = 1 << 7;
97
98 /**
99 * This flag is set if the primitive is a way and
100 * according to the tags, the direction of the way is important.
101 * (e.g. one way street.)
102 */
103 protected static final short FLAG_HAS_DIRECTIONS = 1 << 8;
104
105 /**
106 * If the primitive is tagged.
107 * Some trivial tags like source=* are ignored here.
108 */
109 protected static final short FLAG_TAGGED = 1 << 9;
110
111 /**
112 * This flag is only relevant if FLAG_HAS_DIRECTIONS is set.
113 * It shows, that direction of the arrows should be reversed.
114 * (E.g. oneway=-1.)
115 */
116 protected static final short FLAG_DIRECTION_REVERSED = 1 << 10;
117
118 /**
119 * When hovering over ways and nodes in add mode, the
120 * "target" objects are visually highlighted. This flag indicates
121 * that the primitive is currently highlighted.
122 */
123 protected static final short FLAG_HIGHLIGHTED = 1 << 11;
124
125 /**
126 * If the primitive is annotated with a tag such as note, fixme, etc.
127 * Match the "work in progress" tags in default map style.
128 */
129 protected static final short FLAG_ANNOTATED = 1 << 12;
130
131 /**
132 * Determines if the primitive is preserved from the filter mechanism.
133 */
134 protected static final short FLAG_PRESERVED = 1 << 13;
135
136 /**
137 * Determines if the primitive has all of its referrers
138 */
139 protected static final short FLAG_ALL_REFERRERS_DOWNLOADED = 1 << 14;
140
141 /**
142 * Put several boolean flags to one short int field to save memory.
143 * Other bits of this field are used in subclasses.
144 */
145 private volatile short flags = FLAG_VISIBLE; // visible per default
146
147 /**
148 * The mappaint cache index for this primitive.
149 * This field belongs to {@code OsmPrimitive}, but due to Java's memory layout alignment, see #20830.
150 */
151 protected short mappaintCacheIdx;
152
153 /*-------------------
154 * OTHER PROPERTIES
155 *-------------------*/
156
157 /**
158 * Unique identifier in OSM. This is used to identify objects on the server.
159 * An id of 0 means an unknown id. The object has not been uploaded yet to
160 * know what id it will get.
161 */
162 protected long id;
163
164 /**
165 * User that last modified this primitive, as specified by the server.
166 * Never changed by JOSM.
167 */
168 protected User user;
169
170 /**
171 * Contains the version number as returned by the API. Needed to
172 * ensure update consistency
173 */
174 protected int version;
175
176 /**
177 * The id of the changeset this primitive was last uploaded to.
178 * 0 if it wasn't uploaded to a changeset yet of if the changeset
179 * id isn't known.
180 */
181 protected int changesetId;
182
183 /**
184 * A time value, measured in seconds from the epoch, or in other words,
185 * a number of seconds that have passed since 1970-01-01T00:00:00Z
186 */
187 protected int timestamp;
188
189 /**
190 * Get and write all attributes from the parameter. Does not fire any listener, so
191 * use this only in the data initializing phase
192 * @param other the primitive to clone data from
193 */
194 public void cloneFrom(AbstractPrimitive other) {
195 setKeys(other.getKeys());
196 id = other.id;
197 if (id <= 0) {
198 // reset version and changeset id
199 version = 0;
200 changesetId = 0;
201 }
202 timestamp = other.timestamp;
203 if (id > 0) {
204 version = other.version;
205 }
206 flags = other.flags;
207 user = other.user;
208 if (id > 0 && other.changesetId > 0) {
209 // #4208: sometimes we cloned from other with id < 0 *and*
210 // an assigned changeset id. Don't know why yet. For primitives
211 // with id < 0 we don't propagate the changeset id any more.
212 //
213 setChangesetId(other.changesetId);
214 }
215 }
216
217 @Override
218 public int getVersion() {
219 return version;
220 }
221
222 @Override
223 public long getId() {
224 return id >= 0 ? id : 0;
225 }
226
227 /**
228 * Gets a unique id representing this object.
229 *
230 * @return Osm id if primitive already exists on the server. Unique negative value if primitive is new
231 */
232 @Override
233 public long getUniqueId() {
234 return id;
235 }
236
237 /**
238 * Determines if this primitive is new.
239 * @return {@code true} if this primitive is new (not yet uploaded the server, id &lt;= 0)
240 */
241 @Override
242 public boolean isNew() {
243 return id <= 0;
244 }
245
246 @Override
247 public boolean isNewOrUndeleted() {
248 return isNew() || ((flags & (FLAG_VISIBLE + FLAG_DELETED)) == 0);
249 }
250
251 @Override
252 public void setOsmId(long id, int version) {
253 if (id <= 0)
254 throw new IllegalArgumentException(tr("ID > 0 expected. Got {0}.", id));
255 if (version <= 0)
256 throw new IllegalArgumentException(tr("Version > 0 expected. Got {0}.", version));
257 this.id = id;
258 this.version = version;
259 this.setIncomplete(false);
260 }
261
262 /**
263 * Clears the metadata, including id and version known to the OSM API.
264 * The id is a new unique id. The version, changeset and timestamp are set to 0.
265 * incomplete and deleted are set to false. It's preferred to use copy constructor with clearMetadata set to true instead
266 * of calling this method.
267 * @since 6140
268 */
269 public void clearOsmMetadata() {
270 // Not part of dataset - no lock necessary
271 this.id = getIdGenerator().generateUniqueId();
272 this.version = 0;
273 this.user = null;
274 this.changesetId = 0; // reset changeset id on a new object
275 this.timestamp = 0;
276 this.setIncomplete(false);
277 this.setDeleted(false);
278 this.setVisible(true);
279 }
280
281 /**
282 * Returns the unique identifier generator.
283 * @return the unique identifier generator
284 * @since 15820
285 */
286 public abstract UniqueIdGenerator getIdGenerator();
287
288 @Override
289 public User getUser() {
290 return user;
291 }
292
293 @Override
294 public void setUser(User user) {
295 this.user = user;
296 }
297
298 @Override
299 public int getChangesetId() {
300 return changesetId;
301 }
302
303 @Override
304 public void setChangesetId(int changesetId) {
305 if (this.changesetId == changesetId)
306 return;
307 if (changesetId < 0)
308 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' >= 0 expected, got {1}", "changesetId", changesetId));
309 if (changesetId > 0 && isNew())
310 throw new IllegalStateException(tr("Cannot assign a changesetId > 0 to a new primitive. Value of changesetId is {0}", changesetId));
311
312 this.changesetId = changesetId;
313 }
314
315 @Deprecated
316 @Override
317 public void setTimestamp(Date timestamp) {
318 this.timestamp = (int) TimeUnit.MILLISECONDS.toSeconds(timestamp.getTime());
319 }
320
321 @Override
322 public void setInstant(Instant timestamp) {
323 this.timestamp = (int) timestamp.getEpochSecond();
324 }
325
326 @Override
327 public void setRawTimestamp(int timestamp) {
328 this.timestamp = timestamp;
329 }
330
331 @Deprecated
332 @Override
333 public Date getTimestamp() {
334 return Date.from(getInstant());
335 }
336
337 @Override
338 public Instant getInstant() {
339 return Instant.ofEpochSecond(Integer.toUnsignedLong(timestamp));
340 }
341
342 @Override
343 public int getRawTimestamp() {
344 return timestamp;
345 }
346
347 @Override
348 public boolean isTimestampEmpty() {
349 return timestamp == 0;
350 }
351
352 /* -------
353 /* FLAGS
354 /* ------*/
355
356 protected void updateFlags(short flag, boolean value) {
357 if (value) {
358 flags |= flag;
359 } else {
360 flags &= (short) ~flag;
361 }
362 }
363
364 /**
365 * Update flags
366 * @param flag The flag to update
367 * @param value The value to set
368 * @return {@code true} if the flags have changed
369 */
370 protected boolean updateFlagsChanged(short flag, boolean value) {
371 int oldFlags = flags;
372 updateFlags(flag, value);
373 return oldFlags != flags;
374 }
375
376 /**
377 * Write common data to a serialization stream. At time of writing, this should <i>only</i> be used by {@link PrimitiveData}.
378 * @param oos The output stream to write to
379 * @throws IOException see {@link ObjectOutputStream#write}
380 */
381 protected void writeObjectCommon(ObjectOutputStream oos) throws IOException {
382 oos.writeLong(id);
383 oos.writeLong(user == null ? -1 : user.getId());
384 oos.writeInt(version);
385 oos.writeInt(changesetId);
386 oos.writeInt(timestamp);
387 oos.writeObject(keys);
388 oos.writeShort(flags);
389 }
390
391 /**
392 * Read common data from a serialization stream. At time of writing, this should <i>only</i> be used by {@link PrimitiveData}.
393 * @param ois The serialization stream to read from
394 * @throws ClassNotFoundException see {@link ObjectInputStream#readObject()}
395 * @throws IOException see {@link ObjectInputStream#read}
396 */
397 protected void readObjectCommon(ObjectInputStream ois) throws ClassNotFoundException, IOException {
398 id = ois.readLong();
399 final long userId = ois.readLong();
400 user = userId == -1 ? null : User.getById(userId);
401 version = ois.readInt();
402 changesetId = ois.readInt();
403 timestamp = ois.readInt();
404 keys = (String[]) ois.readObject();
405 flags = ois.readShort();
406 }
407
408 @Override
409 public void setModified(boolean modified) {
410 updateFlags(FLAG_MODIFIED, modified);
411 }
412
413 @Override
414 public boolean isModified() {
415 return (flags & FLAG_MODIFIED) != 0;
416 }
417
418 @Override
419 public boolean isDeleted() {
420 return (flags & FLAG_DELETED) != 0;
421 }
422
423 @Override
424 public void setReferrersDownloaded(boolean referrersDownloaded) {
425 this.updateFlags(FLAG_ALL_REFERRERS_DOWNLOADED, referrersDownloaded);
426 }
427
428 @Override
429 public boolean isUndeleted() {
430 return (flags & (FLAG_VISIBLE + FLAG_DELETED)) == 0;
431 }
432
433 @Override
434 public boolean isUsable() {
435 return (flags & (FLAG_DELETED + FLAG_INCOMPLETE)) == 0;
436 }
437
438 @Override
439 public boolean isVisible() {
440 return (flags & FLAG_VISIBLE) != 0;
441 }
442
443 @Override
444 public void setVisible(boolean visible) {
445 if (!visible && isNew())
446 throw new IllegalStateException(tr("A primitive with ID = 0 cannot be invisible."));
447 updateFlags(FLAG_VISIBLE, visible);
448 }
449
450 @Override
451 public void setDeleted(boolean deleted) {
452 updateFlags(FLAG_DELETED, deleted);
453 setModified(deleted ^ !isVisible());
454 }
455
456 @Override
457 public boolean hasDirectionKeys() {
458 return (flags & FLAG_HAS_DIRECTIONS) != 0;
459 }
460
461 @Override
462 public boolean reversedDirection() {
463 return (flags & FLAG_DIRECTION_REVERSED) != 0;
464 }
465
466 @Override
467 public boolean isTagged() {
468 return (flags & FLAG_TAGGED) != 0;
469 }
470
471 @Override
472 public boolean isAnnotated() {
473 return (flags & FLAG_ANNOTATED) != 0;
474 }
475
476 @Override
477 public boolean isHighlighted() {
478 return (flags & FLAG_HIGHLIGHTED) != 0;
479 }
480
481 @Override
482 public boolean isDisabled() {
483 return (flags & FLAG_DISABLED) != 0;
484 }
485
486 @Override
487 public boolean isDisabledAndHidden() {
488 return ((flags & FLAG_DISABLED) != 0) && ((flags & FLAG_HIDE_IF_DISABLED) != 0);
489 }
490
491 @Override
492 public boolean isPreserved() {
493 return (flags & FLAG_PRESERVED) != 0;
494 }
495
496 @Override
497 public boolean isReferrersDownloaded() {
498 return isNew() || (flags & FLAG_ALL_REFERRERS_DOWNLOADED) != 0;
499 }
500
501 /**
502 * If set to true, this object is incomplete, which means only the id
503 * and type is known (type is the objects instance class)
504 * @param incomplete incomplete flag value
505 */
506 protected void setIncomplete(boolean incomplete) {
507 updateFlags(FLAG_INCOMPLETE, incomplete);
508 }
509
510 @Override
511 public boolean isIncomplete() {
512 return (flags & FLAG_INCOMPLETE) != 0;
513 }
514
515 @Override
516 public boolean getHiddenType() {
517 return (flags & FLAG_HIDDEN_TYPE) != 0;
518 }
519
520 @Override
521 public boolean getDisabledType() {
522 return (flags & FLAG_DISABLED_TYPE) != 0;
523 }
524
525 @Override
526 public boolean setDisabledState(boolean hidden) {
527 // Store as variables to avoid short circuit boolean return
528 final boolean flagDisabled = updateFlagsChanged(FLAG_DISABLED, true);
529 final boolean flagHideIfDisabled = updateFlagsChanged(FLAG_HIDE_IF_DISABLED, hidden);
530 return flagDisabled || flagHideIfDisabled;
531 }
532
533 @Override
534 public boolean unsetDisabledState() {
535 // Store as variables to avoid short circuit boolean return
536 final boolean flagDisabled = updateFlagsChanged(FLAG_DISABLED, false);
537 final boolean flagHideIfDisabled = updateFlagsChanged(FLAG_HIDE_IF_DISABLED, false);
538 return flagDisabled || flagHideIfDisabled;
539 }
540
541 @Override
542 public void setDisabledType(boolean isExplicit) {
543 updateFlags(FLAG_DISABLED_TYPE, isExplicit);
544 }
545
546 @Override
547 public void setHiddenType(boolean isExplicit) {
548 updateFlags(FLAG_HIDDEN_TYPE, isExplicit);
549 }
550
551 @Override
552 public boolean isDrawable() {
553 return (flags & (FLAG_DELETED + FLAG_INCOMPLETE + FLAG_HIDE_IF_DISABLED)) == 0;
554 }
555
556 protected String getFlagsAsString() {
557 StringBuilder builder = new StringBuilder();
558
559 if (isIncomplete()) {
560 builder.append('I');
561 }
562 if (isModified()) {
563 builder.append('M');
564 }
565 if (isVisible()) {
566 builder.append('V');
567 }
568 if (isDeleted()) {
569 builder.append('D');
570 }
571 return builder.toString();
572 }
573
574 /*------------
575 * Keys handling
576 ------------*/
577
578 /**
579 * The key/value list for this primitive.
580 * <p>
581 * Note that the keys field is synchronized using RCU.
582 * Writes to it are not synchronized by this object, the writers have to synchronize writes themselves.
583 * <p>
584 * In short this means that you should not rely on this variable being the same value when read again and your should always
585 * copy it on writes.
586 * <p>
587 * Further reading:
588 * <ul>
589 * <li>{@link java.util.concurrent.CopyOnWriteArrayList}</li>
590 * <li> <a href="http://stackoverflow.com/questions/2950871/how-can-copyonwritearraylist-be-thread-safe">
591 * http://stackoverflow.com/questions/2950871/how-can-copyonwritearraylist-be-thread-safe</a></li>
592 * <li> <a href="https://en.wikipedia.org/wiki/Read-copy-update">
593 * https://en.wikipedia.org/wiki/Read-copy-update</a> (mind that we have a Garbage collector,
594 * {@code rcu_assign_pointer} and {@code rcu_dereference} are ensured by the {@code volatile} keyword)</li>
595 * </ul>
596 */
597 protected volatile String[] keys;
598
599 /**
600 * Replies the map of key/value pairs. Never replies null. The map can be empty, though.
601 *
602 * @return tags of this primitive. Changes made in returned map are not mapped
603 * back to the primitive, use setKeys() to modify the keys
604 * @see #visitKeys(KeyValueVisitor)
605 */
606 @Override
607 public TagMap getKeys() {
608 return new TagMap(keys);
609 }
610
611 @Override
612 public void visitKeys(KeyValueVisitor visitor) {
613 String[] tKeys = this.keys;
614 if (tKeys != null) {
615 for (int i = 0; i < tKeys.length; i += 2) {
616 visitor.visitKeyValue(this, tKeys[i], tKeys[i + 1]);
617 }
618 }
619 }
620
621 /**
622 * Sets the keys of this primitives to the key/value pairs in <code>keys</code>.
623 * Old key/value pairs are removed.
624 * If <code>keys</code> is null, clears existing key/value pairs.
625 * <p>
626 * Note that this method, like all methods that modify keys, is not synchronized and may lead to data corruption when being used
627 * from multiple threads.
628 *
629 * @param keys the key/value pairs to set. If null, removes all existing key/value pairs.
630 */
631 @Override
632 public void setKeys(Map<String, String> keys) {
633 Map<String, String> originalKeys = getKeys();
634 if (Utils.isEmpty(keys)) {
635 this.keys = null;
636 keysChangedImpl(originalKeys);
637 return;
638 }
639 String[] newKeys = new String[keys.size() * 2];
640 int index = 0;
641 for (Entry<String, String> entry:keys.entrySet()) {
642 newKeys[index++] = Objects.requireNonNull(entry.getKey());
643 newKeys[index++] = Objects.requireNonNull(entry.getValue());
644 }
645 this.keys = newKeys;
646 keysChangedImpl(originalKeys);
647 }
648
649 /**
650 * Copy the keys from a TagMap.
651 * @param keys The new key map.
652 */
653 public void setKeys(TagMap keys) {
654 Map<String, String> originalKeys = getKeys();
655 if (keys == null) {
656 this.keys = null;
657 } else {
658 String[] arr = keys.getTagsArray();
659 if (arr.length == 0) {
660 this.keys = null;
661 } else {
662 this.keys = arr;
663 }
664 }
665 keysChangedImpl(originalKeys);
666 }
667
668 /**
669 * Set the given value to the given key. If key is null, does nothing. If value is null,
670 * removes the key and behaves like {@link #remove(String)}.
671 * <p>
672 * Note that this method, like all methods that modify keys, is not synchronized and may lead to data corruption when being used
673 * from multiple threads.
674 *
675 * @param key The key, for which the value is to be set. Can be null or empty, does nothing in this case.
676 * @param value The value for the key. If null, removes the respective key/value pair.
677 *
678 * @see #remove(String)
679 */
680 @Override
681 public void put(String key, String value) {
682 Map<String, String> originalKeys = getKeys();
683 if (key == null || Utils.isStripEmpty(key))
684 return;
685 else if (value == null) {
686 remove(key);
687 } else if (keys == null) {
688 keys = new String[] {key, value};
689 keysChangedImpl(originalKeys);
690 } else {
691 int keyIndex = indexOfKey(keys, key);
692 int tagArrayLength = keys.length;
693 if (keyIndex < 0) {
694 keyIndex = tagArrayLength;
695 tagArrayLength += 2;
696 }
697
698 // Do not try to optimize this array creation if the key already exists.
699 // We would need to convert the keys array to be an AtomicReferenceArray
700 // Or we would at least need a volatile write after the array was modified to
701 // ensure that changes are visible by other threads.
702 String[] newKeys = Arrays.copyOf(keys, tagArrayLength);
703 newKeys[keyIndex] = key;
704 newKeys[keyIndex + 1] = value;
705 keys = newKeys;
706 keysChangedImpl(originalKeys);
707 }
708 }
709
710 @Override
711 public void putAll(Map<String, String> tags) {
712 if (tags == null || tags.isEmpty()) {
713 return;
714 }
715 // Defensive copy of keys
716 final String[] tKeys = this.keys; // Used to avoid highly unlikely NPE (from a race) during clone operation.
717 String[] newKeys = tKeys == null ? null : tKeys.clone();
718 Map<String, String> originalKeys = getKeys();
719 List<Map.Entry<String, String>> tagsToAdd = new ArrayList<>(tags.size());
720 for (Map.Entry<String, String> tag : tags.entrySet()) {
721 if (!Utils.isBlank(tag.getKey())) {
722 int keyIndex = indexOfKey(newKeys, tag.getKey());
723 // Realistically, we will not hit the newKeys == null branch. If it is null, keyIndex is always < 0
724 if (keyIndex < 0 || newKeys == null) {
725 tagsToAdd.add(tag);
726 } else {
727 newKeys[keyIndex + 1] = tag.getValue();
728 }
729 }
730 }
731 if (!tagsToAdd.isEmpty()) {
732 int index = newKeys != null ? newKeys.length : 0;
733 newKeys = newKeys != null ? Arrays.copyOf(newKeys, newKeys.length + 2 * tagsToAdd.size()) : new String[2 * tagsToAdd.size()];
734 for (Map.Entry<String, String> tag : tagsToAdd) {
735 newKeys[index++] = tag.getKey();
736 newKeys[index++] = tag.getValue();
737 }
738 }
739 keys = newKeys;
740 keysChangedImpl(originalKeys);
741 }
742
743 /**
744 * Scans a key/value array for a given key.
745 * @param keys The key array. It is not modified. It may be null to indicate an empty array.
746 * @param key The key to search for.
747 * @return The position of that key in the keys array - which is always a multiple of 2 - or -1 if it was not found.
748 */
749 private static int indexOfKey(String[] keys, String key) {
750 if (keys == null) {
751 return -1;
752 }
753 for (int i = 0; i < keys.length; i += 2) {
754 if (keys[i].equals(key)) {
755 return i;
756 }
757 }
758 return -1;
759 }
760
761 /**
762 * Remove the given key from the list
763 * <p>
764 * Note that this method, like all methods that modify keys, is not synchronized and may lead to data corruption when being used
765 * from multiple threads.
766 *
767 * @param key the key to be removed. Ignored, if key is null.
768 */
769 @Override
770 public void remove(String key) {
771 if (key == null || keys == null) return;
772 if (!hasKey(key))
773 return;
774 Map<String, String> originalKeys = getKeys();
775 if (keys.length == 2) {
776 keys = null;
777 keysChangedImpl(originalKeys);
778 return;
779 }
780 String[] newKeys = new String[keys.length - 2];
781 int j = 0;
782 for (int i = 0; i < keys.length; i += 2) {
783 if (!keys[i].equals(key)) {
784 newKeys[j++] = keys[i];
785 newKeys[j++] = keys[i+1];
786 }
787 }
788 keys = newKeys;
789 keysChangedImpl(originalKeys);
790 }
791
792 /**
793 * Removes all keys from this primitive.
794 * <p>
795 * Note that this method, like all methods that modify keys, is not synchronized and may lead to data corruption when being used
796 * from multiple threads.
797 */
798 @Override
799 public void removeAll() {
800 if (keys != null) {
801 Map<String, String> originalKeys = getKeys();
802 keys = null;
803 keysChangedImpl(originalKeys);
804 }
805 }
806
807 protected final String doGet(String key, BiPredicate<String, String> predicate) {
808 if (key == null)
809 return null;
810 if (keys == null)
811 return null;
812 for (int i = 0; i < keys.length; i += 2) {
813 if (predicate.test(keys[i], key)) return keys[i+1];
814 }
815 return null;
816 }
817
818 /**
819 * Replies the value for key <code>key</code>. Replies null, if <code>key</code> is null.
820 * Replies null, if there is no value for the given key.
821 *
822 * @param key the key. Can be null, replies null in this case.
823 * @return the value for key <code>key</code>.
824 */
825 @Override
826 public final String get(String key) {
827 return doGet(key, String::equals);
828 }
829
830 /**
831 * Gets a key ignoring the case of the key
832 * @param key The key to get
833 * @return The value for a key that matches the given key ignoring case.
834 */
835 public final String getIgnoreCase(String key) {
836 return doGet(key, String::equalsIgnoreCase);
837 }
838
839 @Override
840 public final int getNumKeys() {
841 return keys == null ? 0 : keys.length / 2;
842 }
843
844 @Override
845 public final Collection<String> keySet() {
846 String[] tKeys = this.keys;
847 if (tKeys == null) {
848 return Collections.emptySet();
849 }
850 if (tKeys.length == 2) {
851 return Collections.singleton(tKeys[0]);
852 }
853
854 final Set<String> result = new HashSet<>(Utils.hashMapInitialCapacity(tKeys.length / 2));
855 for (int i = 0; i < tKeys.length; i += 2) {
856 result.add(tKeys[i]);
857 }
858 return result;
859 }
860
861 @Override
862 public Stream<String> keys() {
863 final String[] k = this.keys;
864 if (k == null) {
865 return Stream.empty();
866 } else if (k.length == 2) {
867 return Stream.of(k[0]);
868 } else {
869 return IntStream.range(0, k.length / 2).mapToObj(i -> k[i * 2]);
870 }
871 }
872
873 /**
874 * Replies true, if the map of key/value pairs of this primitive is not empty.
875 *
876 * @return true, if the map of key/value pairs of this primitive is not empty; false otherwise
877 */
878 @Override
879 public final boolean hasKeys() {
880 return keys != null;
881 }
882
883 /**
884 * Replies true if this primitive has a tag with key <code>key</code>.
885 *
886 * @param key the key
887 * @return true, if this primitive has a tag with key <code>key</code>
888 */
889 @Override
890 public boolean hasKey(String key) {
891 return key != null && indexOfKey(keys, key) >= 0;
892 }
893
894 /**
895 * Replies true if this primitive has a tag any of the <code>keys</code>.
896 *
897 * @param keys the keys
898 * @return true, if this primitive has a tag with any of the <code>keys</code>
899 * @since 11587
900 */
901 public boolean hasKey(String... keys) {
902 if (keys != null) {
903 for (String key : keys) {
904 if (this.hasKey(key)) {
905 return true;
906 }
907 }
908 }
909 return false;
910 }
911
912 /**
913 * What to do, when the tags have changed by one of the tag-changing methods.
914 * @param originalKeys original tags
915 */
916 protected abstract void keysChangedImpl(Map<String, String> originalKeys);
917
918 /*-------------------------------------
919 * WORK IN PROGRESS, UNINTERESTING KEYS
920 *-------------------------------------*/
921
922 private static volatile Collection<String> workinprogress;
923 private static volatile Collection<String> uninteresting;
924 private static volatile Collection<String> discardable;
925
926 /**
927 * Returns a list of "uninteresting" keys that do not make an object
928 * "tagged". Entries that end with ':' are causing a whole namespace to be considered
929 * "uninteresting". Only the first level namespace is considered.
930 * Initialized by isUninterestingKey()
931 * @return The list of uninteresting keys.
932 */
933 public static Collection<String> getUninterestingKeys() {
934 if (uninteresting == null) {
935 List<String> l = new LinkedList<>(Arrays.asList(
936 "source", "source_ref", "source:", "comment", "import",
937 "watch", "watch:", "description", "attribution", GpxConstants.GPX_PREFIX));
938 l.addAll(getDiscardableKeys());
939 l.addAll(getWorkInProgressKeys());
940 uninteresting = new HashSet<>(Config.getPref().getList("tags.uninteresting", l));
941 }
942 return uninteresting;
943 }
944
945 /**
946 * Returns a list of keys which have been deemed uninteresting to the point
947 * that they can be silently removed from data which is being edited.
948 * @return The list of discardable keys.
949 */
950 public static Collection<String> getDiscardableKeys() {
951 if (discardable == null) {
952 discardable = new HashSet<>(Config.getPref().getList("tags.discardable",
953 Arrays.asList(
954 "created_by",
955 "converted_by",
956 "current_id", /* prevent export of this JOSM internal information, see OsmReader */
957 "geobase:datasetName",
958 "geobase:uuid",
959 "KSJ2:ADS",
960 "KSJ2:ARE",
961 "KSJ2:AdminArea",
962 "KSJ2:COP_label",
963 "KSJ2:DFD",
964 "KSJ2:INT",
965 "KSJ2:INT_label",
966 "KSJ2:LOC",
967 "KSJ2:LPN",
968 "KSJ2:OPC",
969 "KSJ2:PubFacAdmin",
970 "KSJ2:RAC",
971 "KSJ2:RAC_label",
972 "KSJ2:RIC",
973 "KSJ2:RIN",
974 "KSJ2:WSC",
975 "KSJ2:coordinate",
976 "KSJ2:curve_id",
977 "KSJ2:curve_type",
978 "KSJ2:filename",
979 "KSJ2:lake_id",
980 "KSJ2:lat",
981 "KSJ2:long",
982 "KSJ2:river_id",
983 "odbl",
984 "odbl:note",
985 "osmarender:nameDirection",
986 "osmarender:renderName",
987 "osmarender:renderRef",
988 "osmarender:rendernames",
989 "SK53_bulk:load",
990 "sub_sea:type",
991 "tiger:source",
992 "tiger:separated",
993 "tiger:tlid",
994 "tiger:upload_uuid",
995 "import_uuid",
996 "gnis:import_uuid",
997 "yh:LINE_NAME",
998 "yh:LINE_NUM",
999 "yh:STRUCTURE",
1000 "yh:TOTYUMONO",
1001 "yh:TYPE",
1002 "yh:WIDTH",
1003 "yh:WIDTH_RANK"
1004 )));
1005 }
1006 return discardable;
1007 }
1008
1009 /**
1010 * Returns a list of "work in progress" keys that do not make an object
1011 * "tagged" but "annotated".
1012 * @return The list of work in progress keys.
1013 * @since 5754
1014 */
1015 public static Collection<String> getWorkInProgressKeys() {
1016 if (workinprogress == null) {
1017 workinprogress = new HashSet<>(Config.getPref().getList("tags.workinprogress",
1018 Arrays.asList("note", "fixme", "FIXME")));
1019 }
1020 return workinprogress;
1021 }
1022
1023 /**
1024 * Determines if key is considered "uninteresting".
1025 * @param key The key to check
1026 * @return true if key is considered "uninteresting".
1027 */
1028 public static boolean isUninterestingKey(String key) {
1029 getUninterestingKeys();
1030 if (uninteresting.contains(key))
1031 return true;
1032 int pos = key.indexOf(':');
1033 if (pos > 0)
1034 return uninteresting.contains(key.substring(0, pos + 1));
1035 return false;
1036 }
1037
1038 @Override
1039 public Map<String, String> getInterestingTags() {
1040 Map<String, String> result = new HashMap<>();
1041 if (keys != null) {
1042 for (int i = 0; i < keys.length; i += 2) {
1043 if (!isUninterestingKey(keys[i])) {
1044 result.put(keys[i], keys[i + 1]);
1045 }
1046 }
1047 }
1048 return result;
1049 }
1050}
Note: See TracBrowser for help on using the repository browser.