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 }