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