001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.gpx; 003 004import java.util.Objects; 005import java.util.Optional; 006 007import org.openstreetmap.josm.data.gpx.GpxData.XMLNamespace; 008import org.openstreetmap.josm.tools.Utils; 009import org.xml.sax.Attributes; 010 011/** 012 * A GpxExtension that has attributes and child extensions (implements {@link IWithAttributes} and {@link GpxConstants}). 013 * @since 15496 014 */ 015public class GpxExtension extends WithAttributes { 016 private final String qualifiedName, prefix, key; 017 private IWithAttributes parent; 018 private String value; 019 private boolean visible = true; 020 021 /** 022 * Constructs a new {@link GpxExtension}. 023 * @param prefix the prefix 024 * @param key the key 025 * @param value the value 026 */ 027 public GpxExtension(String prefix, String key, String value) { 028 this.prefix = Optional.ofNullable(prefix).orElse(""); 029 this.key = key; 030 this.value = value; 031 this.qualifiedName = (this.prefix.isEmpty() ? "" : this.prefix + ":") + key; 032 } 033 034 /** 035 * Creates a new {@link GpxExtension} 036 * 037 * @param namespaceURI the URI of the XML namespace, used to determine supported extensions 038 * (josm, gpxx, gpxd) regardless of the prefix that could legally vary from file to file. 039 * @param qName the qualified name of the XML element including prefix 040 * @param atts the attributes 041 */ 042 public GpxExtension(String namespaceURI, String qName, Attributes atts) { 043 qualifiedName = qName; 044 int dot = qName.indexOf(':'); 045 String p = findPrefix(namespaceURI); 046 if (p == null) { 047 if (dot != -1) { 048 prefix = qName.substring(0, dot); 049 } else { 050 prefix = ""; 051 } 052 } else { 053 prefix = p; 054 } 055 key = qName.substring(dot + 1); 056 for (int i = 0; i < atts.getLength(); i++) { 057 attr.put(atts.getLocalName(i), atts.getValue(i)); 058 } 059 } 060 061 /** 062 * Finds the default prefix used by JOSM for the given namespaceURI as the document is free specify another one. 063 * @param namespaceURI namespace URI 064 * @return the prefix 065 */ 066 public static String findPrefix(String namespaceURI) { 067 if (XML_URI_EXTENSIONS_DRAWING.equals(namespaceURI)) 068 return "gpxd"; 069 070 if (XML_URI_EXTENSIONS_GARMIN.equals(namespaceURI)) 071 return "gpxx"; 072 073 if (XML_URI_EXTENSIONS_JOSM.equals(namespaceURI)) 074 return "josm"; 075 076 return null; 077 } 078 079 /** 080 * Finds the namespace for the given default prefix, if supported with schema location 081 * @param prefix the prefix used by JOSM 082 * @return the {@link XMLNamespace} element, location and URI can be <code>null</code> if not found. 083 */ 084 public static XMLNamespace findNamespace(String prefix) { 085 switch (prefix) { 086 case "gpxx": 087 return new XMLNamespace("gpxx", XML_URI_EXTENSIONS_GARMIN, XML_XSD_EXTENSIONS_GARMIN); 088 case "gpxd": 089 return new XMLNamespace("gpxd", XML_URI_EXTENSIONS_DRAWING, XML_XSD_EXTENSIONS_DRAWING); 090 case "josm": 091 return new XMLNamespace("josm", XML_URI_EXTENSIONS_JOSM, XML_XSD_EXTENSIONS_JOSM); 092 } 093 return null; 094 } 095 096 /** 097 * Returns the qualified name of the XML element. 098 * @return the qualified name of the XML element 099 */ 100 public String getQualifiedName() { 101 return qualifiedName; 102 } 103 104 /** 105 * Returns the prefix of the XML namespace. 106 * @return the prefix of the XML namespace 107 */ 108 public String getPrefix() { 109 return prefix; 110 } 111 112 /** 113 * Returns the key (local element name) of the extension. 114 * @return the key (local element name) of the extension 115 */ 116 public String getKey() { 117 return key; 118 } 119 120 /** 121 * Returns the flattened extension key of this extension. 122 * @return the flattened extension key of this extension, used for conversion to OSM layers 123 */ 124 public String getFlatKey() { 125 String ret = ""; 126 if (parent instanceof GpxExtension) { 127 GpxExtension ext = (GpxExtension) parent; 128 ret = ext.getFlatKey() + ":"; 129 } 130 return ret + getKey(); 131 } 132 133 /** 134 * Searches recursively for the extension with the given key in all children 135 * @param sPrefix the prefix to look for 136 * @param sKey the key to look for 137 * @return the extension if found, otherwise <code>null</code> 138 */ 139 public GpxExtension findExtension(String sPrefix, String sKey) { 140 if (prefix.equalsIgnoreCase(sPrefix) && key.equalsIgnoreCase(sKey)) { 141 return this; 142 } else { 143 return getExtensions().stream() 144 .map(child -> child.findExtension(sPrefix, sKey)) 145 .filter(Objects::nonNull) 146 .findFirst().orElse(null); 147 } 148 } 149 150 /** 151 * Returns the value of the extension. 152 * @return the value of the extension 153 */ 154 public String getValue() { 155 return value; 156 } 157 158 /** 159 * Sets the value. 160 * @param value the value to set 161 */ 162 public void setValue(String value) { 163 this.value = value; 164 } 165 166 /** 167 * Removes this extension from its parent and all then-empty parents 168 * @throws IllegalStateException if parent not set 169 */ 170 public void remove() { 171 if (parent == null) 172 throw new IllegalStateException("Extension " + qualifiedName + " has no parent, can't remove it."); 173 174 parent.getExtensions().remove(this); 175 if (parent instanceof GpxExtension) { 176 GpxExtension gpx = ((GpxExtension) parent); 177 if (Utils.isBlank(gpx.getValue()) 178 && Utils.isEmpty(gpx.getAttributes()) 179 && Utils.isEmpty(gpx.getExtensions())) { 180 gpx.remove(); 181 } 182 } 183 } 184 185 /** 186 * Hides this extension and all then-empty parents so it isn't written 187 * @see #isVisible() 188 */ 189 public void hide() { 190 visible = false; 191 if (parent != null && parent instanceof GpxExtension) { 192 GpxExtension gpx = (GpxExtension) parent; 193 if (Utils.isBlank(gpx.getValue()) 194 && gpx.getAttributes().isEmpty() 195 && !gpx.getExtensions().isVisible()) { 196 gpx.hide(); 197 } 198 } 199 } 200 201 /** 202 * Shows this extension and all parents so it can be written 203 * @see #isVisible() 204 */ 205 public void show() { 206 visible = true; 207 if (parent != null && parent instanceof GpxExtension) { 208 ((GpxExtension) parent).show(); 209 } 210 } 211 212 /** 213 * Determines if this extension should be written. 214 * @return if this extension should be written, used for hiding colors during export without removing them 215 */ 216 public boolean isVisible() { 217 return visible; 218 } 219 220 /** 221 * Returns the parent element of this extension. 222 * @return the parent element of this extension, can be another extension or gpx elements (data, track, segment, ...) 223 */ 224 public IWithAttributes getParent() { 225 return parent; 226 } 227 228 /** 229 * Sets the parent for this extension 230 * @param parent the parent 231 * @throws IllegalStateException if parent already set 232 */ 233 public void setParent(IWithAttributes parent) { 234 if (this.parent != null) 235 throw new IllegalStateException("Parent of extension " + qualifiedName + " is already set"); 236 237 this.parent = parent; 238 } 239 240 @Override 241 public int hashCode() { 242 return Objects.hash(prefix, key, value, attr, visible, super.hashCode()); 243 } 244 245 @Override 246 public boolean equals(Object obj) { 247 if (this == obj) 248 return true; 249 if (obj == null) 250 return false; 251 if (!super.equals(obj)) 252 return false; 253 if (!(obj instanceof GpxExtension)) 254 return false; 255 GpxExtension other = (GpxExtension) obj; 256 if (visible != other.visible) 257 return false; 258 if (prefix == null) { 259 if (other.prefix != null) 260 return false; 261 } else if (!prefix.equals(other.prefix)) 262 return false; 263 if (key == null) { 264 if (other.key != null) 265 return false; 266 } else if (!key.equals(other.key)) 267 return false; 268 if (value == null) { 269 if (other.value != null) 270 return false; 271 } else if (!value.equals(other.value)) 272 return false; 273 if (attr == null) { 274 if (other.attr != null) 275 return false; 276 } else if (!attr.equals(other.attr)) 277 return false; 278 return true; 279 } 280}