ISO6709.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tika.parser.mp4.boxes;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Parses ISO 6709 location strings as written in QuickTime/MP4 metadata: the udta
* "©xyz" box and the {@code com.apple.quicktime.location.ISO6709} value. Returns the
* latitude, longitude and optional altitude.
* <p>
* Both the decimal-degree form (e.g. {@code +12.3456-098.7654+010.500/}) and the ISO 6709
* sexagesimal compact forms ({@code ��DDMM.MMMM} / {@code ��DDMMSS.SSSS} for latitude and the
* {@code ��DDD...} equivalents for longitude) are handled. The form is chosen by the
* integer-digit count of each angle, which is unambiguous for valid coordinates: a decimal
* latitude ({@code |lat| <= 90}) has at most 2 integer digits and a decimal longitude
* ({@code |lon| <= 180}) at most 3, so neither can collide with the 4/6 (latitude) or 5/7
* (longitude) integer-digit counts of the minute/second forms. Any other digit count falls
* back to a lenient decimal parse.
*/
public final class ISO6709 {
//latitude, longitude and an optional altitude, each a signed number; any trailing
//CRS designator and the "/" terminator are ignored
private static final Pattern PATTERN = Pattern.compile(
"([+-]\\d+(?:\\.\\d+)?)([+-]\\d+(?:\\.\\d+)?)([+-]\\d+(?:\\.\\d+)?)?");
private ISO6709() {
}
public static final class Location {
public final double latitude;
public final double longitude;
//null when the string carries no altitude component
public final Double altitude;
Location(double latitude, double longitude, Double altitude) {
this.latitude = latitude;
this.longitude = longitude;
this.altitude = altitude;
}
}
/**
* @param s an ISO 6709 location string, or null
* @return the parsed location, or null if {@code s} is null or contains no location
*/
public static Location parse(String s) {
if (s == null) {
return null;
}
Matcher matcher = PATTERN.matcher(s);
if (!matcher.find()) {
return null;
}
double latitude = decodeAngle(matcher.group(1), 2); //latitude: 2 degree digits
double longitude = decodeAngle(matcher.group(2), 3); //longitude: 3 degree digits
Double altitude = matcher.group(3) == null ? null : Double.parseDouble(matcher.group(3));
return new Location(latitude, longitude, altitude);
}
/**
* Decodes one signed angle. {@code degreeDigits} is the number of integer digits the
* degrees field occupies in the ISO 6709 compact forms: 2 for latitude, 3 for longitude.
*/
private static double decodeAngle(String token, int degreeDigits) {
char sign = token.charAt(0);
int dot = token.indexOf('.');
String intPart = dot < 0 ? token.substring(1) : token.substring(1, dot);
String frac = dot < 0 ? "" : token.substring(dot); //includes the '.'
int digits = intPart.length();
double value;
if (digits == degreeDigits + 2) {
//��DDMM.MMMM (or ��DDDMM.MMMM): degrees + minutes/60
double degrees = Integer.parseInt(intPart.substring(0, degreeDigits));
double minutes = Double.parseDouble(intPart.substring(degreeDigits) + frac);
value = degrees + minutes / 60.0;
} else if (digits == degreeDigits + 4) {
//��DDMMSS.SSSS (or ��DDDMMSS.SSSS): degrees + minutes/60 + seconds/3600
double degrees = Integer.parseInt(intPart.substring(0, degreeDigits));
double minutes = Integer.parseInt(intPart.substring(degreeDigits, degreeDigits + 2));
double seconds = Double.parseDouble(intPart.substring(degreeDigits + 2) + frac);
value = degrees + minutes / 60.0 + seconds / 3600.0;
} else {
//decimal degrees (the common case) or a non-conformant count: lenient fallback
value = Double.parseDouble(intPart + frac);
}
return sign == '-' ? -value : value;
}
}