source: josm/trunk/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java@ 9853

Last change on this file since 9853 was 9695, checked in by simon04, 8 years ago

see #12457 - MapCSS "modified" not updating after uploading changes

  • Property svn:eol-style set to native
File size: 49.7 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.text.MessageFormat;
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.Collection;
10import java.util.Collections;
11import java.util.Date;
12import java.util.HashMap;
13import java.util.HashSet;
14import java.util.LinkedHashSet;
15import java.util.LinkedList;
16import java.util.List;
17import java.util.Locale;
18import java.util.Map;
19import java.util.Objects;
20import java.util.Set;
21
22import org.openstreetmap.josm.Main;
23import org.openstreetmap.josm.actions.search.SearchCompiler;
24import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
25import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
26import org.openstreetmap.josm.data.osm.visitor.Visitor;
27import org.openstreetmap.josm.gui.mappaint.StyleCache;
28import org.openstreetmap.josm.tools.CheckParameterUtil;
29import org.openstreetmap.josm.tools.Predicate;
30import org.openstreetmap.josm.tools.Utils;
31import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider;
32
33/**
34 * The base class for OSM objects ({@link Node}, {@link Way}, {@link Relation}).
35 *
36 * It can be created, deleted and uploaded to the OSM-Server.
37 *
38 * Although OsmPrimitive is designed as a base class, it is not to be meant to subclass
39 * it by any other than from the package {@link org.openstreetmap.josm.data.osm}. The available primitives are a fixed set that are given
40 * by the server environment and not an extendible data stuff.
41 *
42 * @author imi
43 */
44public abstract class OsmPrimitive extends AbstractPrimitive implements Comparable<OsmPrimitive>, TemplateEngineDataProvider {
45 private static final String SPECIAL_VALUE_ID = "id";
46 private static final String SPECIAL_VALUE_LOCAL_NAME = "localname";
47
48 /**
49 * An object can be disabled by the filter mechanism.
50 * Then it will show in a shade of gray on the map or it is completely
51 * hidden from the view.
52 * Disabled objects usually cannot be selected or modified
53 * while the filter is active.
54 */
55 protected static final int FLAG_DISABLED = 1 << 4;
56
57 /**
58 * This flag is only relevant if an object is disabled by the
59 * filter mechanism (i.e.&nbsp;FLAG_DISABLED is set).
60 * Then it indicates, whether it is completely hidden or
61 * just shown in gray color.
62 *
63 * When the primitive is not disabled, this flag should be
64 * unset as well (for efficient access).
65 */
66 protected static final int FLAG_HIDE_IF_DISABLED = 1 << 5;
67
68 /**
69 * Flag used internally by the filter mechanism.
70 */
71 protected static final int FLAG_DISABLED_TYPE = 1 << 6;
72
73 /**
74 * Flag used internally by the filter mechanism.
75 */
76 protected static final int FLAG_HIDDEN_TYPE = 1 << 7;
77
78 /**
79 * This flag is set if the primitive is a way and
80 * according to the tags, the direction of the way is important.
81 * (e.g. one way street.)
82 */
83 protected static final int FLAG_HAS_DIRECTIONS = 1 << 8;
84
85 /**
86 * If the primitive is tagged.
87 * Some trivial tags like source=* are ignored here.
88 */
89 protected static final int FLAG_TAGGED = 1 << 9;
90
91 /**
92 * This flag is only relevant if FLAG_HAS_DIRECTIONS is set.
93 * It shows, that direction of the arrows should be reversed.
94 * (E.g. oneway=-1.)
95 */
96 protected static final int FLAG_DIRECTION_REVERSED = 1 << 10;
97
98 /**
99 * When hovering over ways and nodes in add mode, the
100 * "target" objects are visually highlighted. This flag indicates
101 * that the primitive is currently highlighted.
102 */
103 protected static final int FLAG_HIGHLIGHTED = 1 << 11;
104
105 /**
106 * If the primitive is annotated with a tag such as note, fixme, etc.
107 * Match the "work in progress" tags in default map style.
108 */
109 protected static final int FLAG_ANNOTATED = 1 << 12;
110
111 /**
112 * Replies the sub-collection of {@link OsmPrimitive}s of type <code>type</code> present in
113 * another collection of {@link OsmPrimitive}s. The result collection is a list.
114 *
115 * If <code>list</code> is null, replies an empty list.
116 *
117 * @param <T> type of data (must be one of the {@link OsmPrimitive} types
118 * @param list the original list
119 * @param type the type to filter for
120 * @return the sub-list of OSM primitives of type <code>type</code>
121 */
122 public static <T extends OsmPrimitive> List<T> getFilteredList(Collection<OsmPrimitive> list, Class<T> type) {
123 if (list == null) return Collections.emptyList();
124 List<T> ret = new LinkedList<>();
125 for (OsmPrimitive p: list) {
126 if (type.isInstance(p)) {
127 ret.add(type.cast(p));
128 }
129 }
130 return ret;
131 }
132
133 /**
134 * Replies the sub-collection of {@link OsmPrimitive}s of type <code>type</code> present in
135 * another collection of {@link OsmPrimitive}s. The result collection is a set.
136 *
137 * If <code>list</code> is null, replies an empty set.
138 *
139 * @param <T> type of data (must be one of the {@link OsmPrimitive} types
140 * @param set the original collection
141 * @param type the type to filter for
142 * @return the sub-set of OSM primitives of type <code>type</code>
143 */
144 public static <T extends OsmPrimitive> Set<T> getFilteredSet(Collection<OsmPrimitive> set, Class<T> type) {
145 Set<T> ret = new LinkedHashSet<>();
146 if (set != null) {
147 for (OsmPrimitive p: set) {
148 if (type.isInstance(p)) {
149 ret.add(type.cast(p));
150 }
151 }
152 }
153 return ret;
154 }
155
156 /**
157 * Replies the collection of referring primitives for the primitives in <code>primitives</code>.
158 *
159 * @param primitives the collection of primitives.
160 * @return the collection of referring primitives for the primitives in <code>primitives</code>;
161 * empty set if primitives is null or if there are no referring primitives
162 */
163 public static Set<OsmPrimitive> getReferrer(Collection<? extends OsmPrimitive> primitives) {
164 Set<OsmPrimitive> ret = new HashSet<>();
165 if (primitives == null || primitives.isEmpty()) return ret;
166 for (OsmPrimitive p: primitives) {
167 ret.addAll(p.getReferrers());
168 }
169 return ret;
170 }
171
172 /**
173 * Some predicates, that describe conditions on primitives.
174 */
175 public static final Predicate<OsmPrimitive> isUsablePredicate = new Predicate<OsmPrimitive>() {
176 @Override
177 public boolean evaluate(OsmPrimitive primitive) {
178 return primitive.isUsable();
179 }
180 };
181
182 public static final Predicate<OsmPrimitive> isSelectablePredicate = new Predicate<OsmPrimitive>() {
183 @Override
184 public boolean evaluate(OsmPrimitive primitive) {
185 return primitive.isSelectable();
186 }
187 };
188
189 public static final Predicate<OsmPrimitive> nonDeletedPredicate = new Predicate<OsmPrimitive>() {
190 @Override public boolean evaluate(OsmPrimitive primitive) {
191 return !primitive.isDeleted();
192 }
193 };
194
195 public static final Predicate<OsmPrimitive> nonDeletedCompletePredicate = new Predicate<OsmPrimitive>() {
196 @Override public boolean evaluate(OsmPrimitive primitive) {
197 return !primitive.isDeleted() && !primitive.isIncomplete();
198 }
199 };
200
201 public static final Predicate<OsmPrimitive> nonDeletedPhysicalPredicate = new Predicate<OsmPrimitive>() {
202 @Override public boolean evaluate(OsmPrimitive primitive) {
203 return !primitive.isDeleted() && !primitive.isIncomplete() && !(primitive instanceof Relation);
204 }
205 };
206
207 public static final Predicate<OsmPrimitive> modifiedPredicate = new Predicate<OsmPrimitive>() {
208 @Override public boolean evaluate(OsmPrimitive primitive) {
209 return primitive.isModified();
210 }
211 };
212
213 public static final Predicate<OsmPrimitive> nodePredicate = new Predicate<OsmPrimitive>() {
214 @Override public boolean evaluate(OsmPrimitive primitive) {
215 return primitive.getClass() == Node.class;
216 }
217 };
218
219 public static final Predicate<OsmPrimitive> wayPredicate = new Predicate<OsmPrimitive>() {
220 @Override public boolean evaluate(OsmPrimitive primitive) {
221 return primitive.getClass() == Way.class;
222 }
223 };
224
225 public static final Predicate<OsmPrimitive> relationPredicate = new Predicate<OsmPrimitive>() {
226 @Override public boolean evaluate(OsmPrimitive primitive) {
227 return primitive.getClass() == Relation.class;
228 }
229 };
230
231 public static final Predicate<OsmPrimitive> multipolygonPredicate = new Predicate<OsmPrimitive>() {
232 @Override public boolean evaluate(OsmPrimitive primitive) {
233 return primitive.getClass() == Relation.class && ((Relation) primitive).isMultipolygon();
234 }
235 };
236
237 public static final Predicate<OsmPrimitive> allPredicate = new Predicate<OsmPrimitive>() {
238 @Override public boolean evaluate(OsmPrimitive primitive) {
239 return true;
240 }
241 };
242
243 public static final Predicate<Tag> directionalKeyPredicate = new Predicate<Tag>() {
244 @Override
245 public boolean evaluate(Tag tag) {
246 return directionKeys.match(tag);
247 }
248 };
249
250 /**
251 * Creates a new primitive for the given id.
252 *
253 * If allowNegativeId is set, provided id can be &lt; 0 and will be set to primitive without any processing.
254 * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or
255 * positive number.
256 *
257 * @param id the id
258 * @param allowNegativeId {@code true} to allow negative id
259 * @throws IllegalArgumentException if id &lt; 0 and allowNegativeId is false
260 */
261 protected OsmPrimitive(long id, boolean allowNegativeId) {
262 if (allowNegativeId) {
263 this.id = id;
264 } else {
265 if (id < 0)
266 throw new IllegalArgumentException(MessageFormat.format("Expected ID >= 0. Got {0}.", id));
267 else if (id == 0) {
268 this.id = generateUniqueId();
269 } else {
270 this.id = id;
271 }
272
273 }
274 this.version = 0;
275 this.setIncomplete(id > 0);
276 }
277
278 /**
279 * Creates a new primitive for the given id and version.
280 *
281 * If allowNegativeId is set, provided id can be &lt; 0 and will be set to primitive without any processing.
282 * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or
283 * positive number.
284 *
285 * If id is not &gt; 0 version is ignored and set to 0.
286 *
287 * @param id the id
288 * @param version the version (positive integer)
289 * @param allowNegativeId {@code true} to allow negative id
290 * @throws IllegalArgumentException if id &lt; 0 and allowNegativeId is false
291 */
292 protected OsmPrimitive(long id, int version, boolean allowNegativeId) {
293 this(id, allowNegativeId);
294 this.version = (id > 0 ? version : 0);
295 setIncomplete(id > 0 && version == 0);
296 }
297
298 /*----------
299 * MAPPAINT
300 *--------*/
301 public StyleCache mappaintStyle;
302 public int mappaintCacheIdx;
303
304 /* This should not be called from outside. Fixing the UI to add relevant
305 get/set functions calling this implicitely is preferred, so we can have
306 transparent cache handling in the future. */
307 public void clearCachedStyle() {
308 mappaintStyle = null;
309 }
310 /* end of mappaint data */
311
312 /*---------
313 * DATASET
314 *---------*/
315
316 /** the parent dataset */
317 private DataSet dataSet;
318
319 /**
320 * This method should never ever by called from somewhere else than Dataset.addPrimitive or removePrimitive methods
321 * @param dataSet the parent dataset
322 */
323 void setDataset(DataSet dataSet) {
324 if (this.dataSet != null && dataSet != null && this.dataSet != dataSet)
325 throw new DataIntegrityProblemException("Primitive cannot be included in more than one Dataset");
326 this.dataSet = dataSet;
327 }
328
329 /**
330 *
331 * @return DataSet this primitive is part of.
332 */
333 public DataSet getDataSet() {
334 return dataSet;
335 }
336
337 /**
338 * Throws exception if primitive is not part of the dataset
339 */
340 public void checkDataset() {
341 if (dataSet == null)
342 throw new DataIntegrityProblemException("Primitive must be part of the dataset: " + toString());
343 }
344
345 protected boolean writeLock() {
346 if (dataSet != null) {
347 dataSet.beginUpdate();
348 return true;
349 } else
350 return false;
351 }
352
353 protected void writeUnlock(boolean locked) {
354 if (locked) {
355 // It shouldn't be possible for dataset to become null because
356 // method calling setDataset would need write lock which is owned by this thread
357 dataSet.endUpdate();
358 }
359 }
360
361 /**
362 * Sets the id and the version of this primitive if it is known to the OSM API.
363 *
364 * Since we know the id and its version it can't be incomplete anymore. incomplete
365 * is set to false.
366 *
367 * @param id the id. &gt; 0 required
368 * @param version the version &gt; 0 required
369 * @throws IllegalArgumentException if id &lt;= 0
370 * @throws IllegalArgumentException if version &lt;= 0
371 * @throws DataIntegrityProblemException if id is changed and primitive was already added to the dataset
372 */
373 @Override
374 public void setOsmId(long id, int version) {
375 boolean locked = writeLock();
376 try {
377 if (id <= 0)
378 throw new IllegalArgumentException(tr("ID > 0 expected. Got {0}.", id));
379 if (version <= 0)
380 throw new IllegalArgumentException(tr("Version > 0 expected. Got {0}.", version));
381 if (dataSet != null && id != this.id) {
382 DataSet datasetCopy = dataSet;
383 // Reindex primitive
384 datasetCopy.removePrimitive(this);
385 this.id = id;
386 datasetCopy.addPrimitive(this);
387 }
388 super.setOsmId(id, version);
389 } finally {
390 writeUnlock(locked);
391 }
392 }
393
394 /**
395 * Clears the metadata, including id and version known to the OSM API.
396 * The id is a new unique id. The version, changeset and timestamp are set to 0.
397 * incomplete and deleted are set to false. It's preferred to use copy constructor with clearMetadata set to true instead
398 *
399 * <strong>Caution</strong>: Do not use this method on primitives which are already added to a {@link DataSet}.
400 *
401 * @throws DataIntegrityProblemException If primitive was already added to the dataset
402 * @since 6140
403 */
404 @Override
405 public void clearOsmMetadata() {
406 if (dataSet != null)
407 throw new DataIntegrityProblemException("Method cannot be called after primitive was added to the dataset");
408 super.clearOsmMetadata();
409 }
410
411 @Override
412 public void setUser(User user) {
413 boolean locked = writeLock();
414 try {
415 super.setUser(user);
416 } finally {
417 writeUnlock(locked);
418 }
419 }
420
421 @Override
422 public void setChangesetId(int changesetId) {
423 boolean locked = writeLock();
424 try {
425 int old = this.changesetId;
426 super.setChangesetId(changesetId);
427 if (dataSet != null) {
428 dataSet.fireChangesetIdChanged(this, old, changesetId);
429 }
430 } finally {
431 writeUnlock(locked);
432 }
433 }
434
435 @Override
436 public void setTimestamp(Date timestamp) {
437 boolean locked = writeLock();
438 try {
439 super.setTimestamp(timestamp);
440 } finally {
441 writeUnlock(locked);
442 }
443 }
444
445
446 /* -------
447 /* FLAGS
448 /* ------*/
449
450 private void updateFlagsNoLock(int flag, boolean value) {
451 super.updateFlags(flag, value);
452 }
453
454 @Override
455 protected final void updateFlags(int flag, boolean value) {
456 boolean locked = writeLock();
457 try {
458 updateFlagsNoLock(flag, value);
459 } finally {
460 writeUnlock(locked);
461 }
462 }
463
464 /**
465 * Make the primitive disabled (e.g.&nbsp;if a filter applies).
466 *
467 * To enable the primitive again, use unsetDisabledState.
468 * @param hidden if the primitive should be completely hidden from view or
469 * just shown in gray color.
470 * @return true, any flag has changed; false if you try to set the disabled
471 * state to the value that is already preset
472 */
473 public boolean setDisabledState(boolean hidden) {
474 boolean locked = writeLock();
475 try {
476 int oldFlags = flags;
477 updateFlagsNoLock(FLAG_DISABLED, true);
478 updateFlagsNoLock(FLAG_HIDE_IF_DISABLED, hidden);
479 return oldFlags != flags;
480 } finally {
481 writeUnlock(locked);
482 }
483 }
484
485 /**
486 * Remove the disabled flag from the primitive.
487 * Afterwards, the primitive is displayed normally and can be selected again.
488 * @return {@code true} if a change occurred
489 */
490 public boolean unsetDisabledState() {
491 boolean locked = writeLock();
492 try {
493 int oldFlags = flags;
494 updateFlagsNoLock(FLAG_DISABLED + FLAG_HIDE_IF_DISABLED, false);
495 return oldFlags != flags;
496 } finally {
497 writeUnlock(locked);
498 }
499 }
500
501 /**
502 * Set binary property used internally by the filter mechanism.
503 * @param isExplicit new "disabled type" flag value
504 */
505 public void setDisabledType(boolean isExplicit) {
506 updateFlags(FLAG_DISABLED_TYPE, isExplicit);
507 }
508
509 /**
510 * Set binary property used internally by the filter mechanism.
511 * @param isExplicit new "hidden type" flag value
512 */
513 public void setHiddenType(boolean isExplicit) {
514 updateFlags(FLAG_HIDDEN_TYPE, isExplicit);
515 }
516
517 /**
518 * Replies true, if this primitive is disabled. (E.g. a filter applies)
519 * @return {@code true} if this object has the "disabled" flag enabled
520 */
521 public boolean isDisabled() {
522 return (flags & FLAG_DISABLED) != 0;
523 }
524
525 /**
526 * Replies true, if this primitive is disabled and marked as completely hidden on the map.
527 * @return {@code true} if this object has both the "disabled" and "hide if disabled" flags enabled
528 */
529 public boolean isDisabledAndHidden() {
530 return ((flags & FLAG_DISABLED) != 0) && ((flags & FLAG_HIDE_IF_DISABLED) != 0);
531 }
532
533 /**
534 * Get binary property used internally by the filter mechanism.
535 * @return {@code true} if this object has the "hidden type" flag enabled
536 */
537 public boolean getHiddenType() {
538 return (flags & FLAG_HIDDEN_TYPE) != 0;
539 }
540
541 /**
542 * Get binary property used internally by the filter mechanism.
543 * @return {@code true} if this object has the "disabled type" flag enabled
544 */
545 public boolean getDisabledType() {
546 return (flags & FLAG_DISABLED_TYPE) != 0;
547 }
548
549 /**
550 * Determines if this object is selectable.
551 * @return {@code true} if this object is selectable
552 */
553 public boolean isSelectable() {
554 return (flags & (FLAG_DELETED + FLAG_INCOMPLETE + FLAG_DISABLED + FLAG_HIDE_IF_DISABLED)) == 0;
555 }
556
557 /**
558 * Determines if this object is drawable.
559 * @return {@code true} if this object is drawable
560 */
561 public boolean isDrawable() {
562 return (flags & (FLAG_DELETED + FLAG_INCOMPLETE + FLAG_HIDE_IF_DISABLED)) == 0;
563 }
564
565 @Override
566 public void setModified(boolean modified) {
567 boolean locked = writeLock();
568 try {
569 super.setModified(modified);
570 clearCachedStyle();
571 } finally {
572 writeUnlock(locked);
573 }
574 }
575
576 @Override
577 public void setVisible(boolean visible) {
578 boolean locked = writeLock();
579 try {
580 super.setVisible(visible);
581 clearCachedStyle();
582 } finally {
583 writeUnlock(locked);
584 }
585 }
586
587 @Override
588 public void setDeleted(boolean deleted) {
589 boolean locked = writeLock();
590 try {
591 super.setDeleted(deleted);
592 if (dataSet != null) {
593 if (deleted) {
594 dataSet.firePrimitivesRemoved(Collections.singleton(this), false);
595 } else {
596 dataSet.firePrimitivesAdded(Collections.singleton(this), false);
597 }
598 }
599 clearCachedStyle();
600 } finally {
601 writeUnlock(locked);
602 }
603 }
604
605 @Override
606 protected final void setIncomplete(boolean incomplete) {
607 boolean locked = writeLock();
608 try {
609 if (dataSet != null && incomplete != this.isIncomplete()) {
610 if (incomplete) {
611 dataSet.firePrimitivesRemoved(Collections.singletonList(this), true);
612 } else {
613 dataSet.firePrimitivesAdded(Collections.singletonList(this), true);
614 }
615 }
616 super.setIncomplete(incomplete);
617 } finally {
618 writeUnlock(locked);
619 }
620 }
621
622 /**
623 * Determines whether the primitive is selected
624 * @return whether the primitive is selected
625 * @see DataSet#isSelected(OsmPrimitive)
626 */
627 public boolean isSelected() {
628 return dataSet != null && dataSet.isSelected(this);
629 }
630
631 /**
632 * Determines if this primitive is a member of a selected relation.
633 * @return {@code true} if this primitive is a member of a selected relation, {@code false} otherwise
634 */
635 public boolean isMemberOfSelected() {
636 if (referrers == null)
637 return false;
638 if (referrers instanceof OsmPrimitive)
639 return referrers instanceof Relation && ((OsmPrimitive) referrers).isSelected();
640 for (OsmPrimitive ref : (OsmPrimitive[]) referrers) {
641 if (ref instanceof Relation && ref.isSelected())
642 return true;
643 }
644 return false;
645 }
646
647 /**
648 * Determines if this primitive is an outer member of a selected multipolygon relation.
649 * @return {@code true} if this primitive is an outer member of a selected multipolygon relation, {@code false} otherwise
650 * @since 7621
651 */
652 public boolean isOuterMemberOfSelected() {
653 if (referrers == null)
654 return false;
655 if (referrers instanceof OsmPrimitive) {
656 return isOuterMemberOfMultipolygon((OsmPrimitive) referrers);
657 }
658 for (OsmPrimitive ref : (OsmPrimitive[]) referrers) {
659 if (isOuterMemberOfMultipolygon(ref))
660 return true;
661 }
662 return false;
663 }
664
665 private boolean isOuterMemberOfMultipolygon(OsmPrimitive ref) {
666 if (ref instanceof Relation && ref.isSelected() && ((Relation) ref).isMultipolygon()) {
667 for (RelationMember rm : ((Relation) ref).getMembersFor(Collections.singleton(this))) {
668 if ("outer".equals(rm.getRole())) {
669 return true;
670 }
671 }
672 }
673 return false;
674 }
675
676 public void setHighlighted(boolean highlighted) {
677 if (isHighlighted() != highlighted) {
678 updateFlags(FLAG_HIGHLIGHTED, highlighted);
679 if (dataSet != null) {
680 dataSet.fireHighlightingChanged();
681 }
682 }
683 }
684
685 public boolean isHighlighted() {
686 return (flags & FLAG_HIGHLIGHTED) != 0;
687 }
688
689 /*---------------------------------------------------
690 * WORK IN PROGRESS, UNINTERESTING AND DIRECTION KEYS
691 *--------------------------------------------------*/
692
693 private static volatile Collection<String> workinprogress;
694 private static volatile Collection<String> uninteresting;
695 private static volatile Collection<String> discardable;
696
697 /**
698 * Returns a list of "uninteresting" keys that do not make an object
699 * "tagged". Entries that end with ':' are causing a whole namespace to be considered
700 * "uninteresting". Only the first level namespace is considered.
701 * Initialized by isUninterestingKey()
702 * @return The list of uninteresting keys.
703 */
704 public static Collection<String> getUninterestingKeys() {
705 if (uninteresting == null) {
706 List<String> l = new LinkedList<>(Arrays.asList(
707 "source", "source_ref", "source:", "comment",
708 "converted_by", "watch", "watch:",
709 "description", "attribution"));
710 l.addAll(getDiscardableKeys());
711 l.addAll(getWorkInProgressKeys());
712 uninteresting = Main.pref.getCollection("tags.uninteresting", l);
713 }
714 return uninteresting;
715 }
716
717 /**
718 * Returns a list of keys which have been deemed uninteresting to the point
719 * that they can be silently removed from data which is being edited.
720 * @return The list of discardable keys.
721 */
722 public static Collection<String> getDiscardableKeys() {
723 if (discardable == null) {
724 discardable = Main.pref.getCollection("tags.discardable",
725 Arrays.asList(
726 "created_by",
727 "geobase:datasetName",
728 "geobase:uuid",
729 "KSJ2:ADS",
730 "KSJ2:ARE",
731 "KSJ2:AdminArea",
732 "KSJ2:COP_label",
733 "KSJ2:DFD",
734 "KSJ2:INT",
735 "KSJ2:INT_label",
736 "KSJ2:LOC",
737 "KSJ2:LPN",
738 "KSJ2:OPC",
739 "KSJ2:PubFacAdmin",
740 "KSJ2:RAC",
741 "KSJ2:RAC_label",
742 "KSJ2:RIC",
743 "KSJ2:RIN",
744 "KSJ2:WSC",
745 "KSJ2:coordinate",
746 "KSJ2:curve_id",
747 "KSJ2:curve_type",
748 "KSJ2:filename",
749 "KSJ2:lake_id",
750 "KSJ2:lat",
751 "KSJ2:long",
752 "KSJ2:river_id",
753 "odbl",
754 "odbl:note",
755 "SK53_bulk:load",
756 "sub_sea:type",
757 "tiger:source",
758 "tiger:separated",
759 "tiger:tlid",
760 "tiger:upload_uuid",
761 "yh:LINE_NAME",
762 "yh:LINE_NUM",
763 "yh:STRUCTURE",
764 "yh:TOTYUMONO",
765 "yh:TYPE",
766 "yh:WIDTH",
767 "yh:WIDTH_RANK"
768 ));
769 }
770 return discardable;
771 }
772
773 /**
774 * Returns a list of "work in progress" keys that do not make an object
775 * "tagged" but "annotated".
776 * @return The list of work in progress keys.
777 * @since 5754
778 */
779 public static Collection<String> getWorkInProgressKeys() {
780 if (workinprogress == null) {
781 workinprogress = Main.pref.getCollection("tags.workinprogress",
782 Arrays.asList("note", "fixme", "FIXME"));
783 }
784 return workinprogress;
785 }
786
787 /**
788 * Determines if key is considered "uninteresting".
789 * @param key The key to check
790 * @return true if key is considered "uninteresting".
791 */
792 public static boolean isUninterestingKey(String key) {
793 getUninterestingKeys();
794 if (uninteresting.contains(key))
795 return true;
796 int pos = key.indexOf(':');
797 if (pos > 0)
798 return uninteresting.contains(key.substring(0, pos + 1));
799 return false;
800 }
801
802 /**
803 * Returns {@link #getKeys()} for which {@code key} does not fulfill {@link #isUninterestingKey}.
804 * @return A map of interesting tags
805 */
806 public Map<String, String> getInterestingTags() {
807 Map<String, String> result = new HashMap<>();
808 String[] keys = this.keys;
809 if (keys != null) {
810 for (int i = 0; i < keys.length; i += 2) {
811 if (!isUninterestingKey(keys[i])) {
812 result.put(keys[i], keys[i + 1]);
813 }
814 }
815 }
816 return result;
817 }
818
819 private static volatile Match directionKeys;
820 private static volatile Match reversedDirectionKeys;
821
822 /**
823 * Contains a list of direction-dependent keys that make an object
824 * direction dependent.
825 * Initialized by checkDirectionTagged()
826 */
827 static {
828 String reversedDirectionDefault = "oneway=\"-1\"";
829
830 String directionDefault = "oneway? | (aerialway=* -aerialway=station) | "+
831 "waterway=stream | waterway=river | waterway=ditch | waterway=drain | "+
832 "\"piste:type\"=downhill | \"piste:type\"=sled | man_made=\"piste:halfpipe\" | "+
833 "junction=roundabout | (highway=motorway & -oneway=no & -oneway=reversible) | "+
834 "(highway=motorway_link & -oneway=no & -oneway=reversible)";
835
836 try {
837 reversedDirectionKeys = SearchCompiler.compile(Main.pref.get("tags.reversed_direction", reversedDirectionDefault));
838 } catch (ParseError e) {
839 Main.error("Unable to compile pattern for tags.reversed_direction, trying default pattern: " + e.getMessage());
840
841 try {
842 reversedDirectionKeys = SearchCompiler.compile(reversedDirectionDefault);
843 } catch (ParseError e2) {
844 throw new AssertionError("Unable to compile default pattern for direction keys: " + e2.getMessage(), e2);
845 }
846 }
847 try {
848 directionKeys = SearchCompiler.compile(Main.pref.get("tags.direction", directionDefault));
849 } catch (ParseError e) {
850 Main.error("Unable to compile pattern for tags.direction, trying default pattern: " + e.getMessage());
851
852 try {
853 directionKeys = SearchCompiler.compile(directionDefault);
854 } catch (ParseError e2) {
855 throw new AssertionError("Unable to compile default pattern for direction keys: " + e2.getMessage(), e2);
856 }
857 }
858 }
859
860 private void updateTagged() {
861 for (String key: keySet()) {
862 // 'area' is not really uninteresting (putting it in that list may have unpredictable side effects)
863 // but it's clearly not enough to consider an object as tagged (see #9261)
864 if (!isUninterestingKey(key) && !"area".equals(key)) {
865 updateFlagsNoLock(FLAG_TAGGED, true);
866 return;
867 }
868 }
869 updateFlagsNoLock(FLAG_TAGGED, false);
870 }
871
872 private void updateAnnotated() {
873 for (String key: keySet()) {
874 if (getWorkInProgressKeys().contains(key)) {
875 updateFlagsNoLock(FLAG_ANNOTATED, true);
876 return;
877 }
878 }
879 updateFlagsNoLock(FLAG_ANNOTATED, false);
880 }
881
882 /**
883 * Determines if this object is considered "tagged". To be "tagged", an object
884 * must have one or more "interesting" tags. "created_by" and "source"
885 * are typically considered "uninteresting" and do not make an object
886 * "tagged".
887 * @return true if this object is considered "tagged"
888 */
889 public boolean isTagged() {
890 return (flags & FLAG_TAGGED) != 0;
891 }
892
893 /**
894 * Determines if this object is considered "annotated". To be "annotated", an object
895 * must have one or more "work in progress" tags, such as "note" or "fixme".
896 * @return true if this object is considered "annotated"
897 * @since 5754
898 */
899 public boolean isAnnotated() {
900 return (flags & FLAG_ANNOTATED) != 0;
901 }
902
903 private void updateDirectionFlags() {
904 boolean hasDirections = false;
905 boolean directionReversed = false;
906 if (reversedDirectionKeys.match(this)) {
907 hasDirections = true;
908 directionReversed = true;
909 }
910 if (directionKeys.match(this)) {
911 hasDirections = true;
912 }
913
914 updateFlagsNoLock(FLAG_DIRECTION_REVERSED, directionReversed);
915 updateFlagsNoLock(FLAG_HAS_DIRECTIONS, hasDirections);
916 }
917
918 /**
919 * true if this object has direction dependent tags (e.g. oneway)
920 * @return {@code true} if this object has direction dependent tags
921 */
922 public boolean hasDirectionKeys() {
923 return (flags & FLAG_HAS_DIRECTIONS) != 0;
924 }
925
926 /**
927 * true if this object has the "reversed diretion" flag enabled
928 * @return {@code true} if this object has the "reversed diretion" flag enabled
929 */
930 public boolean reversedDirection() {
931 return (flags & FLAG_DIRECTION_REVERSED) != 0;
932 }
933
934 /*------------
935 * Keys handling
936 ------------*/
937
938 @Override
939 public final void setKeys(TagMap keys) {
940 boolean locked = writeLock();
941 try {
942 super.setKeys(keys);
943 } finally {
944 writeUnlock(locked);
945 }
946 }
947
948 @Override
949 public final void setKeys(Map<String, String> keys) {
950 boolean locked = writeLock();
951 try {
952 super.setKeys(keys);
953 } finally {
954 writeUnlock(locked);
955 }
956 }
957
958 @Override
959 public final void put(String key, String value) {
960 boolean locked = writeLock();
961 try {
962 super.put(key, value);
963 } finally {
964 writeUnlock(locked);
965 }
966 }
967
968 @Override
969 public final void remove(String key) {
970 boolean locked = writeLock();
971 try {
972 super.remove(key);
973 } finally {
974 writeUnlock(locked);
975 }
976 }
977
978 @Override
979 public final void removeAll() {
980 boolean locked = writeLock();
981 try {
982 super.removeAll();
983 } finally {
984 writeUnlock(locked);
985 }
986 }
987
988 @Override
989 protected void keysChangedImpl(Map<String, String> originalKeys) {
990 clearCachedStyle();
991 if (dataSet != null) {
992 for (OsmPrimitive ref : getReferrers()) {
993 ref.clearCachedStyle();
994 }
995 }
996 updateDirectionFlags();
997 updateTagged();
998 updateAnnotated();
999 if (dataSet != null) {
1000 dataSet.fireTagsChanged(this, originalKeys);
1001 }
1002 }
1003
1004 /*------------
1005 * Referrers
1006 ------------*/
1007
1008 private Object referrers;
1009
1010 /**
1011 * Add new referrer. If referrer is already included then no action is taken
1012 * @param referrer The referrer to add
1013 */
1014 protected void addReferrer(OsmPrimitive referrer) {
1015 if (referrers == null) {
1016 referrers = referrer;
1017 } else if (referrers instanceof OsmPrimitive) {
1018 if (referrers != referrer) {
1019 referrers = new OsmPrimitive[] {(OsmPrimitive) referrers, referrer};
1020 }
1021 } else {
1022 for (OsmPrimitive primitive:(OsmPrimitive[]) referrers) {
1023 if (primitive == referrer)
1024 return;
1025 }
1026 referrers = Utils.addInArrayCopy((OsmPrimitive[]) referrers, referrer);
1027 }
1028 }
1029
1030 /**
1031 * Remove referrer. No action is taken if referrer is not registered
1032 * @param referrer The referrer to remove
1033 */
1034 protected void removeReferrer(OsmPrimitive referrer) {
1035 if (referrers instanceof OsmPrimitive) {
1036 if (referrers == referrer) {
1037 referrers = null;
1038 }
1039 } else if (referrers instanceof OsmPrimitive[]) {
1040 OsmPrimitive[] orig = (OsmPrimitive[]) referrers;
1041 int idx = -1;
1042 for (int i = 0; i < orig.length; i++) {
1043 if (orig[i] == referrer) {
1044 idx = i;
1045 break;
1046 }
1047 }
1048 if (idx == -1)
1049 return;
1050
1051 if (orig.length == 2) {
1052 referrers = orig[1-idx]; // idx is either 0 or 1, take the other
1053 } else { // downsize the array
1054 OsmPrimitive[] smaller = new OsmPrimitive[orig.length-1];
1055 System.arraycopy(orig, 0, smaller, 0, idx);
1056 System.arraycopy(orig, idx+1, smaller, idx, smaller.length-idx);
1057 referrers = smaller;
1058 }
1059 }
1060 }
1061
1062 /**
1063 * Find primitives that reference this primitive. Returns only primitives that are included in the same
1064 * dataset as this primitive. <br>
1065 *
1066 * For example following code will add wnew as referer to all nodes of existingWay, but this method will
1067 * not return wnew because it's not part of the dataset <br>
1068 *
1069 * <code>Way wnew = new Way(existingWay)</code>
1070 *
1071 * @param allowWithoutDataset If true, method will return empty list if primitive is not part of the dataset. If false,
1072 * exception will be thrown in this case
1073 *
1074 * @return a collection of all primitives that reference this primitive.
1075 */
1076 public final List<OsmPrimitive> getReferrers(boolean allowWithoutDataset) {
1077 // Returns only referrers that are members of the same dataset (primitive can have some fake references, for example
1078 // when way is cloned
1079
1080 if (dataSet == null && allowWithoutDataset)
1081 return Collections.emptyList();
1082
1083 checkDataset();
1084 Object referrers = this.referrers;
1085 List<OsmPrimitive> result = new ArrayList<>();
1086 if (referrers != null) {
1087 if (referrers instanceof OsmPrimitive) {
1088 OsmPrimitive ref = (OsmPrimitive) referrers;
1089 if (ref.dataSet == dataSet) {
1090 result.add(ref);
1091 }
1092 } else {
1093 for (OsmPrimitive o:(OsmPrimitive[]) referrers) {
1094 if (dataSet == o.dataSet) {
1095 result.add(o);
1096 }
1097 }
1098 }
1099 }
1100 return result;
1101 }
1102
1103 public final List<OsmPrimitive> getReferrers() {
1104 return getReferrers(false);
1105 }
1106
1107 /**
1108 * <p>Visits {@code visitor} for all referrers.</p>
1109 *
1110 * @param visitor the visitor. Ignored, if null.
1111 */
1112 public void visitReferrers(Visitor visitor) {
1113 if (visitor == null) return;
1114 if (this.referrers == null)
1115 return;
1116 else if (this.referrers instanceof OsmPrimitive) {
1117 OsmPrimitive ref = (OsmPrimitive) this.referrers;
1118 if (ref.dataSet == dataSet) {
1119 ref.accept(visitor);
1120 }
1121 } else if (this.referrers instanceof OsmPrimitive[]) {
1122 OsmPrimitive[] refs = (OsmPrimitive[]) this.referrers;
1123 for (OsmPrimitive ref: refs) {
1124 if (ref.dataSet == dataSet) {
1125 ref.accept(visitor);
1126 }
1127 }
1128 }
1129 }
1130
1131 /**
1132 Return true, if this primitive is referred by at least n ways
1133 @param n Minimal number of ways to return true. Must be positive
1134 * @return {@code true} if this primitive is referred by at least n ways
1135 */
1136 public final boolean isReferredByWays(int n) {
1137 // Count only referrers that are members of the same dataset (primitive can have some fake references, for example
1138 // when way is cloned
1139 Object referrers = this.referrers;
1140 if (referrers == null) return false;
1141 checkDataset();
1142 if (referrers instanceof OsmPrimitive)
1143 return n <= 1 && referrers instanceof Way && ((OsmPrimitive) referrers).dataSet == dataSet;
1144 else {
1145 int counter = 0;
1146 for (OsmPrimitive o : (OsmPrimitive[]) referrers) {
1147 if (dataSet == o.dataSet && o instanceof Way) {
1148 if (++counter >= n)
1149 return true;
1150 }
1151 }
1152 return false;
1153 }
1154 }
1155
1156 /*-----------------
1157 * OTHER METHODS
1158 *----------------*/
1159
1160 /**
1161 * Implementation of the visitor scheme. Subclasses have to call the correct
1162 * visitor function.
1163 * @param visitor The visitor from which the visit() function must be called.
1164 */
1165 public abstract void accept(Visitor visitor);
1166
1167 /**
1168 * Get and write all attributes from the parameter. Does not fire any listener, so
1169 * use this only in the data initializing phase
1170 * @param other other primitive
1171 */
1172 public void cloneFrom(OsmPrimitive other) {
1173 // write lock is provided by subclasses
1174 if (id != other.id && dataSet != null)
1175 throw new DataIntegrityProblemException("Osm id cannot be changed after primitive was added to the dataset");
1176
1177 super.cloneFrom(other);
1178 clearCachedStyle();
1179 }
1180
1181 /**
1182 * Merges the technical and semantical attributes from <code>other</code> onto this.
1183 *
1184 * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code>
1185 * have an assigend OSM id, the IDs have to be the same.
1186 *
1187 * @param other the other primitive. Must not be null.
1188 * @throws IllegalArgumentException if other is null.
1189 * @throws DataIntegrityProblemException if either this is new and other is not, or other is new and this is not
1190 * @throws DataIntegrityProblemException if other isn't new and other.getId() != this.getId()
1191 */
1192 public void mergeFrom(OsmPrimitive other) {
1193 boolean locked = writeLock();
1194 try {
1195 CheckParameterUtil.ensureParameterNotNull(other, "other");
1196 if (other.isNew() ^ isNew())
1197 throw new DataIntegrityProblemException(
1198 tr("Cannot merge because either of the participating primitives is new and the other is not"));
1199 if (!other.isNew() && other.getId() != id)
1200 throw new DataIntegrityProblemException(
1201 tr("Cannot merge primitives with different ids. This id is {0}, the other is {1}", id, other.getId()));
1202
1203 setKeys(other.hasKeys() ? other.getKeys() : null);
1204 timestamp = other.timestamp;
1205 version = other.version;
1206 setIncomplete(other.isIncomplete());
1207 flags = other.flags;
1208 user = other.user;
1209 changesetId = other.changesetId;
1210 } finally {
1211 writeUnlock(locked);
1212 }
1213 }
1214
1215 /**
1216 * Replies true if other isn't null and has the same interesting tags (key/value-pairs) as this.
1217 *
1218 * @param other the other object primitive
1219 * @return true if other isn't null and has the same interesting tags (key/value-pairs) as this.
1220 */
1221 public boolean hasSameInterestingTags(OsmPrimitive other) {
1222 return (keys == null && other.keys == null)
1223 || getInterestingTags().equals(other.getInterestingTags());
1224 }
1225
1226 /**
1227 * Replies true if this primitive and other are equal with respect to their semantic attributes.
1228 * <ol>
1229 * <li>equal id</li>
1230 * <li>both are complete or both are incomplete</li>
1231 * <li>both have the same tags</li>
1232 * </ol>
1233 * @param other other primitive to compare
1234 * @return true if this primitive and other are equal with respect to their semantic attributes.
1235 */
1236 public boolean hasEqualSemanticAttributes(OsmPrimitive other) {
1237 if (!isNew() && id != other.id)
1238 return false;
1239 if (isIncomplete() ^ other.isIncomplete()) // exclusive or operator for performance (see #7159)
1240 return false;
1241 // can't do an equals check on the internal keys array because it is not ordered
1242 //
1243 return hasSameInterestingTags(other);
1244 }
1245
1246 /**
1247 * Replies true if this primitive and other are equal with respect to their technical attributes.
1248 * The attributes:
1249 * <ol>
1250 * <li>deleted</li>
1251 * <li>modified</li>
1252 * <li>timestamp</li>
1253 * <li>version</li>
1254 * <li>visible</li>
1255 * <li>user</li>
1256 * </ol>
1257 * have to be equal
1258 * @param other the other primitive
1259 * @return true if this primitive and other are equal with respect to their technical attributes
1260 */
1261 public boolean hasEqualTechnicalAttributes(OsmPrimitive other) {
1262 if (other == null) return false;
1263
1264 return isDeleted() == other.isDeleted()
1265 && isModified() == other.isModified()
1266 && timestamp == other.timestamp
1267 && version == other.version
1268 && isVisible() == other.isVisible()
1269 && (user == null ? other.user == null : user == other.user)
1270 && changesetId == other.changesetId;
1271 }
1272
1273 /**
1274 * Loads (clone) this primitive from provided PrimitiveData
1275 * @param data The object which should be cloned
1276 */
1277 public void load(PrimitiveData data) {
1278 // Write lock is provided by subclasses
1279 setKeys(data.hasKeys() ? data.getKeys() : null);
1280 setRawTimestamp(data.getRawTimestamp());
1281 user = data.getUser();
1282 setChangesetId(data.getChangesetId());
1283 setDeleted(data.isDeleted());
1284 setModified(data.isModified());
1285 setIncomplete(data.isIncomplete());
1286 version = data.getVersion();
1287 }
1288
1289 /**
1290 * Save parameters of this primitive to the transport object
1291 * @return The saved object data
1292 */
1293 public abstract PrimitiveData save();
1294
1295 /**
1296 * Save common parameters of primitives to the transport object
1297 * @param data The object to save the data into
1298 */
1299 protected void saveCommonAttributes(PrimitiveData data) {
1300 data.setId(id);
1301 data.setKeys(hasKeys() ? getKeys() : null);
1302 data.setRawTimestamp(getRawTimestamp());
1303 data.setUser(user);
1304 data.setDeleted(isDeleted());
1305 data.setModified(isModified());
1306 data.setVisible(isVisible());
1307 data.setIncomplete(isIncomplete());
1308 data.setChangesetId(changesetId);
1309 data.setVersion(version);
1310 }
1311
1312 /**
1313 * Fetch the bounding box of the primitive
1314 * @return Bounding box of the object
1315 */
1316 public abstract BBox getBBox();
1317
1318 /**
1319 * Called by Dataset to update cached position information of primitive (bbox, cached EarthNorth, ...)
1320 */
1321 public abstract void updatePosition();
1322
1323 /*----------------
1324 * OBJECT METHODS
1325 *---------------*/
1326
1327 @Override
1328 protected String getFlagsAsString() {
1329 StringBuilder builder = new StringBuilder(super.getFlagsAsString());
1330
1331 if (isDisabled()) {
1332 if (isDisabledAndHidden()) {
1333 builder.append('h');
1334 } else {
1335 builder.append('d');
1336 }
1337 }
1338 if (isTagged()) {
1339 builder.append('T');
1340 }
1341 if (hasDirectionKeys()) {
1342 if (reversedDirection()) {
1343 builder.append('<');
1344 } else {
1345 builder.append('>');
1346 }
1347 }
1348 return builder.toString();
1349 }
1350
1351 /**
1352 * Equal, if the id (and class) is equal.
1353 *
1354 * An primitive is equal to its incomplete counter part.
1355 */
1356 @Override
1357 public boolean equals(Object obj) {
1358 if (this == obj) return true;
1359 if (obj == null || getClass() != obj.getClass()) return false;
1360 OsmPrimitive that = (OsmPrimitive) obj;
1361 return Objects.equals(id, that.id);
1362 }
1363
1364 /**
1365 * Return the id plus the class type encoded as hashcode or super's hashcode if id is 0.
1366 *
1367 * An primitive has the same hashcode as its incomplete counterpart.
1368 */
1369 @Override
1370 public int hashCode() {
1371 return Objects.hash(id);
1372 }
1373
1374 /**
1375 * Replies the display name of a primitive formatted by <code>formatter</code>
1376 * @param formatter formatter to use
1377 *
1378 * @return the display name
1379 */
1380 public abstract String getDisplayName(NameFormatter formatter);
1381
1382 @Override
1383 public Collection<String> getTemplateKeys() {
1384 Collection<String> keySet = keySet();
1385 List<String> result = new ArrayList<>(keySet.size() + 2);
1386 result.add(SPECIAL_VALUE_ID);
1387 result.add(SPECIAL_VALUE_LOCAL_NAME);
1388 result.addAll(keySet);
1389 return result;
1390 }
1391
1392 @Override
1393 public Object getTemplateValue(String name, boolean special) {
1394 if (special) {
1395 String lc = name.toLowerCase(Locale.ENGLISH);
1396 if (SPECIAL_VALUE_ID.equals(lc))
1397 return getId();
1398 else if (SPECIAL_VALUE_LOCAL_NAME.equals(lc))
1399 return getLocalName();
1400 else
1401 return null;
1402
1403 } else
1404 return getIgnoreCase(name);
1405 }
1406
1407 @Override
1408 public boolean evaluateCondition(Match condition) {
1409 return condition.match(this);
1410 }
1411
1412 /**
1413 * Replies the set of referring relations
1414 * @param primitives primitives to fetch relations from
1415 *
1416 * @return the set of referring relations
1417 */
1418 public static Set<Relation> getParentRelations(Collection<? extends OsmPrimitive> primitives) {
1419 Set<Relation> ret = new HashSet<>();
1420 for (OsmPrimitive w : primitives) {
1421 ret.addAll(OsmPrimitive.getFilteredList(w.getReferrers(), Relation.class));
1422 }
1423 return ret;
1424 }
1425
1426 /**
1427 * Determines if this primitive has tags denoting an area.
1428 * @return {@code true} if this primitive has tags denoting an area, {@code false} otherwise.
1429 * @since 6491
1430 */
1431 public final boolean hasAreaTags() {
1432 return hasKey("landuse")
1433 || "yes".equals(get("area"))
1434 || "riverbank".equals(get("waterway"))
1435 || hasKey("natural")
1436 || hasKey("amenity")
1437 || hasKey("leisure")
1438 || hasKey("building")
1439 || hasKey("building:part");
1440 }
1441
1442 /**
1443 * Determines if this primitive semantically concerns an area.
1444 * @return {@code true} if this primitive semantically concerns an area, according to its type, geometry and tags, {@code false} otherwise.
1445 * @since 6491
1446 */
1447 public abstract boolean concernsArea();
1448
1449 /**
1450 * Tests if this primitive lies outside of the downloaded area of its {@link DataSet}.
1451 * @return {@code true} if this primitive lies outside of the downloaded area
1452 */
1453 public abstract boolean isOutsideDownloadArea();
1454}
Note: See TracBrowser for help on using the repository browser.