It's a cross-platform java application, and its home page is at https://gpsprune.activityworkshop.net .
-Here on github you'll find all the sources from version 1 to the current version 20, and in the wiki at https://github.com/activityworkshop/GpsPrune/wiki there's the beginning of a translation effort for anyone to contribute.
-Currently just the Spanish translations and some missing French texts are online, to see whether it's a workable idea or not. Please help with these if you can.
+Here on github you'll find all the sources from version 1 to the current version 20.4, and in the wiki at https://github.com/activityworkshop/GpsPrune/wiki there's the beginning of a translation effort for anyone to contribute.
+Currently just the missing French texts and Polish texts are online, to see whether it's a workable idea or not. Please help with these if you can.
# Build script
set -e
# Version number
-PRUNENAME=gpsprune_20
+PRUNENAME=gpsprune_20.4
# remove compile directory
rm -rf compile
# remove dist directory
<groupId>tim.prune</groupId>
<artifactId>gpsprune</artifactId>
- <version>20</version>
+ <version>20.4</version>
<packaging>jar</packaging>
<name>tim.prune.gpsprune</name>
<artifactId>j3dutils</artifactId>
<version>${j3dutils.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <version>5.7.1</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <version>5.7.1</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
<finalName>${project.artifactId}_${project.version}</finalName>
- <sourceDirectory>${project.basedir}/</sourceDirectory>
+ <sourceDirectory>${project.basedir}/src</sourceDirectory>
+ <testSourceDirectory>${project.basedir}/test</testSourceDirectory>
<resources>
<resource>
<directory>${project.basedir}/src/</directory>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
+ <configuration>
+ <compilerArgs>
+ <arg>-Xlint:deprecation</arg>
+ </compilerArgs>
+ </configuration>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<mainClass>${app.mainClass}</mainClass>
</configuration>
</plugin>
-
+ <plugin>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>2.22.2</version>
+ </plugin>
+ <plugin>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <version>2.22.2</version>
+ </plugin>
</plugins>
</pluginManagement>
</build>
-version=20
+version=20.4
public class GpsPrune
{
/** Version number of application, used in about screen and for version check */
- public static final String VERSION_NUMBER = "20.3";
+ public static final String VERSION_NUMBER = "20.4";
/** Build number, just used for about screen */
- public static final String BUILD_NUMBER = "385";
+ public static final String BUILD_NUMBER = "387";
/** Static reference to App object */
private static App APP = null;
private boolean _foundTrackPoint = false;
protected AltitudeRange _totalAltitudeRange = new AltitudeRange();
protected AltitudeRange _movingAltitudeRange = new AltitudeRange();
- private Timestamp _earliestTimestamp = null, _latestTimestamp = null;
+ private Timestamp _earliestTimestamp = null, _latestTimestamp = null, _movingTimestamp = null;
private long _movingMilliseconds = 0L;
private boolean _timesIncomplete = false;
private boolean _timesOutOfSequence = false;
}
// timestamps
+ if (inPoint.getSegmentStart())
+ {
+ // reset movingTimestamp for moving time at the start
+ // of each segment
+ _movingTimestamp = null;
+ }
if (inPoint.hasTimestamp())
{
Timestamp currTstamp = inPoint.getTimestamp();
if (_latestTimestamp == null || currTstamp.isAfter(_latestTimestamp)) {
_latestTimestamp = currTstamp;
}
+
// Work out duration without segment gaps
- if (!inPoint.getSegmentStart() && _prevPoint != null && _prevPoint.hasTimestamp())
+ if (_movingTimestamp != null)
{
- long millisLater = currTstamp.getMillisecondsSince(_prevPoint.getTimestamp());
+ long millisLater = currTstamp.getMillisecondsSince(_movingTimestamp);
if (millisLater < 0) {
_timesOutOfSequence = true;
}
_movingMilliseconds += millisLater;
}
}
+ _movingTimestamp = currTstamp;
}
else {
_timesIncomplete = true;
// try to download to cache
TileDownloader cacher = new TileDownloader();
TileDownloader.Result result = cacher.downloadTile(inUrl);
- System.out.println("Result: " + result);
+ // System.out.println("Result: " + result);
if (result == TileDownloader.Result.DOWNLOADED)
{
_numCached++;
final int[] xPixels = new int[numPoints];
final int[] yPixels = new int[numPoints];
- final int pointSeparationForArrowsSqd = 350;
+ final int pointSeparationForArrowsSqd = 400;
final int pointSeparation1dForArrows = (int) (Math.sqrt(pointSeparationForArrowsSqd) * 0.7);
+ final int hugePointSeparationForArrows = 120;
// try to set line width for painting
if (inG instanceof Graphics2D)
pointsPainted = true;
// Now consider whether we need to draw an arrow as well
- if (drawArrows
- && !drawnLastArrow
- && (Math.abs(prevX-px) > pointSeparation1dForArrows || Math.abs(prevY-py) > pointSeparation1dForArrows))
+ if (drawArrows)
{
- final double pointSeparationSqd = (prevX-px) * (prevX-px) + (prevY-py) * (prevY-py);
- if (pointSeparationSqd > pointSeparationForArrowsSqd)
+ final double pointDist = Math.max(Math.abs(prevX - px), Math.abs(prevY - py));
+ final int separationLimit = (drawnLastArrow ? hugePointSeparationForArrows : pointSeparation1dForArrows);
+ if (pointDist > separationLimit)
{
- final double midX = (prevX + px) / 2.0;
- final double midY = (prevY + py) / 2.0;
- final boolean midPointVisible = midX >= 0 && midX < winWidth && midY >= 0 && midY < winHeight;
- if (midPointVisible)
+ final double pointSeparationSqd = (prevX-px) * (prevX-px) + (prevY-py) * (prevY-py);
+ if (pointSeparationSqd > pointSeparationForArrowsSqd)
{
- final double alpha = Math.atan2(py - prevY, px - prevX);
- //System.out.println("Draw arrow from (" + prevX + "," + prevY + ") to (" + px + "," + py
- // + ") with angle" + (int) (alpha * 180/Math.PI));
- final double MID_TO_VERTEX = 3.0;
- final double arrowX = MID_TO_VERTEX * Math.cos(alpha);
- final double arrowY = MID_TO_VERTEX * Math.sin(alpha);
- final double vertexX = midX + arrowX;
- final double vertexY = midY + arrowY;
- inG.drawLine((int)(midX-arrowX-2*arrowY), (int)(midY-arrowY+2*arrowX), (int)vertexX, (int)vertexY);
- inG.drawLine((int)(midX-arrowX+2*arrowY), (int)(midY-arrowY-2*arrowX), (int)vertexX, (int)vertexY);
+ final double midX = (prevX + px) / 2.0;
+ final double midY = (prevY + py) / 2.0;
+ final boolean midPointVisible = midX >= 0 && midX < winWidth && midY >= 0 && midY < winHeight;
+ if (midPointVisible)
+ {
+ final double alpha = Math.atan2(py - prevY, px - prevX);
+ //System.out.println("Draw arrow from (" + prevX + "," + prevY + ") to (" + px + "," + py
+ // + ") with angle" + (int) (alpha * 180/Math.PI));
+ final double MID_TO_VERTEX = 3.0;
+ final double arrowX = MID_TO_VERTEX * Math.cos(alpha);
+ final double arrowY = MID_TO_VERTEX * Math.sin(alpha);
+ final double vertexX = midX + arrowX;
+ final double vertexY = midY + arrowY;
+ inG.drawLine((int)(midX-arrowX-2*arrowY), (int)(midY-arrowY+2*arrowX), (int)vertexX, (int)vertexY);
+ inG.drawLine((int)(midX-arrowX+2*arrowY), (int)(midY-arrowY-2*arrowX), (int)vertexX, (int)vertexY);
+ }
+ drawnLastArrow = midPointVisible;
}
- drawnLastArrow = midPointVisible;
}
- }
- else
- {
- drawnLastArrow = false;
+ else
+ {
+ drawnLastArrow = false;
+ }
}
}
prevX = px; prevY = py;
import java.net.MalformedURLException;
import java.net.URL;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+
/**
* Class to represent any map source, whether an OsmMapSource
/** File extensions */
protected String[] _extensions = null;
- /** Regular expression for catching server wildcards */
- protected static final Pattern WILD_PATTERN = Pattern.compile("^(.*)\\[(.*)\\](.*)$");
-
/**
* @return the number of layers used in this source
return urlstr;
}
- /**
- * Fix the site name by stripping off protocol and www.
- * This is used to create the file path for disk caching
- * @param inUrl url to strip
- * @return stripped url
- */
- protected static String fixSiteName(String inUrl)
- {
- if (inUrl == null || inUrl.equals("")) {return null;}
- String url = inUrl.toLowerCase();
- int idx = url.indexOf("://");
- if (idx >= 0) {url = url.substring(idx + 3);}
- if (url.startsWith("www.")) {url = url.substring(4);}
- // Strip out any "[.*]" as well
- if (url.indexOf('[') >= 0)
- {
- Matcher matcher = WILD_PATTERN.matcher(url);
- if (matcher.matches()) {
- url = matcher.group(1) + matcher.group(3);
- if (url.length() > 1 && url.charAt(0) == '.') {
- url = url.substring(1);
- }
- }
- }
- return url;
- }
/**
* @return string which can be written to the Config
_baseUrls[0] = fixBaseUrl(inUrl1);
_baseUrls[1] = fixBaseUrl(inUrl2);
_siteNames = new String[2];
- _siteNames[0] = fixSiteName(_baseUrls[0]);
- _siteNames[1] = fixSiteName(_baseUrls[1]);
+ _siteNames[0] = SiteNameUtils.convertUrlToDirectory(_baseUrls[0]);
+ _siteNames[1] = SiteNameUtils.convertUrlToDirectory(_baseUrls[1]);
_extensions = new String[2];
_extensions[0] = inExt1;
_extensions[1] = inExt2;
package tim.prune.gui.map;
-import java.util.regex.Matcher;
import tim.prune.I18nManager;
/**
_extensions[0] = inExt1;
_extensions[1] = inExt2;
_siteNames = new String[2];
- _siteNames[0] = fixSiteName(_baseUrls[0]);
- _siteNames[1] = fixSiteName(_baseUrls[1]);
+ _siteNames[0] = SiteNameUtils.convertUrlToDirectory(_baseUrls[0]);
+ _siteNames[1] = SiteNameUtils.convertUrlToDirectory(_baseUrls[1]);
// Swap layers if second layer given without first
if (_baseUrls[0] == null && _baseUrls[1] != null)
{
{
// Check if the base url has a [1234], if so replace at random
StringBuffer url = new StringBuffer();
- url.append(pickServerUrl(_baseUrls[inLayerNum]));
+ url.append(SiteNameUtils.pickServerUrl(_baseUrls[inLayerNum]));
url.append(inZoom).append('/').append(inX).append('/').append(inY);
url.append('.').append(getFileExtension(inLayerNum));
if (_apiKey != null)
return _maxZoom;
}
- /**
- * If the base url contains something like [1234], then pick a server
- * @param inBaseUrl base url
- * @return modified base url
- */
- protected static final String pickServerUrl(String inBaseUrl)
- {
- if (inBaseUrl == null || inBaseUrl.indexOf('[') < 0) {
- return inBaseUrl;
- }
- // Check for [.*] (once only)
- // Only need to support one, make things a bit easier
- final Matcher matcher = WILD_PATTERN.matcher(inBaseUrl);
- // if not, return base url unchanged
- if (!matcher.matches()) {
- return inBaseUrl;
- }
- // if so, pick one at random and replace in the String
- final String match = matcher.group(2);
- final int numMatches = match.length();
- String server = null;
- if (numMatches > 0)
- {
- int matchNum = (int) Math.floor(Math.random() * numMatches);
- server = "" + match.charAt(matchNum);
- }
- final String result = matcher.group(1) + (server==null?"":server) + matcher.group(3);
- return result;
- }
-
/**
* @return semicolon-separated list of all fields
*/
--- /dev/null
+package tim.prune.gui.map;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper functions for manipulating tile site names
+ */
+public abstract class SiteNameUtils
+{
+ /** Regular expression for catching server wildcards */
+ private static final Pattern WILD_PATTERN = Pattern.compile("^(.*)\\[(.*)\\](.*)$");
+
+
+ /**
+ * If the base url contains something like [1234], then pick a server
+ * @param inBaseUrl base url
+ * @return modified base url
+ */
+ public static String pickServerUrl(String inBaseUrl)
+ {
+ if (inBaseUrl == null || inBaseUrl.indexOf('[') < 0) {
+ return inBaseUrl;
+ }
+ // Check for [.*] (once only)
+ // Only need to support one, make things a bit easier
+ final Matcher matcher = WILD_PATTERN.matcher(inBaseUrl);
+ // if not, return base url unchanged
+ if (!matcher.matches()) {
+ return inBaseUrl;
+ }
+ // if so, pick one at random and replace in the String
+ final String match = matcher.group(2);
+ final int numMatches = match.length();
+ String server = null;
+ if (numMatches > 0)
+ {
+ int matchNum = (int) Math.floor(Math.random() * numMatches);
+ server = "" + match.charAt(matchNum);
+ }
+ final String result = matcher.group(1) + (server==null?"":server) + matcher.group(3);
+ return result;
+ }
+
+
+ /**
+ * Fix the site name by stripping off protocol and www.
+ * This is used to create the file path for disk caching
+ * @param inUrl url to strip
+ * @return stripped url
+ */
+ public static String convertUrlToDirectory(String inUrl)
+ {
+ if (inUrl == null || inUrl.equals("")) {return null;}
+ String url = inUrl.toLowerCase();
+ int idx = url.indexOf("://");
+ if (idx >= 0) {url = url.substring(idx + 3);}
+ if (url.startsWith("www.")) {url = url.substring(4);}
+ // Strip out any "[.*]" as well
+ if (url.indexOf('[') >= 0)
+ {
+ Matcher matcher = WILD_PATTERN.matcher(url);
+ if (matcher.matches())
+ {
+ url = matcher.group(1) + matcher.group(3);
+ if (url.length() > 1 && url.charAt(0) == '.') {
+ url = url.substring(1);
+ }
+ }
+ }
+ return url;
+ }
+}
error.convertnamestotimes.nonames=Geen name kon in tye omgeskakel word
error.lookupsrtm.nonefound=Geen hoogte waardes beskikbaar vir die punte
error.lookupsrtm.nonerequired=Al die punte het klaar hoogtes, so daar is niks meer om te soek
-error.gpsies.uploadnotok=Die gpsies server het die boodskap terug gestuur
-error.gpsies.uploadfailed=Die oplaai het misluk met die volgende fout boodskap
error.showphoto.failed=Foto het nie gelaai nie
error.playaudiofailed=Klankgreep het nie gespeel nie
error.cache.notthere=Die te\u00ebl stoorarea gids kon nie opgespoor word nie
function.deletefieldvalues=Smazat hodnoty pole
function.findwaypoint=Hledat bod
function.pastecoordinates=Zadat sou\u0159adnice
+function.enterpluscode=Zadat plus k\u00f3d
function.charts=Grafy
function.show3d=Trojrozm\u011brn\u011b
function.distances=Vzd\u00e1lenosti
error.convertnamestotimes.nonames=N\u00e1zvy nemohou b\u00fdt p\u0159evedeny na \u010dasov\u00e9 zna\u010dky
error.lookupsrtm.nonefound=Pro tyto body nen\u00ed k dispozici informace o nadmo\u0159sk\u00e9 v\u00fd\u0161ce
error.lookupsrtm.nonerequired=U v\u0161ech bod\u016f u\u017e je informaci o v\u00fd\u0161ce, tak\u017ee nen\u00ed co dohled\u00e1vat
-error.gpsies.uploadnotok=Server gpsies vr\u00e1til hl\u00e1\u0161en\u00ed
-error.gpsies.uploadfailed=Chyba - nepoda\u0159ilo se nahr\u00e1t data.
error.showphoto.failed=Nepoda\u0159ilo se na\u010d\u00edst fotografii
error.playaudiofailed=Nepoda\u0159ilo se p\u0159ehr\u00e1t zvukov\u00fd soubor.
error.cache.notthere=Nepoda\u0159ilo se nal\u00e9zt adres\u00e1\u0159 s cache map.
error.convertnamestotimes.nonames=Es konnten keine Namen umgewandelt werden
error.lookupsrtm.nonefound=Keine H\u00f6hendaten verf\u00fcgbar f\u00fcr diese Punkte
error.lookupsrtm.nonerequired=Alle Punkte haben schon H\u00f6hendaten
-error.gpsies.uploadnotok=Der Gpsies Server hat geantwortet
-error.gpsies.uploadfailed=Das Hochladen ist fehlgeschlagen
error.showphoto.failed=Das Foto konnte nicht geladen werden
error.playaudiofailed=Das Abspielen der Audiodatei ist fehlgeschlagen
error.cache.notthere=Der Ordner wurde nicht gefunden
error.convertnamestotimes.nonames=Kei Namen han k\u00f6nnet verwondlet werde
error.lookupsrtm.nonefound=Kei H\u00f6hendate verf\u00fcegbar f\u00fcr d'P\u00fcnkte
error.lookupsrtm.nonerequired=Alle P\u00fcnkte han die H\u00f6hendate scho. N\u00fc\u00fct z'tue.
-error.gpsies.uploadnotok=Der Gpsies Server h\u00e4t gseit gha
-error.gpsies.uploadfailed=S Uufalade isch fehlgschlage
error.showphoto.failed=S F\u00f6teli han i n\u00f6d k\u00f6nne lade
error.playaudiofailed=S Abschpiele vonem File isch fehlgschlage
error.cache.notthere=D Ordner isch n\u00f6d gfunde worde
error.convertnamestotimes.nonames=No names could be converted into times
error.lookupsrtm.nonefound=No altitude values available for these points
error.lookupsrtm.nonerequired=All points already have altitudes, so there's nothing to lookup
-error.gpsies.uploadnotok=The gpsies server returned the message
-error.gpsies.uploadfailed=The upload failed with the error
error.showphoto.failed=Failed to load photo
error.playaudiofailed=Failed to play audio clip
error.cache.notthere=The tile cache directory was not found
error.convertnamestotimes.nonames=Los nombres no pudieron ser convertidos en tiempos
error.lookupsrtm.nonefound=No se encontraron valores de altitud
error.lookupsrtm.nonerequired=Todos los puntos tienen altitudes, as\u00ed que no hay nada que buscar.
-error.gpsies.uploadnotok=El servidor de gpsies ha devuelto el mensaje
-error.gpsies.uploadfailed=La carga ha fallado con el error
error.showphoto.failed=Fallo al cargar la foto
error.playaudiofailed=Fallo reproduciendo archivo de audio
error.cache.notthere=No se encontr\u00f3 la carpeta del cache de recuadros
function.charts=Kaaviot
function.show3d=3D-n\u00e4kym\u00e4
function.distances=V\u00e4limatkat
+function.viewfulldetails=Kaikki yksityiskohdat
function.estimatetime=Arvioi aika
function.learnestimationparams=Opi aika-arvion parametrit
function.autoplay=Animoi reitti
dialog.diskcache.table.tiles=Tiles
dialog.diskcache.table.megabytes=Megabytes
dialog.diskcache.tileset=Tileset
+dialog.diskcache.tileset.multiple=usea
dialog.diskcache.deleteold=Poista vanhan karttapalat
dialog.diskcache.maximumage=Enimm\u00e4isik\u00e4 (p\u00e4ivi\u00e4)
dialog.diskcache.deleteall=Poista kaikki kartapalat
# Field names
fieldname.latitude=Leveysaste (Lat.)
fieldname.longitude=Pituusaste (Long.)
+fieldname.coordinates=Koordinaatit
fieldname.altitude=Korkeus
fieldname.timestamp=Aikaleima
fieldname.time=Aika
fieldname.speed=Nopeus
fieldname.verticalspeed=Pystysuora nopeus
fieldname.description=Kuvaus
+fieldname.comment=Kommentti
fieldname.mediafilename=Median tiedostonimi
# Measurement units
error.convertnamestotimes.nonames=Kohdepisteiden nimi\u00e4 ei voitu muuntaa ajoiksi
error.lookupsrtm.nonefound=N\u00e4iss\u00e4 pisteiss\u00e4 ei ole korkeustietoja
error.lookupsrtm.nonerequired=Kaikilla pisteill\u00e4 on jo korkeustieto,\njoten ei ole mit\u00e4\u00e4n haettavaa
-error.gpsies.uploadnotok='gpsies'-palvelin palautti viestin:
-error.gpsies.uploadfailed=Tiedoston uppaus ep\u00e4onnistui virheilmoitukseen:
error.showphoto.failed=Kuvan lataus ei onnistunut
error.playaudiofailed=\u00c4\u00e4nileikkeen toisto ei onnistunut
error.cache.notthere=Karttapalojen v\u00e4limuistihakemistoa ei l\u00f6ytynyt
error.convertnamestotimes.nonames=Aucun nom n'a pu \u00eatre converti en horaire
error.lookupsrtm.nonefound=Aucune valeur d'altitude trouv\u00e9e pour les points
error.lookupsrtm.nonerequired=Tous les points ont d\u00e9j\u00e0 une altitude, il n'y a rien \u00e0 r\u00e9cup\u00e9rer
-error.gpsies.uploadnotok=Le serveur de Gpsies a renvoy\u00e9 le message
-error.gpsies.uploadfailed=L'envoi a \u00e9chou\u00e9 avec l'erreur
error.showphoto.failed=Impossible de charger la photo
error.playaudiofailed=\u00c9chec de la lecture du fichier audio
error.cache.notthere=Le dossier du cache n'a pas \u00e9t\u00e9 trouv\u00e9
error.convertnamestotimes.nonames=A nevek nem konvert\u00e1lhat\u00f3k id\u0151adatokk\u00e1
error.lookupsrtm.nonefound=Nem \u00e9rhet\u0151 el magass\u00e1gi \u00e9rt\u00e9k ezekhez a pontokhoz
error.lookupsrtm.nonerequired=Az \u00f6sszes pont m\u00e1r rendelkezik magass\u00e1gadatokkal, \u00edgy nincs mit keresni
-error.gpsies.uploadnotok=A gpsies szerver a k\u00f6vetkez\u0151 \u00fczenetet adta vissza
-error.gpsies.uploadfailed=A felt\u00f6lt\u00e9s nem siker\u00fclt a k\u00f6vetkez\u0151 hib\u00e1val
error.showphoto.failed=F\u00e9nyk\u00e9p bet\u00f6lt\u00e9se sikertelen
error.playaudiofailed=A hangf\u00e1jl lej\u00e1tsz\u00e1sa nem siker\u00fclt
error.cache.notthere=A csempegyors\u00edt\u00f3t\u00e1r k\u00f6nyvt\u00e1ra nem tal\u00e1lhat\u00f3
error.convertnamestotimes.nonames=Nomi non convertibili in orari
error.lookupsrtm.nonefound=Valori di quota non trovati
error.lookupsrtm.nonerequired=Tutti i punti hanno gi\u00e0 una quota, non c'\u00e8 niente da cercare
-error.gpsies.uploadnotok=Il server Gpsies ha riportato il messaggio
-error.gpsies.uploadfailed=Il caricamento \u00e8 fallito con l'errore
error.showphoto.failed=Caricamento foto fallito
error.playaudiofailed=Ripresa audio non riprodotta
error.cache.notthere=Directory del cache di tasselli non trovato
error.convertnamestotimes.nonames=\u3069\u306e\u540d\u524d\u3082\u6642\u9593\u306b\u5909\u63db\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
error.lookupsrtm.nonefound=\u3069\u306e\u6a19\u9ad8\u5024\u3082\u3053\u308c\u3089\u306e\u30dd\u30a4\u30f3\u30c8\u304b\u3089\u53d6\u5f97\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002
error.lookupsrtm.nonerequired=\u5168\u3066\u306e\u30dd\u30a4\u30f3\u30c8\u306f\u6a19\u9ad8\u5024\u3092\u6301\u3063\u3066\u3044\u308b\u305f\u3081\u3001\u691c\u7d22\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002
-error.gpsies.uploadnotok=Gpsies\u30b5\u30fc\u30d0\u30fc\u304c\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u8fd4\u3057\u307e\u3057\u305f\u3002
-error.gpsies.uploadfailed=\u30a8\u30e9\u30fc\u306e\u305f\u3081\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u306b\u5931\u6557\u3057\u307e\u3057\u305f
error.playaudiofailed=\u30aa\u30fc\u30c7\u30a3\u30aa\u30d5\u30a1\u30a4\u30eb\u306e\u518d\u751f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002
error.convertnamestotimes.nonames=\uc774\ub984\uc744 \uc2dc\uac04\uc73c\ub85c \ubcc0\ud658 \ud560 \uc218 \uc5c6\uc5b4\uc694./n(\uacbd\uc720\uc9c0\uc774\ub984\uc744 \ucc3e\uc9c0 \ubabb\ud588\uac70\ub098 \ubcc0\ud658\ud560 \uc218 \uc5c6\ub294 \uacbd\uc6b0\uc5d0\uc694.)
error.lookupsrtm.nonefound=\uc774 \uc9c0\uc810\ub4e4\uc5d0\uc11c \uace0\ub3c4\uac12\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc5b4\uc694./n\uc774 \uc601\uc5ed\uc5d0\uc11c \ud0c0\uc77c\uc774 \uc5c6\uac70\ub098, \uc790\ub8cc\uac00 \uc54c\uc218 \uc5c6\ub294 \uac83\ub4e4\uc744 \ud3ec\ud568\ud558\uace0 \uc788\ub294 \uacbd\uc720.
error.lookupsrtm.nonerequired=\ubaa8\ub4e0 \uc9c0\uc810\uc774 \uace0\ub3c4 \uc815\ubcf4\uac00 \uc788\uc5b4\uc11c, \ucc3e\uc744\uac8c \uc544\ubb34\uac83\ub3c4 \uc5c6\ub124\uc694.
-error.gpsies.uploadnotok=gpsies \uc11c\ubc84\uac00 \uba54\uc138\uc9c0\ub97c \ub2e4\uc74c\uacfc \uac19\uc740 \ub3cc\ub824\uc90d\ub2c8\ub2e4
-error.gpsies.uploadfailed=\ub2e4\uc74c\uacfc \uac19\uc740 \uc774\uc720\ub85c \uc5c5\ub85c\ub4dc\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4
error.playaudiofailed=\uc18c\ub9ac\ud30c\uc77c \uc7ac\uc0dd \uc2e4\ud328
error.convertnamestotimes.nonames=Namen konden niet naar tijden worden geconverteerd
error.lookupsrtm.nonefound=Geen hoogtewaarden beschikbaar voor deze punten
error.lookupsrtm.nonerequired=Alle punten hebben reeds hoogte, er hoeft niets te worden opgezocht.
-error.gpsies.uploadnotok=Gpsies server antwoordde met
-error.gpsies.uploadfailed=De upload is mislukt. Fout
error.showphoto.failed=Foto laden mislukt
error.playaudiofailed=Kon audiobestand niet afspelen
error.cache.notthere=De tegelcache map niet gevonden
function.deletefieldvalues=Usu\u0144 warto\u015bci
function.findwaypoint=Znajd\u017a punkt po\u015bredni
function.pastecoordinates=Wprowad\u017a nowe wsp\u00f3\u0142rz\u0119dne
+function.enterpluscode=Wpisz kod plus
function.charts=Wykres
function.show3d=Poka\u017c model 3D
function.distances=Odleg\u0142o\u015bci
function.mapillary=Szukaj zdj\u0119cia w Mapillary
function.downloadosm=Za\u0142aduj dane obszaru z OSM
function.duplicatepoint=Duplikuj plik
+function.projectpoint=Rzucaj plik
function.setcolours=Ustaw kolory
function.setdisplaysettings=Ustawienia wy\u015bwietlacza
function.setlanguage=Zmie\u0144 j\u0119zyk
dialog.wikipedia.nonefound=Brak wpis\u00f3w w wikipedii
dialog.wikipedia.gallery=Galeria
dialog.osmpois.column.name=Nazwa
+dialog.osmpois.column.type=Typ
+dialog.osmpois.nonefound=Nie znaleziono punkt\u00f3w
dialog.geocaching.nonefound=Nic nie zosta\u0142o znalezione
dialog.correlate.notimestamps=Punkty nie maj\u0105 znacznik\u00f3w czasu, nie mo\u017cna ich powi\u0105za\u0107 ze zdj\u0119ciami.
dialog.correlate.nouncorrelatedphotos=Nie ma nie powi\u0105zanych zdj\u0119\u0107.\nCzy na pewno chcesz kontynuowa\u0107?
dialog.diskcache.deleted=Usuni\u0119to %d plik\u00f3w z kesza
dialog.deletefieldvalues.intro=Wybierz pola do skasowania z wybranego zakresu
dialog.deletefieldvalues.nofields=Brak p\u00f3l do skasowania dla tego zakresu
+dialog.displaysettings.waypointicons=Ikony dla punkt\u00f3w
+dialog.displaysettings.wpicon.plectrum=Plektron
+dialog.displaysettings.wpicon.ring=Pier\u015Bcie\u0144
dialog.displaysettings.linewidth=Wprowad\u017a grubo\u015b\u0107 linii do rysowania \u015bcie\u017cek
dialog.displaysettings.antialias=U\u017Cyj antyaliasingu
dialog.displaysettings.size.small=Ma\u0142e
dialog.displaysettings.size.medium=\u015arednie
dialog.displaysettings.size.large=Du\u017ce
+dialog.displaysettings.windowstyle=Styl okna (wymaga to restartu)
+dialog.displaysettings.windowstyle.default=Domy\u015Blny
dialog.downloadosm.desc=Potwierd\u017a \u015bci\u0105gni\u0119cie danych dla tego obszaru z OSM:
dialog.searchwikipedianames.search=Szukaj
dialog.weather.location=Pozycja
dialog.deletebydate.column.delete=Usu\u0144
dialog.setaltitudetolerance.text.metres=Limit (w metrach) poni\u017cej kt\u00f3rego, ma\u0142e spadki wzniosy b\u0119d\u0105 ignorowane
dialog.setaltitudetolerance.text.feet=Limit (w stopach) poni\u017cej kt\u00f3rego, ma\u0142e spadki wzniosy b\u0119d\u0105 ignorowane
+dialog.settimezone.system=Stref\u0119 czasow\u0105 systemu
dialog.settimezone.selectedzone=Wybrana strefa czasowa
dialog.autoplay.duration=Czas trwania (sek)
dialog.autoplay.usetimestamps=U\u017Cyj znacznik\u00f3w czasowych
dialog.autoplay.rewind=Przewin\u0105\u0107
dialog.autoplay.pause=Pauza
dialog.autoplay.play=Graj
+dialog.projectpoint.bearing=Kierunek (stopnie od p\u00f3\u0142nocy)
# 3d window
dialog.3d.title=GpsPrune widok tr\u00f3jwymiarowy
error.load.unknownxml=Nieznany format xml:
error.load.noxmlinzip=Nie znaleziono pliku xml w pliku zip
error.load.othererror=B\u0142\u0105d czytania pliku:
+error.load.nopointsintext=Nie znaleziono wsp\u00f3\u0142rz\u0119dnych
error.jpegload.dialogtitle=B\u0142\u0105d \u0142adowania zdj\u0119cia
error.jpegload.nofilesfound=Nie znaleziono plik\u00f3w
error.jpegload.nojpegsfound=Nie znaleziono plik\u00f3w jpeg
error.convertnamestotimes.nonames=\u017badne nazwy nie mog\u0142y zosta\u0107 zmienione na czas
error.lookupsrtm.nonefound=Nie znaleziono danych o wysoko\u015bci.
error.lookupsrtm.nonerequired=Wszystkie pola maj\u0105 informacj\u0119 o wysoko\u015bci, nie ma czego szuka\u0107
-error.gpsies.uploadnotok=Serwer Gpsies zwr\u00f3ci\u0142 informacj\u0119
-error.gpsies.uploadfailed=B\u0142\u0105d wysy\u0142ania
error.showphoto.failed=Nie powiod\u0142o si\u0119 za\u0142adowanie zdj\u0119cia
error.playaudiofailed=Nie powiod\u0142o si\u0119 odtwarzanie pliku audio
error.cache.notthere=Nie znaleziono katalogu kesza
function.deletefieldvalues=Remover valores do campo
function.findwaypoint=Encontrar ponto
function.pastecoordinates=Inserir novas coordenadas
+function.enterpluscode=Inserir um plus code
function.charts=Gr\u00e1ficos
function.show3d=Visualizar 3D
function.distances=Dist\u00e2ncias
error.convertnamestotimes.nonames=Nenhum nome pode ser convertido para tempo
error.lookupsrtm.nonefound=Nenhum valor de altitude encontrado
error.lookupsrtm.nonerequired=Todos os pontos j\u00e1 possuem altitude, assim n\u00e3o h\u00e1 nada a procurar
-error.gpsies.uploadnotok=O servidor Gpsies retornou a mensagem
-error.gpsies.uploadfailed=O envio falhou com o erro
error.showphoto.failed=Falha ao carregar foto
error.playaudiofailed=Falha ao reproduzir arquivo de \u00e1udio
error.cache.notthere=A paste de cache de fundos n\u00e3o foi encontrada
error.convertnamestotimes.nonames=Niciun nume de waypoint nu a fost g\u0103sit sau nu au putut fi convertite
error.lookupsrtm.nonefound=Pentru aceste puncte nu exist\u0103 valori de altitudine
error.lookupsrtm.nonerequired=Toate punctele au deja altitudine, nu e nimic de calculat
-error.gpsies.uploadnotok=Server-ul gpsies a \u00eentors mesajul
-error.gpsies.uploadfailed=Upload-ul a e\u0219uat cu eroarea
error.showphoto.failed=\u00cenc\u0103rcarea foto a e\u0219uat
error.playaudiofailed=\u00cencercarea de a reda clipul audio a e\u0219uat
error.cache.notthere=Directorul tile cache nu a fost g\u0103sit
error.convertnamestotimes.nonames=\u041d\u0435\u0442 \u0438\u043c\u0435\u043d, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438 \u0432\u043e \u0432\u0440\u0435\u043c\u044f
error.lookupsrtm.nonefound=\u041d\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u043e\u0442\u043c\u0435\u0442\u043e\u043a \u0432\u044b\u0441\u043e\u0442 \u0434\u043b\u044f \u044d\u0442\u0438\u0445 \u0442\u043e\u0447\u0435\u043a.
error.lookupsrtm.nonerequired=\u0412\u0441\u0435 \u0442\u043e\u0447\u043a\u0438 \u0443\u0436\u0435 \u0438\u043c\u0435\u044e\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0432\u044b\u0441\u043e\u0442, \u043d\u0435\u0447\u0435\u0433\u043e \u043f\u0440\u043e\u0441\u043c\u0430\u0442\u0440\u0438\u0432\u0430\u0442\u044c
-error.gpsies.uploadnotok=Gpsies \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0435\u0440\u043d\u0443\u043b \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435
-error.gpsies.uploadfailed=\u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430 \u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0430 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439
error.showphoto.failed=\u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0444\u043e\u0442\u043e
error.playaudiofailed=\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u0437\u0432\u0443\u043a\u043e\u0437\u0430\u043f\u0438\u0441\u0438
error.cache.notthere=\u041f\u0430\u043f\u043a\u0430 \u043a\u044d\u0448\u0430 \u0441 \u0442\u0430\u0439\u043b\u0430\u043c\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430
dialog.load.table.field=F\u00e4lt
dialog.load.table.datatype=Datatyp
dialog.load.table.description=Beskrivning
+dialog.delimiter.label=Avgr\u00e4nsare
dialog.delimiter.comma=Komma ,
dialog.delimiter.tab=Tabb
dialog.delimiter.space=Mellanslag
details.range.avespeed=Medelhastighet
details.range.maxspeed=Maxhastighet
details.range.numsegments=Antal segment
+details.range.pace=Tempo
details.range.gradient=Gradient
details.lists.waypoints=Waypoints
details.lists.photos=Foton
fieldname.waypointname=Namn
fieldname.waypointtype=Typ
fieldname.newsegment=Segment
+fieldname.custom=Anpassad
fieldname.prefix=F\u00e4lt
fieldname.distance=Distans
+fieldname.duration=Varaktighet
fieldname.speed=Hastighet
fieldname.verticalspeed=Vertikal hastighet
fieldname.description=Beskrivning
error.audioload.nofilesfound=Inga ljud-klipp hittades
error.gpsload.unknown=Ok\u00e4nt fel
error.undofailed.title=\u00c5ngra misslyckades
+error.function.notavailable.title=Funktionen \u00e4r inte tillg\u00e4nglig
+error.readme.notfound=Ingen L\u00e4sMig-fil hittades
error.convertnamestotimes.nonames=\u540d\u79f0\u65e0\u6cd5\u8f6c\u6362\u6210\u65f6\u95f4
error.lookupsrtm.nonefound=\u65e0\u9ad8\u5ea6\u4fe1\u606f
error.lookupsrtm.nonerequired=\u6240\u6709\u70b9\u5747\u542b\u9ad8\u5ea6\u4fe1\u606f
-error.gpsies.uploadnotok=gpsies\u670d\u52a1\u5668\u8fd4\u56de\u4fe1\u606f\uff1a
-error.gpsies.uploadfailed=\u4e0a\u4f20\u5931\u8d25
error.showphoto.failed=\u52a0\u8f7d\u7167\u7247\u5931\u8d25
error.playaudiofailed=\u65e0\u6cd5\u64ad\u653e\u58f0\u97f3\u6587\u4ef6
error.cache.notthere=\u672a\u627e\u5230\u533a\u57df\u6570\u636e\u7f13\u5b58\u6587\u4ef6\u5939
"GeocachingDB for Palm/OS", "gcdb", null,
"Geogrid-Viewer ascii overlay file", "ggv_ovl", ".ovl",
"Geogrid-Viewer tracklogs", "ggv_log", ".log",
+ "GeoJSON", "geojson", ".json",
"GEOnet Names Server (GNS)", "geonet", null,
"GlobalSat DG-100/BT-335", "dg-100", null,
"GlobalSat DG-200", "dg-200", null,
import tim.prune.App;
import tim.prune.config.Config;
import tim.prune.data.Photo;
+import tim.prune.load.json.JsonFileLoader;
import tim.prune.load.xml.GzipFileLoader;
import tim.prune.load.xml.XmlFileLoader;
import tim.prune.load.xml.ZipFileLoader;
_app.informPhotosLoaded(photoSet);
_app.informNoDataLoaded(); // To trigger load of next file if any
}
+ else if (fileExtension.equals("json"))
+ {
+ new JsonFileLoader(_app).openFile(inFile);
+ }
else
{
// Use text loader for everything else
--- /dev/null
+package tim.prune.load.json;
+
+import java.util.ArrayList;
+
+/**
+ * Structure to hold the contents of a Json block during parsing.
+ * This will be held within the current [] or {} block
+ */
+public class JsonBlock
+{
+ private String _latitude = null;
+ private String _longitude = null;
+ private String _altitude = null;
+
+ private Expectation _nextField = Expectation.NONE;
+ private BlockType _type = BlockType.NONE;
+ private boolean _hasNonNumbers = false;
+ private ArrayList<String> _pointCoords = null;
+ private ArrayList<ArrayList<String>> _coordList = null;
+
+
+ private enum BlockType {
+ NONE, FIELDS, POINTCOORDS, ISPOINTLIST, HASPOINTLIST
+ }
+ private enum Expectation {
+ NONE, LATITUDE, LONGITUDE, ALTITUDE, COORDS
+ }
+
+ /** Internal method to remove quotes and NaNs from altitude strings */
+ private String modifyAltitudeString(String inAlt)
+ {
+ if (inAlt == null || inAlt.equals("") || inAlt.equals("\"\"")) {
+ return null;
+ }
+ String result = inAlt;
+ if (inAlt.length() > 2 && inAlt.startsWith("\"") && inAlt.endsWith("\""))
+ {
+ result = inAlt.substring(1, inAlt.length()-1);
+ }
+ if (result.equals("NaN")) {return null;}
+ return result;
+ }
+
+ /**
+ * Receive a token to this block
+ * @param inToken token from Json source
+ */
+ public void addToken(String inToken)
+ {
+ if (inToken == null || inToken.isEmpty()) {return;}
+ if (!_hasNonNumbers && !looksLikeNumber(inToken)) {
+ _hasNonNumbers = true;
+ }
+ if (inToken.equals("\"latitude\"")) {
+ _nextField = Expectation.LATITUDE;
+ }
+ else if (inToken.equals("\"longitude\"")) {
+ _nextField = Expectation.LONGITUDE;
+ }
+ else if (inToken.equals("\"altitude\"")) {
+ _nextField = Expectation.ALTITUDE;
+ }
+ else if (inToken.equals("\"coordinates\"")) {
+ _nextField = Expectation.COORDS;
+ }
+ else
+ {
+ switch (_nextField)
+ {
+ case LATITUDE:
+ _latitude = inToken;
+ _type = BlockType.FIELDS;
+ break;
+ case LONGITUDE:
+ _longitude = inToken;
+ _type = BlockType.FIELDS;
+ break;
+ case ALTITUDE:
+ _altitude = modifyAltitudeString(inToken);
+ _type = BlockType.FIELDS;
+ break;
+ default:
+ if (!_hasNonNumbers && looksLikeNumber(inToken))
+ {
+ if (_pointCoords == null) {
+ _pointCoords = new ArrayList<String>();
+ }
+ _pointCoords.add(inToken);
+ _type = BlockType.POINTCOORDS;
+ }
+ break;
+ }
+ _nextField = Expectation.NONE;
+ }
+ }
+
+ /** @return true if String can be parsed as a double */
+ private static boolean looksLikeNumber(String inToken)
+ {
+ double value = Double.NaN;
+ try {
+ value = Double.parseDouble(inToken);
+ }
+ catch (NumberFormatException nfe) {}
+ return !Double.isNaN(value);
+ }
+
+ /** @return true if named fields are valid */
+ public boolean areFieldsValid() {
+ return _type == BlockType.FIELDS && _latitude != null && _longitude != null;
+ }
+
+ /** @return true if list of 2 or 3 doubles for a single point is valid */
+ public boolean areSingleCoordsValid()
+ {
+ return !_hasNonNumbers && _pointCoords != null
+ && _pointCoords.size() >= 2 && _pointCoords.size() <= 3;
+ }
+
+ /**
+ * @param inNewSegment new segment flag
+ * @return created point
+ */
+ public JsonPoint createSinglePoint(boolean inNewSegment)
+ {
+ return new JsonPoint(_latitude, _longitude, _altitude, inNewSegment);
+ }
+
+ /**
+ * Child block has finished processing a single set of point coordinates
+ * @param inChild child block
+ */
+ public void addSingleCoordsFrom(JsonBlock inChild)
+ {
+ if (_type == BlockType.NONE && _nextField == Expectation.COORDS
+ && inChild._type == BlockType.POINTCOORDS)
+ {
+ // Named coordinates field
+ _type = BlockType.FIELDS;
+ _longitude = inChild.getSinglePointCoords(0);
+ _latitude = inChild.getSinglePointCoords(1);
+ _altitude = inChild.getSinglePointCoords(2);
+ }
+ else if ((_type == BlockType.NONE || _type == BlockType.ISPOINTLIST)
+ && !_hasNonNumbers && _nextField == Expectation.NONE
+ && inChild._type == BlockType.POINTCOORDS)
+ {
+ // add coordinates to list
+ _type = BlockType.ISPOINTLIST;
+ if (_coordList == null) {
+ _coordList = new ArrayList<ArrayList<String>>();
+ }
+ _coordList.add(inChild._pointCoords);
+ }
+ }
+
+ /** @return point coordinate for given index, or null if not present */
+ private String getSinglePointCoords(int inIndex)
+ {
+ if (_pointCoords == null || inIndex < 0 || inIndex >= _pointCoords.size()) {
+ return null;
+ }
+ return _pointCoords.get(inIndex);
+ }
+
+ /** @return true if list of point coords is valid */
+ public boolean isCoordListValid()
+ {
+ return !_hasNonNumbers && _type == BlockType.ISPOINTLIST
+ && _coordList != null;
+ }
+
+ /** @return true if this block has a valid list of point coords */
+ public boolean hasValidCoordList()
+ {
+ return _type == BlockType.HASPOINTLIST && _coordList != null;
+ }
+
+ /**
+ * Child block has finished processing a list of point coordinates
+ * @param inChild child block
+ */
+ public void addCoordListFrom(JsonBlock inChild)
+ {
+ if (_type == BlockType.NONE && _nextField == Expectation.COORDS
+ && inChild._type == BlockType.ISPOINTLIST)
+ {
+ _coordList = inChild._coordList;
+ _type = BlockType.HASPOINTLIST;
+ }
+ else if ((_type == BlockType.NONE || _type == BlockType.ISPOINTLIST)
+ && !_hasNonNumbers && inChild._type == BlockType.ISPOINTLIST)
+ {
+ if (_coordList == null) {
+ _coordList = new ArrayList<ArrayList<String>>();
+ }
+ _coordList.addAll(inChild._coordList);
+ _type = BlockType.ISPOINTLIST;
+ }
+ }
+
+ /**
+ * @return number of points in the list, if this block has a list
+ */
+ public int getNumPoints()
+ {
+ if (hasValidCoordList()) {
+ return _coordList.size();
+ }
+ return 0;
+ }
+
+ /**
+ * @param inIndex point index within list
+ * @return created point for specified index
+ */
+ public JsonPoint createPointFromList(int inIndex)
+ {
+ if (inIndex < 0 || inIndex >= getNumPoints()) {return null;}
+ ArrayList<String> pointCoords = _coordList.get(inIndex);
+ if (pointCoords.size() < 2 || pointCoords.size() > 3) {return null;}
+ final String longitude = pointCoords.get(0);
+ final String latitude = pointCoords.get(1);
+ final String altitude = ((pointCoords.size() == 3) ? pointCoords.get(3) : null);
+ return new JsonPoint(latitude, longitude, altitude, inIndex == 0);
+ }
+}
--- /dev/null
+package tim.prune.load.json;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Stack;
+
+import tim.prune.App;
+import tim.prune.data.Field;
+import tim.prune.data.SourceInfo;
+
+
+/**
+ * Class to handle the loading of GeoJSON files
+ */
+public class JsonFileLoader
+{
+ /** App for callback of file loading */
+ private App _app = null;
+ /** Stack of blocks */
+ private Stack<JsonBlock> _jsonBlocks = null;
+ /** List of points extracted */
+ private ArrayList<JsonPoint> _jsonPoints = null;
+ private boolean _newSegment = true;
+
+
+ /**
+ * Constructor
+ * @param inApp App object
+ */
+ public JsonFileLoader(App inApp)
+ {
+ _app = inApp;
+ _jsonBlocks = new Stack<JsonBlock>();
+ _jsonPoints = new ArrayList<JsonPoint>();
+ }
+
+ /**
+ * Open the selected file
+ * @param inFile File to open
+ */
+ public void openFile(File inFile)
+ {
+ BufferedReader reader = null;
+ try
+ {
+ reader = new BufferedReader(new FileReader(inFile));
+ String currLine = reader.readLine();
+ while (currLine != null)
+ {
+ processTokensInLine(currLine);
+ // Read next line, if any
+ currLine = reader.readLine();
+ }
+ }
+ catch (IOException ioe) {
+ _app.showErrorMessage("error.load.dialogtitle", "error.load.noread");
+ }
+ finally
+ {
+ // close file ignoring errors
+ try {
+ if (reader != null) reader.close();
+ }
+ catch (Exception e) {}
+ }
+ if (_jsonPoints.size() > 0)
+ {
+ Field[] fields = {Field.LATITUDE, Field.LONGITUDE, Field.ALTITUDE,
+ Field.NEW_SEGMENT};
+ _app.informDataLoaded(fields, makeDataArray(),
+ null, new SourceInfo(inFile, SourceInfo.FILE_TYPE.JSON), null);
+ }
+ // TODO: Show message if nothing was found?
+ }
+
+ /** Split the given line from the json into tokens
+ * and process them one by one */
+ private void processTokensInLine(String inLine)
+ {
+ if (inLine == null) {return;}
+ String line = inLine.strip();
+ StringBuilder currToken = new StringBuilder();
+ boolean insideQuotes = false;
+ boolean previousSlash = false;
+ for (char x : line.toCharArray())
+ {
+ if (insideQuotes || x=='"') {
+ currToken.append(x);
+ }
+ else
+ {
+ if (" :,".indexOf(x) >= 0) {
+ processToken(currToken.toString());
+ currToken.setLength(0);
+ }
+ else if ("[{".indexOf(x) >= 0) {
+ // start of a new block
+ _jsonBlocks.add(new JsonBlock());
+ }
+ else if ("]}".indexOf(x) >= 0)
+ {
+ processToken(currToken.toString());
+ currToken.setLength(0);
+ // end of the current block
+ processBlock(_jsonBlocks.pop());
+ }
+ else {
+ currToken.append(x);
+ }
+ }
+ if (x == '"' && !previousSlash) {insideQuotes = !insideQuotes;}
+ previousSlash = (x == '\\') && !previousSlash;
+ }
+ processToken(currToken.toString());
+ }
+
+ private void processToken(String inToken)
+ {
+ if (inToken == null || inToken.isBlank()) {return;}
+ if (inToken.equals("\"coordinates\"")) {
+ _newSegment = true;
+ }
+ _jsonBlocks.peek().addToken(inToken);
+ }
+
+ /** Process the end of the given block */
+ private void processBlock(JsonBlock inBlock)
+ {
+ if (inBlock.areFieldsValid())
+ {
+ _jsonPoints.add(inBlock.createSinglePoint(_newSegment));
+ _newSegment = false;
+ }
+ else if (inBlock.areSingleCoordsValid())
+ {
+ // block contains a single point - pass to parent list
+ _jsonBlocks.peek().addSingleCoordsFrom(inBlock);
+ }
+ else if (inBlock.isCoordListValid())
+ {
+ // block contains a list of point coords
+ _jsonBlocks.peek().addCoordListFrom(inBlock);
+ }
+ else if (inBlock.hasValidCoordList())
+ {
+ for (int i=0; i<inBlock.getNumPoints(); i++)
+ {
+ _jsonPoints.add(inBlock.createPointFromList(i));
+ }
+ _newSegment = true;
+ }
+ }
+
+ /**
+ * Make an object array from the data list
+ * @return object array for loading
+ */
+ private Object[][] makeDataArray()
+ {
+ Object[][] result = new Object[_jsonPoints.size()][];
+ for (int i=0; i<_jsonPoints.size(); i++) {
+ JsonPoint point = _jsonPoints.get(i);
+ result[i] = new String[] {point._latitude, point._longitude, point._altitude, point._newSegment?"1":"0"};
+ }
+ return result;
+ }
+}
--- /dev/null
+package tim.prune.load.json;
+
+/**
+ * Structure for holding a single point extracted from the Json
+ */
+public class JsonPoint
+{
+ public String _latitude = null, _longitude = null, _altitude = null;
+ public boolean _newSegment = false;
+
+ /**
+ * Constructor
+ * @param inLat latitude string
+ * @param inLon longitude string
+ * @param inAlt altitude string
+ * @param inNewSegment true if this point starts a new segment
+ */
+ public JsonPoint(String inLat, String inLon, String inAlt, boolean inNewSegment)
+ {
+ _latitude = inLat;
+ _longitude = inLon;
+ _altitude = inAlt;
+ _newSegment = inNewSegment;
+ }
+}
-GpsPrune version 20.3
+GpsPrune version 20.4
=====================
GpsPrune is an application for viewing, editing and managing coordinate data from GPS systems,
=======
To run GpsPrune from the jar file, simply call it from a command prompt or shell:
- java -jar gpsprune_20.3.jar
+ java -jar gpsprune_20.4.jar
If the jar file is saved in a different directory, you will need to include the path.
Depending on your system settings, you may be able to click or double-click on the jar file
or other link can of course be made should you wish.
To specify a language other than the default, use an additional parameter, eg:
- java -jar gpsprune_20.3.jar --lang=DE
+ java -jar gpsprune_20.4.jar --lang=DE
+New with version 20.4
+=====================
+The following fixes and additions were made since version 20.3:
+ - Loading of GeoJSON files, either directly or via GPSBabel
+ - Fix for calculations when some timestamps are missing (fperrin)
+ - Make 3d rotations more intuitive, avoid rotating upside-down (Issue #34)
+
New with version 20.3
=====================
The following fixes and additions were made since version 20.2:
import tim.prune.save.GroutedImage;
import tim.prune.save.MapGrouter;
-import com.sun.j3d.utils.behaviors.vp.OrbitBehavior;
import com.sun.j3d.utils.geometry.Box;
import com.sun.j3d.utils.geometry.Cylinder;
import com.sun.j3d.utils.geometry.GeometryInfo;
private JFrame _parentFrame = null;
private JFrame _frame = null;
private ThreeDModel _model = null;
- private OrbitBehavior _orbit = null;
+ private UprightOrbiter _orbit = null;
private double _altFactor = -1.0;
private ImageDefinition _imageDefinition = null;
private GroutedImage _baseImage = null;
u.getViewingPlatform().setNominalViewingTransform();
// Add behaviour to rotate using mouse
- _orbit = new OrbitBehavior(canvas, OrbitBehavior.REVERSE_ALL | OrbitBehavior.STOP_ZOOM);
+ _orbit = new UprightOrbiter(canvas, INITIAL_X_ROTATION);
BoundingSphere bounds = new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0);
_orbit.setSchedulingBounds(bounds);
u.getViewingPlatform().setViewPlatformBehavior(_orbit);
// Store this back in the cache, maybe we'll need it again
TerrainCache.storeTerrainTrack(terrainTrack, _dataStatus, _terrainDefinition);
}
- // else System.out.println("Yay - reusing the cached track!");
// Give the terrain definition to the _model as well
_model.setTerrain(terrainTrack);
--- /dev/null
+package tim.prune.threedee;
+
+import java.awt.event.InputEvent;
+
+/*
+ * Copyright (c) 2007 Sun Microsystems, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistribution of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistribution in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * This software is provided "AS IS," without a warranty of any
+ * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND
+ * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
+ * EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL
+ * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF
+ * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
+ * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR
+ * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
+ * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
+ * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
+ * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGES.
+ *
+ * You acknowledge that this software is not designed, licensed or
+ * intended for use in the design, construction, operation or
+ * maintenance of any nuclear facility.
+ *
+ * Copyright (c) 2021 ActivityWorkshop simplifications and renamings,
+ * and restriction to upright orientations.
+ */
+
+import java.awt.event.MouseEvent;
+import java.awt.AWTEvent;
+
+import javax.media.j3d.Transform3D;
+import javax.media.j3d.Canvas3D;
+
+import javax.vecmath.Vector3d;
+import javax.vecmath.Point3d;
+import javax.vecmath.Matrix3d;
+
+import com.sun.j3d.utils.behaviors.vp.ViewPlatformAWTBehavior;
+import com.sun.j3d.utils.universe.ViewingPlatform;
+
+
+/**
+ * Moves the View around a point of interest when the mouse is dragged with
+ * a mouse button pressed. Includes rotation, zoom, and translation
+ * actions. Zooming can also be obtained by using mouse wheel.
+ * <p>
+ * The rotate action rotates the ViewPlatform around the point of interest
+ * when the mouse is moved with the main mouse button pressed. The
+ * rotation is in the direction of the mouse movement, with a default
+ * rotation of 0.01 radians for each pixel of mouse movement.
+ * <p>
+ * The zoom action moves the ViewPlatform closer to or further from the
+ * point of interest when the mouse is moved with the middle mouse button
+ * pressed (or Alt-main mouse button on systems without a middle mouse button).
+ * The default zoom action is to translate the ViewPlatform 0.01 units for each
+ * pixel of mouse movement. Moving the mouse up moves the ViewPlatform closer,
+ * moving the mouse down moves the ViewPlatform further away.
+ * <p>
+ * The translate action translates the ViewPlatform when the mouse is moved
+ * with the right mouse button pressed. The translation is in the direction
+ * of the mouse movement, with a default translation of 0.01 units for each
+ * pixel of mouse movement.
+ * <p>
+ * The actions can be reversed using the <code>REVERSE_</code><i>ACTION</i>
+ * constructor flags. The default action moves the ViewPlatform around the
+ * objects in the scene. The <code>REVERSE_</code><i>ACTION</i> flags can
+ * make the objects in the scene appear to be moving in the direction
+ * of the mouse movement.
+ */
+public class UprightOrbiter extends ViewPlatformAWTBehavior
+{
+ private Transform3D _longitudeTransform = new Transform3D();
+ private Transform3D _latitudeTransform = new Transform3D();
+ private Transform3D _rotateTransform = new Transform3D();
+
+ // needed for integrateTransforms but don't want to new every time
+ private Transform3D _temp1 = new Transform3D();
+ private Transform3D _temp2 = new Transform3D();
+ private Transform3D _translation = new Transform3D();
+ private Vector3d _transVector = new Vector3d();
+ private Vector3d _distanceVector = new Vector3d();
+ private Vector3d _centerVector = new Vector3d();
+ private Vector3d _invertCenterVector = new Vector3d();
+
+ private double _deltaYaw = 0.0, _deltaPitch = 0.0;
+ private double _startDistanceFromCenter = 20.0;
+ private double _distanceFromCenter = 20.0;
+ private Point3d _rotationCenter = new Point3d();
+ private Matrix3d _rotMatrix = new Matrix3d();
+
+ private int _mouseX = 0, _mouseY = 0;
+
+ private double _xtrans = 0.0, _ytrans = 0.0, _ztrans = 0.0;
+
+ private static final double MIN_RADIUS = 0.0;
+
+ // the factor to be applied to wheel zooming so that it does not
+ // look much different with mouse movement zooming.
+ private static final float wheelZoomFactor = 50.0f;
+
+ private static final double NOMINAL_ZOOM_FACTOR = .01;
+ private static final double NOMINAL_ROT_FACTOR = .008;
+ private static final double NOMINAL_TRANS_FACTOR = .003;
+
+ private double _pitchAngle = 0.0;
+
+
+ /**
+ * Creates a new OrbitBehaviour
+ * @param inCanvas The Canvas3D to add the behaviour to
+ * @param inInitialPitch pitch angle in degrees
+ */
+ public UprightOrbiter(Canvas3D inCanvas, double inInitialPitch)
+ {
+ super(inCanvas, MOUSE_LISTENER | MOUSE_MOTION_LISTENER | MOUSE_WHEEL_LISTENER );
+ _pitchAngle = Math.toRadians(inInitialPitch);
+ }
+
+ protected synchronized void processAWTEvents( final AWTEvent[] events )
+ {
+ motion = false;
+ for(int i=0; i<events.length; i++)
+ if (events[i] instanceof MouseEvent)
+ processMouseEvent( (MouseEvent)events[i] );
+ }
+
+ protected void processMouseEvent( final MouseEvent evt )
+ {
+ if (evt.getID() == MouseEvent.MOUSE_PRESSED) {
+ _mouseX = evt.getX();
+ _mouseY = evt.getY();
+ motion = true;
+ }
+ else if (evt.getID() == MouseEvent.MOUSE_DRAGGED)
+ {
+ int xchange = evt.getX() - _mouseX;
+ int ychange = evt.getY() - _mouseY;
+ // rotate
+ if (isRotateEvent(evt))
+ {
+ _deltaYaw -= xchange * NOMINAL_ROT_FACTOR;
+ _deltaPitch -= ychange * NOMINAL_ROT_FACTOR;
+ }
+ // translate
+ else if (isTranslateEvent(evt))
+ {
+ _xtrans -= xchange * NOMINAL_TRANS_FACTOR;
+ _ytrans += ychange * NOMINAL_TRANS_FACTOR;
+ }
+ // zoom
+ else if (isZoomEvent(evt)) {
+ doZoomOperations( ychange );
+ }
+ _mouseX = evt.getX();
+ _mouseY = evt.getY();
+ motion = true;
+ }
+ else if (evt.getID() == MouseEvent.MOUSE_WHEEL )
+ {
+ if (isZoomEvent(evt))
+ {
+ // if zooming is done through mouse wheel, the number of wheel increments
+ // is multiplied by the wheelZoomFactor, to make zoom speed look natural
+ if ( evt instanceof java.awt.event.MouseWheelEvent)
+ {
+ int zoom = ((int)(((java.awt.event.MouseWheelEvent)evt).getWheelRotation()
+ * wheelZoomFactor));
+ doZoomOperations( zoom );
+ motion = true;
+ }
+ }
+ }
+ }
+
+ /*
+ * zoom but stop at MIN_RADIUS
+ */
+ private void doZoomOperations( int ychange )
+ {
+ if ((_distanceFromCenter - ychange * NOMINAL_ZOOM_FACTOR) > MIN_RADIUS) {
+ _distanceFromCenter -= ychange * NOMINAL_ZOOM_FACTOR;
+ }
+ else {
+ _distanceFromCenter = MIN_RADIUS;
+ }
+ }
+
+ /**
+ * Sets the ViewingPlatform for this behaviour. This method is
+ * called by the ViewingPlatform.
+ * If a sub-calls overrides this method, it must call
+ * super.setViewingPlatform(vp).
+ * NOTE: Applications should <i>not</i> call this method.
+ */
+ @Override
+ public void setViewingPlatform(ViewingPlatform vp)
+ {
+ super.setViewingPlatform( vp );
+
+ if (vp != null) {
+ resetView();
+ integrateTransforms();
+ }
+ }
+
+ /**
+ * Reset the orientation and distance of this behaviour to the current
+ * values in the ViewPlatform Transform Group
+ */
+ private void resetView()
+ {
+ Vector3d centerToView = new Vector3d();
+
+ targetTG.getTransform( targetTransform );
+
+ targetTransform.get( _rotMatrix, _transVector );
+ centerToView.sub( _transVector, _rotationCenter );
+ _distanceFromCenter = centerToView.length();
+ _startDistanceFromCenter = _distanceFromCenter;
+
+ targetTransform.get( _rotMatrix );
+ _rotateTransform.set( _rotMatrix );
+
+ // compute the initial x/y/z offset
+ _temp1.set(centerToView);
+ _rotateTransform.invert();
+ _rotateTransform.mul(_temp1);
+ _rotateTransform.get(centerToView);
+ _xtrans = centerToView.x;
+ _ytrans = centerToView.y;
+ _ztrans = centerToView.z;
+
+ // reset rotMatrix
+ _rotateTransform.set( _rotMatrix );
+ }
+
+ protected synchronized void integrateTransforms()
+ {
+ // Check if the transform has been changed by another behaviour
+ Transform3D currentXfm = new Transform3D();
+ targetTG.getTransform(currentXfm);
+ if (! targetTransform.equals(currentXfm))
+ resetView();
+
+ // Three-step rotation process, firstly undo the pitch and apply the delta yaw
+ _latitudeTransform.rotX(_pitchAngle);
+ _rotateTransform.mul(_rotateTransform, _latitudeTransform);
+ _longitudeTransform.rotY( _deltaYaw );
+ _rotateTransform.mul(_rotateTransform, _longitudeTransform);
+ // Now update pitch angle according to delta and apply
+ _pitchAngle = Math.min(Math.max(0.0, _pitchAngle - _deltaPitch), Math.PI/2.0);
+ _latitudeTransform.rotX(-_pitchAngle);
+ _rotateTransform.mul(_rotateTransform, _latitudeTransform);
+
+ _distanceVector.z = _distanceFromCenter - _startDistanceFromCenter;
+
+ _temp1.set(_distanceVector);
+ _temp1.mul(_rotateTransform, _temp1);
+
+ // want to look at rotationCenter
+ _transVector.x = _rotationCenter.x + _xtrans;
+ _transVector.y = _rotationCenter.y + _ytrans;
+ _transVector.z = _rotationCenter.z + _ztrans;
+
+ _translation.set(_transVector);
+ targetTransform.mul(_temp1, _translation);
+
+ // handle rotationCenter
+ _temp1.set(_centerVector);
+ _temp1.mul(targetTransform);
+
+ _invertCenterVector.x = -_centerVector.x;
+ _invertCenterVector.y = -_centerVector.y;
+ _invertCenterVector.z = -_centerVector.z;
+
+ _temp2.set(_invertCenterVector);
+ targetTransform.mul(_temp1, _temp2);
+
+ Vector3d finalTranslation = new Vector3d();
+ targetTransform.get(finalTranslation);
+
+ targetTG.setTransform(targetTransform);
+
+ // reset yaw and pitch deltas
+ _deltaYaw = 0.0;
+ _deltaPitch = 0.0;
+ }
+
+ private boolean isRotateEvent(MouseEvent evt)
+ {
+ final boolean isRightDrag = (evt.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) > 0;
+ return !evt.isAltDown() && !isRightDrag;
+ }
+
+ private boolean isZoomEvent(MouseEvent evt)
+ {
+ if (evt instanceof java.awt.event.MouseWheelEvent) {
+ return true;
+ }
+ return evt.isAltDown() && !evt.isMetaDown();
+ }
+
+ private boolean isTranslateEvent(MouseEvent evt)
+ {
+ final boolean isRightDrag = (evt.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) > 0;
+ return !evt.isAltDown() && isRightDrag;
+ }
+}
--- /dev/null
+package tim.prune.data;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for calculation of moving time of a range
+ * based on different timestamp availability
+ * @author fperrin
+ */
+class RangeStatsTest
+{
+ @Test
+ void movingTime()
+ {
+ Track track = new Track();
+ DataPoint[] points = {
+ new DataPoint(
+ new String[] {
+ "01-Jan-2020 00:00:00",
+ },
+ new FieldList(new Field[] {
+ Field.TIMESTAMP,
+ }),
+ null),
+ new DataPoint(
+ new String[] {
+ "01-Jan-2020 00:00:05",
+ },
+ new FieldList(new Field[] {
+ Field.TIMESTAMP,
+ }),
+ null),
+ new DataPoint(
+ new String[] {
+ "01-Jan-2020 00:00:07",
+ },
+ new FieldList(new Field[] {
+ Field.TIMESTAMP,
+ }),
+ null),
+ };
+ track.appendPoints(points);
+
+ RangeStats range = new RangeStats(track, 0, track.getNumPoints() - 1);
+ assertEquals(7, range.getMovingDurationInSeconds());
+ assertEquals(7, range.getTotalDurationInSeconds());
+ assertFalse(range.getTimestampsIncomplete());
+ assertFalse(range.getTimestampsOutOfSequence());
+ }
+
+ @Test
+ void movingTimeWithGap()
+ {
+ Track track = new Track();
+ DataPoint[] points = {
+ new DataPoint(
+ new String[] {
+ "01-Jan-2020 00:00:00",
+ },
+ new FieldList(new Field[] {
+ Field.TIMESTAMP,
+ }),
+ null),
+ new DataPoint(
+ new String[] {},
+ new FieldList(new Field[] {}),
+ null),
+ new DataPoint(
+ new String[] {
+ "01-Jan-2020 00:00:05",
+ },
+ new FieldList(new Field[] {
+ Field.TIMESTAMP,
+ }),
+ null),
+ new DataPoint(
+ new String[] {
+ "01-Jan-2020 00:00:07",
+ },
+ new FieldList(new Field[] {
+ Field.TIMESTAMP,
+ }),
+ null),
+ };
+ track.appendPoints(points);
+
+ RangeStats range = new RangeStats(track, 0, track.getNumPoints() - 1);
+ assertEquals(7, range.getMovingDurationInSeconds());
+ assertEquals(7, range.getTotalDurationInSeconds());
+ assertTrue(range.getTimestampsIncomplete());
+ assertFalse(range.getTimestampsOutOfSequence());
+ }
+
+ @Test
+ void movingTimeSeveralSegments()
+ {
+ Track track = new Track();
+ DataPoint[] points = {
+ new DataPoint(
+ new String[] {
+ "01-Jan-2020 00:01:00",
+ },
+ new FieldList(new Field[] {
+ Field.TIMESTAMP,
+ }),
+ null),
+ new DataPoint(
+ new String[] {},
+ new FieldList(new Field[] {}),
+ null),
+ new DataPoint(
+ new String[] {
+ "01-Jan-2020 00:01:05",
+ },
+ new FieldList(new Field[] {
+ Field.TIMESTAMP,
+ }),
+ null),
+ new DataPoint(
+ new String[] {
+ "01-Jan-2020 00:01:07",
+ },
+ new FieldList(new Field[] {
+ Field.TIMESTAMP,
+ }),
+ null),
+ // start a second segment
+ new DataPoint(
+ new String[] {
+ "01-Jan-2020 00:00:20",
+ "1",
+ },
+ new FieldList(new Field[] {
+ Field.TIMESTAMP,
+ Field.NEW_SEGMENT,
+ }),
+ null),
+ new DataPoint(
+ new String[] {
+ "01-Jan-2020 00:00:27",
+ },
+ new FieldList(new Field[] {
+ Field.TIMESTAMP,
+ }),
+ null),
+ };
+ track.appendPoints(points);
+
+ RangeStats range = new RangeStats(track, 0, track.getNumPoints() - 1);
+ assertEquals(7 + 7, range.getMovingDurationInSeconds());
+ assertEquals(47, range.getTotalDurationInSeconds());
+ assertTrue(range.getEarliestTimestamp().isEqual(new TimestampUtc("01-Jan-2020 00:00:20")));
+ assertTrue(range.getLatestTimestamp().isEqual(new TimestampUtc("01-Jan-2020 00:01:07")));
+ assertTrue(range.getTimestampsIncomplete());
+
+ // even though segment 2 is earlier than segment 1, timestamps
+ // within each segment are normally ordered
+ assertFalse(range.getTimestampsOutOfSequence());
+ }
+
+ @Test
+ void movingTimeMissingFirstTimestamp()
+ {
+ Track track = new Track();
+ DataPoint[] points = {
+ new DataPoint(
+ new String[] {},
+ new FieldList(new Field[] {}),
+ null),
+ new DataPoint(
+ new String[] {
+ "01-Jan-2020 00:00:00",
+ },
+ new FieldList(new Field[] {
+ Field.TIMESTAMP,
+ }),
+ null),
+ new DataPoint(
+ new String[] {
+ "01-Jan-2020 00:00:05",
+ },
+ new FieldList(new Field[] {
+ Field.TIMESTAMP,
+ }),
+ null),
+ };
+ track.appendPoints(points);
+
+ RangeStats range = new RangeStats(track, 0, track.getNumPoints() - 1);
+ assertEquals(5, range.getMovingDurationInSeconds());
+ assertEquals(5, range.getTotalDurationInSeconds());
+ assertTrue(range.getTimestampsIncomplete());
+ assertFalse(range.getTimestampsOutOfSequence());
+ }
+}
--- /dev/null
+package tim.prune.function.cache;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for tile name checks
+ */
+class TileSetTest
+{
+ @Test
+ void testIsNumeric()
+ {
+ // not numeric, should be false
+ assertFalse(TileSet.isNumeric(null));
+ assertFalse(TileSet.isNumeric(""));
+ assertFalse(TileSet.isNumeric("a"));
+ assertFalse(TileSet.isNumeric(" "));
+ assertFalse(TileSet.isNumeric("155a"));
+ assertFalse(TileSet.isNumeric("-2"));
+ // numeric, should be true
+ assertTrue(TileSet.isNumeric("1"));
+ assertTrue(TileSet.isNumeric("155"));
+ }
+
+ @Test
+ void testIsNumericUntilDot()
+ {
+ // not numeric, should be false
+ assertFalse(TileSet.isNumericUntilDot(null));
+ assertFalse(TileSet.isNumericUntilDot(""));
+ assertFalse(TileSet.isNumericUntilDot("."));
+ assertFalse(TileSet.isNumericUntilDot(".abc"));
+ assertFalse(TileSet.isNumericUntilDot("a3."));
+ assertFalse(TileSet.isNumericUntilDot("4a"));
+ assertFalse(TileSet.isNumericUntilDot("215327h.png"));
+ // numeric but no dot, should be false
+ assertFalse(TileSet.isNumericUntilDot("1234"));
+ // numeric, should be true
+ System.out.println(TileSet.isNumericUntilDot("44.jpg"));
+ System.out.println(TileSet.isNumericUntilDot("0."));
+ }
+}
--- /dev/null
+package tim.prune.function.olc;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for decoding of Open Location Codes (Pluscodes)
+ */
+class OlcDecoderTest
+{
+
+ @Test
+ void testDecodeStringsTooShort()
+ {
+ OlcArea area = OlcDecoder.decode(null);
+ assertEquals(area, null, "Decoding null gives null");
+ area = OlcDecoder.decode("");
+ assertEquals(area, null, "Decoding \"\" gives null");
+ area = OlcDecoder.decode("9");
+ assertEquals(area, null, "Decoding \"9\" gives null");
+ area = OlcDecoder.decode("9999999");
+ assertEquals(area, null, "Decoding \"9999999\" gives null");
+ }
+
+ @Test
+ void testDecodeStringsInvalid()
+ {
+ OlcArea area = OlcDecoder.decode("11111111");
+ assertEquals(area, null, "Decoding lots of 1s gives null");
+ area = OlcDecoder.decode("99999991");
+ assertEquals(area, null, "Decoding with a single 1 gives null");
+ area = OlcDecoder.decode("99999999");
+ assertNotEquals(area, null, "Decoding with all 9s gives non-null");
+ area = OlcDecoder.decode("00000000");
+ assertEquals(area, null, "Decoding with all padding gives null");
+ area = OlcDecoder.decode("99000000");
+ assertNotEquals(area, null, "Decoding with some padding gives non-null");
+ }
+
+ @Test
+ void testDecodeZeroes()
+ {
+ OlcArea area = OlcDecoder.decode("22000000");
+ assertNotEquals(area, null, "Decoding with padding gives non-null");
+ assertEquals(-90.0, area.minLat, 0.0, "South 90");
+ assertEquals(-70.0, area.maxLat, 0.0, "South 70");
+ assertEquals(-180.0, area.minLon, 0.0, "West 180");
+ assertEquals(-160.0, area.maxLon, 0.0, "West 160");
+ }
+
+ @Test
+ void testDecodeZeroes2()
+ {
+ OlcArea area = OlcDecoder.decode("22220000");
+ assertNotEquals(area, null, "Decoding with padding gives non-null");
+ assertEquals(-90.0, area.minLat, 0.0, "South 90");
+ assertEquals(-89.0, area.maxLat, 0.0, "South 89");
+ assertEquals(-180.0, area.minLon, 0.0, "West 180");
+ assertEquals(-179.0, area.maxLon, 0.0, "West 179");
+ }
+
+ @Test
+ void testMountainView()
+ {
+ OlcArea area = OlcDecoder.decode("6PH57VP3+PR6");
+ assertNotEquals(area, null, "Decoding with separator gives non-null");
+ System.out.println("Min lat: " + area.minLat);
+ System.out.println("Max lat: " + area.maxLat);
+ System.out.println("Min lon: " + area.minLon);
+ System.out.println("Max lon: " + area.maxLon);
+ assertTrue(area.maxLat > area.minLat, "latitude range");
+ assertTrue(area.maxLon > area.minLon, "longitude range");
+ }
+}
--- /dev/null
+package tim.prune.function.weather;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for weather icons
+ */
+class SingleForecastTest
+{
+
+ @Test
+ void testWeatherIcons()
+ {
+ testIconName(null, "100", "");
+ testIconName("storm.png", "200", null);
+ testIconName("storm.png", "204", "");
+ testIconName("lightrain.png", "300", null);
+ testIconName("lightrain.png", "301", null);
+ testIconName(null, "400", null);
+ testIconName("lightrain.png", "500", null);
+ testIconName("rain.png", "501", null);
+ testIconName("rain.png", "599", null);
+ testIconName("hail.png", "511", null);
+ testIconName("snow.png", "600", null);
+ testIconName("fog.png", "700", null);
+ testIconName("clear-day.png", "800", null);
+ testIconName("clear-day.png", "800", "");
+ testIconName("clear-day.png", "800", "01d");
+ testIconName("clear-night.png", "800", "01n");
+ testIconName("clouds-day.png", "802", "01d");
+ testIconName("clouds-night.png", "802", "01n");
+ testIconName("clouds.png", "804", "01n");
+ testIconName("extreme.png", "900", "01d");
+ testIconName("hail.png", "906", "01n");
+ }
+
+ /**
+ * Test getting an icon name according to code and image
+ */
+ private static void testIconName(String inExpect, String inCode, String inImage)
+ {
+ String icon = SingleForecast.getIconName(inCode, inImage);
+ assertEquals(inExpect, icon, showString(inCode) + ", " + showString(inImage));
+ }
+
+ private static String showString(String inString)
+ {
+ return inString == null ? "null" : inString;
+ }
+}
--- /dev/null
+package tim.prune.gui.map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for manipulating base Urls
+ */
+class MapSourceTest
+{
+ @Test
+ void testFixBaseUrls()
+ {
+ // Should succeed
+ testUrlFix("8bitcities.s3.amazonaws.com", "http://8bitcities.s3.amazonaws.com/");
+ testUrlFix("8bitcities.s3.amazonaws.com/", "http://8bitcities.s3.amazonaws.com/");
+ testUrlFix("http://8bitcities.s3.amazonaws.com/", "http://8bitcities.s3.amazonaws.com/");
+ testUrlFix("something.com/ok", "http://something.com/ok/");
+
+ // These should fail and return null
+ testUrlFix("something/wrong", null);
+ testUrlFix("protocol://something.com/16/", null);
+ }
+
+ private void testUrlFix(String inStart, String inExpected)
+ {
+ String result = MapSource.fixBaseUrl(inStart);
+ assertEquals(inExpected, result);
+ }
+}
--- /dev/null
+package tim.prune.gui.map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.HashSet;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for site name utils
+ */
+class SiteNameUtilsTest
+{
+
+ @Test
+ void testPickServerNameWithoutWildcards()
+ {
+ testPickSingleUrl("abc", "abc");
+ testPickSingleUrl("ab[]c", "abc");
+ testPickSingleUrl("[]abc", "abc");
+ testPickSingleUrl("abc[]", "abc");
+ }
+
+ /**
+ * Test a pattern without wildcards which should always produce the expected result
+ * @param inPattern pattern for site name
+ * @param inExpected expected resolved name
+ */
+ private void testPickSingleUrl(String inPattern, String inExpected)
+ {
+ for (int i=0; i<20; i++)
+ {
+ String resolved = SiteNameUtils.pickServerUrl(inPattern);
+ assertEquals(inExpected, resolved, "Failed: " + inPattern);
+ }
+ }
+
+ @Test
+ void testPickUsingWildcards()
+ {
+ testRandomPick("ab[123]c", new String[]{"ab1c", "ab2c", "ab3c"});
+ testRandomPick("1234.[abcd]", new String[]{"1234.a", "1234.b", "1234.c", "1234.d"});
+ }
+
+ /**
+ * Test a pattern with wildcards which should produce several different results randomly
+ * @param inPattern pattern for site name
+ * @param inExpected array of expected resolved names
+ */
+ private void testRandomPick(String inPattern, String[] inExpected)
+ {
+ HashSet<String> results = new HashSet<String>();
+ for (int i=0; i<30; i++)
+ {
+ results.add(SiteNameUtils.pickServerUrl(inPattern));
+ }
+ // Check that all expected results were returned
+ assertEquals(inExpected.length, results.size());
+ for (String expec : inExpected) {
+ assertTrue(results.contains(expec));
+ }
+ }
+}
--- /dev/null
+package tim.prune.jpeg.drew;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit tests for the Rational values used by the Exif
+ */
+class RationalTest
+{
+ @Test
+ void testManyInts()
+ {
+ testIntVal(0, 0, 0);
+ testIntVal(1, 0, 0);
+ testIntVal(0, 1, 0);
+ for (int i=0; i<16000; i++)
+ {
+ testIntVal(0, i, 0);
+ testIntVal(i, 0, 0);
+ testIntVal(i, 1, i);
+ testIntVal(-i, 1, -i);
+ testIntVal(i*2, 2, i);
+ testIntVal(i*2+1, 2, i); // rounding down the 0.5
+ testIntVal(-i*2, 2, -i);
+ testIntVal(i*2, -2, -i);
+ testIntVal(-i*2, -2, i);
+ }
+ }
+
+ /**
+ * Check that a rational converts to an integer properly
+ * @param inTop number on top of the rational (numerator)
+ * @param inBottom number on bottom of the rational (denominator)
+ * @param inExpected expected int value
+ */
+ private void testIntVal(long inTop, long inBottom, int inExpected)
+ {
+ Rational value = new Rational(inTop, inBottom);
+ assertEquals(inExpected, value.intValue(), "" + inTop + "/" + inBottom);
+ }
+
+ @Test
+ void testManyDoubles()
+ {
+ for (int i=0; i<16000; i++)
+ {
+ testDoubleVal(0, i, 0.0);
+ testDoubleVal(i, 0, 0.0);
+ testDoubleVal(i, 1, i);
+ testDoubleVal(i*2, 2, i);
+ testDoubleVal(i*2+1, 2, i+0.5);
+ testDoubleVal(i*2, -2, -i);
+ }
+
+ testDoubleVal(123, 3, 123.0/3.0);
+ }
+
+ /**
+ * Check that a rational converts to a double properly
+ * @param inTop number on top of the rational (numerator)
+ * @param inBottom number on bottom of the rational (denominator)
+ * @param inExpected expected double value (exact)
+ */
+ private void testDoubleVal(long inTop, long inBottom, double inExpected)
+ {
+ Rational value = new Rational(inTop, inBottom);
+ assertEquals(inExpected, value.doubleValue(), 0.0, "" + inTop + "/" + inBottom);
+ }
+}