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    }