001 package ezvcard.util; 002 003 import java.net.URI; 004 import java.text.DecimalFormat; 005 import java.text.NumberFormat; 006 import java.util.Arrays; 007 import java.util.LinkedHashMap; 008 import java.util.Map; 009 import java.util.regex.Matcher; 010 import java.util.regex.Pattern; 011 012 /* 013 Copyright (c) 2012, Michael Angstadt 014 All rights reserved. 015 016 Redistribution and use in source and binary forms, with or without 017 modification, are permitted provided that the following conditions are met: 018 019 1. Redistributions of source code must retain the above copyright notice, this 020 list of conditions and the following disclaimer. 021 2. Redistributions in binary form must reproduce the above copyright notice, 022 this list of conditions and the following disclaimer in the documentation 023 and/or other materials provided with the distribution. 024 025 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 026 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 027 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 028 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 029 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 030 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 031 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 032 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 033 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 034 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 035 036 The views and conclusions contained in the software and documentation are those 037 of the authors and should not be interpreted as representing official policies, 038 either expressed or implied, of the FreeBSD Project. 039 */ 040 041 /** 042 * <p> 043 * Represents a "geo" URI. 044 * </p> 045 * <p> 046 * Example geo URI: <code>geo:12.341,56.784</code> 047 * </p> 048 * @author Michael Angstadt 049 * @see <a href="http://tools.ietf.org/html/rfc5870">RFC 5870</a> 050 */ 051 public class GeoUri { 052 /** 053 * The coordinate reference system used by GPS (the default). 054 */ 055 public static final String CRS_WGS84 = "wgs84"; 056 057 /** 058 * The non-alphanumeric characters which are allowed to exist inside of a 059 * parameter value. 060 */ 061 protected static final char validParamValueChars[] = "!$&'()*+-.:[]_~".toCharArray(); 062 static { 063 //make sure the array is sorted for binary search 064 Arrays.sort(validParamValueChars); 065 } 066 067 /** 068 * Finds hex values in a parameter value. 069 */ 070 protected static final Pattern hexPattern = Pattern.compile("(?i)%([0-9a-f]{2})"); 071 072 protected static final String PARAM_CRS = "crs"; 073 protected static final String PARAM_UNCERTAINTY = "u"; 074 075 protected Double coordA; 076 protected Double coordB; 077 protected Double coordC; 078 protected String crs; 079 protected Double uncertainty; 080 protected Map<String, String> parameters = new LinkedHashMap<String, String>(); 081 082 public GeoUri() { 083 //do nothing 084 } 085 086 /** 087 * @param coordA the first coordinate (latitude, required) 088 * @param coordB the second coordinate (longitude, required) 089 * @param coordC the third coordinate (altitude, optional) 090 * @param crs the coordinate system (optional, defaults to WGS-84) 091 * @param uncertainty the accuracy of the coordinates (in meters, optional) 092 */ 093 public GeoUri(Double coordA, Double coordB, Double coordC, String crs, Double uncertainty) { 094 this.coordA = coordA; 095 this.coordB = coordB; 096 this.coordC = coordC; 097 this.crs = crs; 098 this.uncertainty = uncertainty; 099 } 100 101 /** 102 * Parses a geo URI string. 103 * @param uri the URI string 104 * @throws IllegalArgumentException if the string is not a valid geo URI 105 */ 106 public GeoUri(String uri) { 107 Pattern p = Pattern.compile("(?i)^geo:(-?\\d+(\\.\\d+)?),(-?\\d+(\\.\\d+)?)(,(-?\\d+(\\.\\d+)?))?(;(.*))?$"); 108 Matcher m = p.matcher(uri); 109 if (m.find()) { 110 coordA = Double.parseDouble(m.group(1)); 111 coordB = Double.parseDouble(m.group(3)); 112 113 String coordCStr = m.group(6); 114 if (coordCStr != null) { 115 coordC = Double.valueOf(coordCStr); 116 } 117 118 String paramsStr = m.group(9); 119 if (paramsStr != null) { 120 String paramsArray[] = paramsStr.split(";"); 121 122 for (String param : paramsArray) { 123 String paramSplit[] = param.split("=", 2); 124 String paramName = paramSplit[0]; 125 String paramValue = paramSplit.length > 1 ? paramSplit[1] : ""; 126 if (PARAM_CRS.equalsIgnoreCase(paramName)) { 127 crs = paramValue; 128 } else if (PARAM_UNCERTAINTY.equalsIgnoreCase(paramName)) { 129 uncertainty = Double.valueOf(paramValue); 130 } else { 131 paramValue = decodeParamValue(paramValue); 132 parameters.put(paramName, paramValue); 133 } 134 } 135 } 136 } else { 137 throw new IllegalArgumentException("Invalid geo URI: " + uri); 138 } 139 } 140 141 /** 142 * Gets the first coordinate (latitude). 143 * @return the first coordinate or null if there is none 144 */ 145 public Double getCoordA() { 146 return coordA; 147 } 148 149 /** 150 * Sets the first coordinate (latitude). 151 * @param coordA the first coordinate 152 */ 153 public void setCoordA(Double coordA) { 154 this.coordA = coordA; 155 } 156 157 /** 158 * Gets the second coordinate (longitude). 159 * @return the second coordinate or null if there is none 160 */ 161 public Double getCoordB() { 162 return coordB; 163 } 164 165 /** 166 * Sets the second coordinate (longitude). 167 * @param coordB the second coordinate 168 */ 169 public void setCoordB(Double coordB) { 170 this.coordB = coordB; 171 } 172 173 /** 174 * Gets the third coordinate (altitude). 175 * @return the third coordinate or null if there is none 176 */ 177 public Double getCoordC() { 178 return coordC; 179 } 180 181 /** 182 * Sets the third coordinate (altitude). 183 * @param coordC the third coordinate or null to remove 184 */ 185 public void setCoordC(Double coordC) { 186 this.coordC = coordC; 187 } 188 189 /** 190 * Gets the coordinate reference system. 191 * @return the coordinate reference system or null if using the default 192 * (WGS-84) 193 */ 194 public String getCrs() { 195 return crs; 196 } 197 198 /** 199 * Sets the coordinate reference system. 200 * @param crs the coordinate reference system (can only contain letters, 201 * numbers, and hyphens) or null to use the default (WGS-84) 202 * @throws IllegalArgumentException if the CRS name contains invalid 203 * characters 204 */ 205 public void setCrs(String crs) { 206 if (crs != null && !isLabelText(crs)) { 207 throw new IllegalArgumentException("CRS can only contain letters, numbers, and hypens."); 208 } 209 this.crs = crs; 210 } 211 212 /** 213 * Gets the uncertainty (how accurate the coordinates are). 214 * @return the uncertainty (in meters) or null if not set 215 */ 216 public Double getUncertainty() { 217 return uncertainty; 218 } 219 220 /** 221 * Sets the uncertainty (how accurate the coordinates are). 222 * @param uncertainty the uncertainty (in meters) or null to remove 223 */ 224 public void setUncertainty(Double uncertainty) { 225 this.uncertainty = uncertainty; 226 } 227 228 /** 229 * Adds a parameter. 230 * @param name the parameter name (can only contain letters, numbers, and 231 * hyphens) 232 * @param value the parameter value 233 * @throws IllegalArgumentException if the parameter name contains invalid 234 * characters 235 */ 236 public void addParameter(String name, String value) { 237 if (!isLabelText(name)) { 238 throw new IllegalArgumentException("Parameter names can only contain letters, numbers, and hyphens."); 239 } 240 241 if (value == null) { 242 value = ""; 243 } 244 245 parameters.put(name, value); 246 } 247 248 /** 249 * Removes a parameter. 250 * @param name the name of the parameter to remove 251 */ 252 public void removeParameter(String name) { 253 parameters.remove(name); 254 } 255 256 /** 257 * Gets a parameter value. 258 * @param name the parameter name 259 * @return the parameter value or null if not found 260 */ 261 public String getParameter(String name) { 262 return parameters.get(name); 263 } 264 265 /** 266 * Gets all the parameters. 267 * @return all the parameters 268 */ 269 public Map<String, String> getParameters() { 270 return new LinkedHashMap<String, String>(parameters); 271 } 272 273 /** 274 * Creates a {@link URI} object from this geo URI. 275 * @return the {@link URI} object 276 */ 277 public URI toUri() { 278 return URI.create(toString()); 279 } 280 281 /** 282 * Determines if the geo URI is valid or not (i.e. if both an A and B 283 * coordinate is present). 284 * @return true if it is valid, false if not 285 */ 286 public boolean isValid() { 287 return coordA != null && coordB != null; 288 } 289 290 /** 291 * Converts this geo URI to its string representation. 292 * @return the geo URI's string representation 293 */ 294 @Override 295 public String toString() { 296 return toString(6); 297 } 298 299 /** 300 * Converts this geo URI to its string representation. 301 * @param decimals the number of decimals to display for floating point 302 * values 303 * @return the geo URI's string representation 304 */ 305 public String toString(int decimals) { 306 NumberFormat nf = buildNumberFormat(decimals); 307 StringBuilder sb = new StringBuilder("geo:"); 308 309 if (coordA != null) { 310 sb.append(nf.format(coordA)); 311 } 312 313 sb.append(','); 314 if (coordB != null) { 315 sb.append(nf.format(coordB)); 316 } 317 318 if (coordC != null) { 319 sb.append(','); 320 sb.append(coordC); 321 } 322 323 //if the CRS is WGS-84, then it doesn't have to be displayed 324 if (crs != null && !crs.equalsIgnoreCase(CRS_WGS84)) { 325 sb.append(';').append(PARAM_CRS).append('='); 326 sb.append(crs); 327 } 328 329 if (uncertainty != null) { 330 sb.append(';').append(PARAM_UNCERTAINTY).append('='); 331 sb.append(nf.format(uncertainty)); 332 } 333 334 for (Map.Entry<String, String> entry : parameters.entrySet()) { 335 String name = entry.getKey(); 336 String value = entry.getValue(); 337 sb.append(';'); 338 sb.append(name); //note: the param name is validated in "addParameter()" 339 sb.append('='); 340 sb.append(encodeParamValue(value)); 341 } 342 343 return sb.toString(); 344 } 345 346 protected boolean isLabelText(String text) { 347 return text.matches("(?i)[-a-z0-9]+"); 348 } 349 350 protected String encodeParamValue(String value) { 351 StringBuilder sb = new StringBuilder(value.length()); 352 for (char c : value.toCharArray()) { 353 if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || Arrays.binarySearch(validParamValueChars, c) >= 0) { 354 sb.append(c); 355 } else { 356 int i = (int) c; 357 sb.append('%'); 358 sb.append(Integer.toString(i, 16)); 359 } 360 } 361 return sb.toString(); 362 } 363 364 protected String decodeParamValue(String value) { 365 Matcher m = hexPattern.matcher(value); 366 StringBuffer sb = new StringBuffer(); 367 while (m.find()) { 368 int hex = Integer.parseInt(m.group(1), 16); 369 m.appendReplacement(sb, "" + (char) hex); 370 } 371 m.appendTail(sb); 372 return sb.toString(); 373 } 374 375 protected NumberFormat buildNumberFormat(int decimals) { 376 StringBuilder sb = new StringBuilder(); 377 sb.append('0'); 378 if (decimals > 0) { 379 sb.append('.'); 380 for (int i = 0; i < decimals; i++) { 381 sb.append('#'); 382 } 383 } 384 return new DecimalFormat(sb.toString()); 385 } 386 387 @Override 388 public boolean equals(Object obj) { 389 //TODO implement 390 return super.equals(obj); 391 } 392 }