From 50e2bfd13c9f6b172ffbf7efe6a11ffdb6fdd243 Mon Sep 17 00:00:00 2001 From: Hallvard Traetteberg <hal@ntnu.no> Date: Mon, 5 Feb 2018 17:42:07 +0100 Subject: [PATCH] Support for importing GPX files. Both core and UI code. --- tdt4140-gr1800/app.core/pom.xml | 12 ++ .../java/tdt4140/gr1800/app/core/App.java | 106 ++++++++++++++---- .../gr1800/app/core/DocumentStorageImpl.java | 20 ++-- .../tdt4140/gr1800/app/core/GeoLocated.java | 19 ++++ .../tdt4140/gr1800/app/core/GeoLocations.java | 36 +++--- .../gr1800/app/core/IDocumentImporter.java | 7 ++ .../gr1800/app/core/IDocumentLoader.java | 5 + .../gr1800/app/core/IDocumentPersistence.java | 4 + .../gr1800/app/core/IDocumentSaver.java | 5 + .../gr1800/app/core/IDocumentStorage.java | 3 + .../app/core/IGeoLocationsListener.java | 6 + .../java/tdt4140/gr1800/app/core/LatLong.java | 9 +- .../gr1800/app/gpx/GpxDocumentConverter.java | 91 +++++++++++++++ .../gr1800/app/gpx/GpxDocumentLoader.java | 19 ++++ .../app/json/GeoLocationsJsonSerializer.java | 8 +- .../gr1800/app/core/GeoLocationsTest.java | 16 ++- .../gr1800/app/core/GpxPersistenceTest.java | 93 +++++++++++++++ .../gr1800/app/core/Achterbroek-route.gpx | 92 +++++++++++++++ .../gr1800/app/core/Achterbroek-track.gpx | 96 ++++++++++++++++ .../tdt4140/gr1800/app/core/sample1.gpx | 26 +++++ .../gr1800/app/ui/FileMenuController.java | 17 +++ .../gr1800/app/ui/FxAppController.java | 30 +++-- .../tdt4140/gr1800/app/ui/FileMenu.fxml | 4 + 23 files changed, 658 insertions(+), 66 deletions(-) create mode 100644 tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/GeoLocated.java create mode 100644 tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentImporter.java create mode 100644 tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentLoader.java create mode 100644 tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentPersistence.java create mode 100644 tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentSaver.java create mode 100644 tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IGeoLocationsListener.java create mode 100644 tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/gpx/GpxDocumentConverter.java create mode 100644 tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/gpx/GpxDocumentLoader.java create mode 100644 tdt4140-gr1800/app.core/src/test/java/tdt4140/gr1800/app/core/GpxPersistenceTest.java create mode 100644 tdt4140-gr1800/app.core/src/test/resources/tdt4140/gr1800/app/core/Achterbroek-route.gpx create mode 100644 tdt4140-gr1800/app.core/src/test/resources/tdt4140/gr1800/app/core/Achterbroek-track.gpx create mode 100644 tdt4140-gr1800/app.core/src/test/resources/tdt4140/gr1800/app/core/sample1.gpx diff --git a/tdt4140-gr1800/app.core/pom.xml b/tdt4140-gr1800/app.core/pom.xml index 985bded..b780010 100644 --- a/tdt4140-gr1800/app.core/pom.xml +++ b/tdt4140-gr1800/app.core/pom.xml @@ -25,6 +25,18 @@ <artifactId>jackson-annotations</artifactId> <version>2.9.3</version> </dependency> + <dependency> + <groupId>com.fasterxml.jackson.dataformat</groupId> + <artifactId>jackson-dataformat-xml</artifactId> + <version>2.9.3</version> + </dependency> + + <!-- https://mvnrepository.com/artifact/io.jenetics/jpx --> + <dependency> + <groupId>io.jenetics</groupId> + <artifactId>jpx</artifactId> + <version>1.2.2</version> + </dependency> <dependency> <groupId>junit</groupId> diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/App.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/App.java index 7c09f41..b7bd765 100644 --- a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/App.java +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/App.java @@ -4,21 +4,19 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.stream.Collectors; +import tdt4140.gr1800.app.gpx.GpxDocumentConverter; import tdt4140.gr1800.app.json.GeoLocationsJsonPersistence; public class App { private GeoLocationsPersistence geoLocationsLoader = new GeoLocationsJsonPersistence(); - public void loadGeoLocations(URI uri) throws Exception { - geoLocations = geoLocationsLoader.loadLocations(uri.toURL().openStream()); - } - - private Collection<GeoLocations> geoLocations; + private Collection<GeoLocations> geoLocations = null; public Iterable<String> getGeoLocationNames() { Collection<String> names = new ArrayList<String>(geoLocations != null ? geoLocations.size() : 0); @@ -69,8 +67,55 @@ public class App { return result; } + public void setGeoLocations(Collection<GeoLocations> geoLocations) { + this.geoLocations = new ArrayList<>(geoLocations); + fireGeoLocationsUpdated(null); + } + + public void addGeoLocations(GeoLocations geoLocations) { + if (hasGeoLocations(geoLocations.getName())) { + throw new IllegalArgumentException("Duplicate geo-locations name: " + geoLocations.getName()); + } + this.geoLocations.add(geoLocations); + fireGeoLocationsUpdated(geoLocations); + } + + public void removeGeoLocations(GeoLocations geoLocations) { + this.geoLocations.remove(geoLocations); + fireGeoLocationsUpdated(geoLocations); + } + + private Collection<IGeoLocationsListener> geoLocationsListeners = new ArrayList<>(); + + public void addGeoLocationsListener(IGeoLocationsListener listener) { + geoLocationsListeners.add(listener); + } + + public void removeGeoLocationsListener(IGeoLocationsListener listener) { + geoLocationsListeners.remove(listener); + } + + protected void fireGeoLocationsUpdated(GeoLocations geoLocations) { + for (IGeoLocationsListener listener : geoLocationsListeners) { + listener.geoLocationsUpdated(geoLocations); + } + } + // + private IDocumentPersistence<Collection<GeoLocations>, File> documentPersistence = new IDocumentPersistence<Collection<GeoLocations>, File>() { + + @Override + public Collection<GeoLocations> loadDocument(File documentLocation) throws Exception { + return geoLocationsLoader.loadLocations(new FileInputStream(documentLocation)); + } + + @Override + public void saveDocument(Collection<GeoLocations> document, File documentLocation) throws Exception { + geoLocationsLoader.saveLocations(document, new FileOutputStream(documentLocation)); + } + }; + private DocumentStorageImpl<Collection<GeoLocations>, File> documentStorage = new DocumentStorageImpl<Collection<GeoLocations>, File>() { @Override @@ -80,7 +125,7 @@ public class App { @Override protected void setDocument(Collection<GeoLocations> document) { - geoLocations = document; + setGeoLocations(document); } @Override @@ -88,26 +133,43 @@ public class App { return new ArrayList<GeoLocations>(); } - @Override - protected Collection<GeoLocations> loadDocument(File file) throws IOException { - try { - return geoLocationsLoader.loadLocations(new FileInputStream(file)); - } catch (Exception e) { - throw new IOException(e); - } + public Collection<GeoLocations> loadDocument(File documentLocation) throws Exception { + return documentPersistence.loadDocument(documentLocation); } - - @Override - protected void storeDocument(Collection<GeoLocations> document, File file) throws IOException { - try { - geoLocationsLoader.saveLocations(document, new FileOutputStream(file)); - } catch (Exception e) { - throw new IOException(e); - } + + public void saveDocument(Collection<GeoLocations> document, File documentLocation) throws Exception { + documentPersistence.saveDocument(document, documentLocation); + } + + public Collection<IDocumentImporter<File>> getDocumentImporters() { + return documentLoaders.stream().map(loader -> new IDocumentImporter<File>() { + @Override + public void importDocument(File file) throws IOException { + try { + setDocumentAndLocation(loader.loadDocument(file), null); + } catch (Exception e) { + throw new IOException(e); + } + } + }).collect(Collectors.toList()); } }; public IDocumentStorage<File> getDocumentStorage() { return documentStorage; } + + private Collection<IDocumentLoader<Collection<GeoLocations>, File>> documentLoaders = Arrays.asList( + new IDocumentLoader<Collection<GeoLocations>, File>() { + private GpxDocumentConverter gpxConverter = new GpxDocumentConverter(); + @Override + public Collection<GeoLocations> loadDocument(File documentLocation) throws Exception { + return gpxConverter.loadDocument(documentLocation); + } + } + ); + + public Iterable<IDocumentLoader<Collection<GeoLocations>, File>> getDocumentLoaders() { + return documentLoaders; + } } diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/DocumentStorageImpl.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/DocumentStorageImpl.java index 559279c..b673c87 100644 --- a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/DocumentStorageImpl.java +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/DocumentStorageImpl.java @@ -2,7 +2,7 @@ package tdt4140.gr1800.app.core; import java.io.IOException; -public abstract class DocumentStorageImpl<D, L> implements IDocumentStorage<L> { +public abstract class DocumentStorageImpl<D, L> implements IDocumentStorage<L>, IDocumentPersistence<D, L> { private L documentLocation; @@ -25,8 +25,6 @@ public abstract class DocumentStorageImpl<D, L> implements IDocumentStorage<L> { protected abstract void setDocument(D document); protected abstract D createDocument(); - protected abstract D loadDocument(L storage) throws IOException; - protected abstract void storeDocument(D document, L storage) throws IOException; @Override public void newDocument() { @@ -35,12 +33,20 @@ public abstract class DocumentStorageImpl<D, L> implements IDocumentStorage<L> { @Override public void openDocument(L storage) throws IOException { - setDocumentAndLocation(loadDocument(storage), storage); + try { + setDocumentAndLocation(loadDocument(storage), storage); + } catch (Exception e) { + throw new IOException(e); + } } @Override public void saveDocument() throws IOException { - storeDocument(getDocument(), getDocumentLocation()); + try { + saveDocument(getDocument(), getDocumentLocation()); + } catch (Exception e) { + throw new IOException(e); + } } public void saveDocumentAs(L documentLocation) throws IOException { @@ -54,7 +60,7 @@ public abstract class DocumentStorageImpl<D, L> implements IDocumentStorage<L> { } } - public void saveCopyAs(L documentLocation) throws IOException { - storeDocument(getDocument(), documentLocation); + public void saveCopyAs(L documentLocation) throws Exception { + saveDocument(getDocument(), documentLocation); } } diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/GeoLocated.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/GeoLocated.java new file mode 100644 index 0000000..e4ea1ba --- /dev/null +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/GeoLocated.java @@ -0,0 +1,19 @@ +package tdt4140.gr1800.app.core; + +public interface GeoLocated { + + public LatLong getLatLong(); + + default double getLatitude() { + return getLatLong().latitude; + } + default double getLongitude() { + return getLatLong().longitude; + } + default boolean equalsLatLong(GeoLocated geoLoc) { + return getLatitude() == geoLoc.getLatitude() && getLongitude() == geoLoc.getLongitude(); + } + default double distance(GeoLocated geoLoc) { + return getLatLong().distance(geoLoc.getLatLong()); + } +} diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/GeoLocations.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/GeoLocations.java index 3327a22..19e2fd4 100644 --- a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/GeoLocations.java +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/GeoLocations.java @@ -7,7 +7,7 @@ import java.util.Iterator; import java.util.Optional; import java.util.stream.Collectors; -public class GeoLocations implements Iterable<LatLong> { +public class GeoLocations implements Iterable<GeoLocated> { private String name; @@ -19,7 +19,7 @@ public class GeoLocations implements Iterable<LatLong> { this.name = name; } - private Collection<LatLong> locations = new ArrayList<LatLong>(); + private Collection<GeoLocated> locations = new ArrayList<GeoLocated>(); private boolean path = false; public GeoLocations(LatLong...latLongs) { @@ -27,7 +27,7 @@ public class GeoLocations implements Iterable<LatLong> { addLocation(latLongs[i]); } } - + public GeoLocations(String name, LatLong...latLongs) { this(latLongs); setName(name); @@ -41,34 +41,34 @@ public class GeoLocations implements Iterable<LatLong> { this.path = path; } - public Comparator<? super LatLong> closestComparator(LatLong latLong) { - return (latLong1, latLong2) -> (int) Math.signum(latLong.distance(latLong1) - latLong.distance(latLong2)); + public Comparator<? super GeoLocated> closestComparator(GeoLocated latLong) { + return (geoLoc1, geoLoc2) -> (int) Math.signum(latLong.distance(geoLoc1) - latLong.distance(geoLoc2)); } - public Collection<LatLong> findLocationsNearby(LatLong latLong, double distance) { + public Collection<GeoLocated> findLocationsNearby(GeoLocated geoLoc, double distance) { return locations.stream() - .filter(latLong2 -> distance == 0.0 ? latLong2.equals(latLong) : latLong.distance(latLong2) <= distance) - .sorted(closestComparator(latLong)) + .filter(geoLoc2 -> distance == 0.0 ? geoLoc2.equalsLatLong(geoLoc) : geoLoc.distance(geoLoc2) <= distance) + .sorted(closestComparator(geoLoc)) .collect(Collectors.toList()); } - public LatLong findNearestLocation(LatLong latLong) { - Optional<LatLong> min = locations.stream() - .min(closestComparator(latLong)); + public GeoLocated findNearestLocation(GeoLocated geoLoc) { + Optional<GeoLocated> min = locations.stream() + .min(closestComparator(geoLoc)); return min.isPresent() ? min.get() : null; } // - public void addLocation(LatLong latLong) { - locations.add(latLong); + public void addLocation(LatLong geoLoc) { + locations.add(geoLoc); } - public void removeLocations(LatLong latLong, double distance) { - Iterator<LatLong> it = locations.iterator(); + public void removeLocations(GeoLocated geoLoc, double distance) { + Iterator<GeoLocated> it = locations.iterator(); while (it.hasNext()) { - LatLong latLong2 = it.next(); - if (distance == 0.0 ? latLong2.equals(latLong) : latLong.distance(latLong2) <= distance) { + GeoLocated geoLoc2 = it.next(); + if (distance == 0.0 ? geoLoc2.equalsLatLong(geoLoc) : geoLoc.getLatLong().distance(geoLoc2.getLatLong()) <= distance) { it.remove(); } } @@ -79,7 +79,7 @@ public class GeoLocations implements Iterable<LatLong> { } @Override - public Iterator<LatLong> iterator() { + public Iterator<GeoLocated> iterator() { return locations.iterator(); } } diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentImporter.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentImporter.java new file mode 100644 index 0000000..c37c4c5 --- /dev/null +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentImporter.java @@ -0,0 +1,7 @@ +package tdt4140.gr1800.app.core; + +import java.io.IOException; + +public interface IDocumentImporter<L> { + public void importDocument(L documentLocation) throws IOException; +} diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentLoader.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentLoader.java new file mode 100644 index 0000000..928fd9b --- /dev/null +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentLoader.java @@ -0,0 +1,5 @@ +package tdt4140.gr1800.app.core; + +public interface IDocumentLoader<D, L> { + public D loadDocument(L documentLocation) throws Exception; +} diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentPersistence.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentPersistence.java new file mode 100644 index 0000000..756f85d --- /dev/null +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentPersistence.java @@ -0,0 +1,4 @@ +package tdt4140.gr1800.app.core; + +public interface IDocumentPersistence<D, L> extends IDocumentLoader<D, L>, IDocumentSaver<D, L> { +} diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentSaver.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentSaver.java new file mode 100644 index 0000000..4d2ee0a --- /dev/null +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentSaver.java @@ -0,0 +1,5 @@ +package tdt4140.gr1800.app.core; + +public interface IDocumentSaver<D, L> { + public void saveDocument(D document, L documentLocation) throws Exception; +} diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentStorage.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentStorage.java index ceef961..147bd16 100644 --- a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentStorage.java +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IDocumentStorage.java @@ -1,6 +1,7 @@ package tdt4140.gr1800.app.core; import java.io.IOException; +import java.util.Collection; public interface IDocumentStorage<L> { public L getDocumentLocation(); @@ -9,4 +10,6 @@ public interface IDocumentStorage<L> { public void newDocument(); public void openDocument(L documentLocation) throws IOException; public void saveDocument() throws IOException; + + public Collection<IDocumentImporter<L>> getDocumentImporters(); } diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IGeoLocationsListener.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IGeoLocationsListener.java new file mode 100644 index 0000000..787f85d --- /dev/null +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/IGeoLocationsListener.java @@ -0,0 +1,6 @@ +package tdt4140.gr1800.app.core; + +public interface IGeoLocationsListener { + + public void geoLocationsUpdated(GeoLocations geoLocations); +} diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/LatLong.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/LatLong.java index 5576e25..c88e7e9 100644 --- a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/LatLong.java +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/core/LatLong.java @@ -1,6 +1,6 @@ package tdt4140.gr1800.app.core; -public class LatLong { +public class LatLong implements GeoLocated { public final double latitude, longitude; @@ -57,6 +57,13 @@ public class LatLong { return new LatLong(lat, lon); } + // GeoLocated + + @Override + public LatLong getLatLong() { + return this; + } + /*::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/ /*:: :*/ /*:: This routine calculates the distance between two points (given the :*/ diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/gpx/GpxDocumentConverter.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/gpx/GpxDocumentConverter.java new file mode 100644 index 0000000..55550bf --- /dev/null +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/gpx/GpxDocumentConverter.java @@ -0,0 +1,91 @@ +package tdt4140.gr1800.app.gpx; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.Collectors; + +import io.jenetics.jpx.GPX; +import io.jenetics.jpx.Route; +import io.jenetics.jpx.Track; +import io.jenetics.jpx.TrackSegment; +import io.jenetics.jpx.WayPoint; +import tdt4140.gr1800.app.core.GeoLocations; +import tdt4140.gr1800.app.core.IDocumentLoader; +import tdt4140.gr1800.app.core.LatLong; + +public class GpxDocumentConverter implements IDocumentLoader<Collection<GeoLocations>, File> { + + private GpxDocumentLoader gpxLoader = new GpxDocumentLoader(); + + @Override + public Collection<GeoLocations> loadDocument(File documentLocation) throws Exception { + GPX gpx = gpxLoader.loadDocument(documentLocation.toURI().toURL()); + return convert(gpx); + } + + private String trackNameFormat = "%s"; + private String trackCountFormat = "Track %s"; + private String trackSegmentFormat = "%s.%s"; + + public void setTrackNameFormat(String trackNameFormat) { + this.trackNameFormat = trackNameFormat; + } + + public void setTrackCountFormat(String trackCountFormat) { + this.trackCountFormat = trackCountFormat; + } + + public void setTrackSegmentFormat(String trackSegmentFormat) { + this.trackSegmentFormat = trackSegmentFormat; + } + + private String routeNameFormat = "%s"; + private String routeCountFormat = "Route %s"; + + public void setRouteNameFormat(String routeNameFormat) { + this.routeNameFormat = routeNameFormat; + } + + public void setRouteCountFormat(String routeCountFormat) { + this.routeCountFormat = routeCountFormat; + } + + public Collection<GeoLocations> convert(GPX gpx) throws Exception { + Collection<GeoLocations> geoLocations = new ArrayList<GeoLocations>(); + int trackCount = 1; + for (Track track : gpx.getTracks()) { + String trackName = (track.getName().isPresent() ? String.format(trackNameFormat, track.getName().get()) : String.format(trackCountFormat, trackCount)); + int segmentCount = 1; + for (TrackSegment segment : track) { + boolean singleTrack = track.getSegments().size() <= 1; + String name = (singleTrack ? trackName : String.format(trackSegmentFormat, trackName, segmentCount)); + GeoLocations gl = new GeoLocations(name, convert(segment.getPoints())); + gl.setPath(true); + geoLocations.add(gl); + } + } + int routeCount = 1; + for (Route route : gpx.getRoutes()) { + String routeName = (route.getName().isPresent() ? String.format(routeNameFormat, route.getName().get()) : String.format(routeCountFormat, routeCount)); + GeoLocations gl = new GeoLocations(routeName, convert(route.getPoints())); + gl.setPath(true); + geoLocations.add(gl); + } + if (! gpx.getWayPoints().isEmpty()) { + geoLocations.add(new GeoLocations("Waypoints", convert(gpx.getWayPoints()))); + } + return geoLocations; + } + + private LatLong[] convert(Collection<WayPoint> points) { + Collection<LatLong> latLongs = points.stream() + .map(point -> convert(point)) + .collect(Collectors.toList()); + return latLongs.toArray(new LatLong[latLongs.size()]); + } + + private LatLong convert(WayPoint point) { + return new LatLong(point.getLatitude().doubleValue(), point.getLongitude().doubleValue()); + } +} diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/gpx/GpxDocumentLoader.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/gpx/GpxDocumentLoader.java new file mode 100644 index 0000000..28e971d --- /dev/null +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/gpx/GpxDocumentLoader.java @@ -0,0 +1,19 @@ +package tdt4140.gr1800.app.gpx; + +import java.io.InputStream; +import java.net.URL; + +import io.jenetics.jpx.GPX; +import tdt4140.gr1800.app.core.IDocumentLoader; + +public class GpxDocumentLoader implements IDocumentLoader<GPX, URL> { + + public GPX loadDocument(InputStream inputStream) throws Exception { + return GPX.read(inputStream, true); + } + + @Override + public GPX loadDocument(URL documentLocation) throws Exception { + return loadDocument(documentLocation.openStream()); + } +} diff --git a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/json/GeoLocationsJsonSerializer.java b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/json/GeoLocationsJsonSerializer.java index da7aa07..2f08083 100644 --- a/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/json/GeoLocationsJsonSerializer.java +++ b/tdt4140-gr1800/app.core/src/main/java/tdt4140/gr1800/app/json/GeoLocationsJsonSerializer.java @@ -6,8 +6,8 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import tdt4140.gr1800.app.core.GeoLocated; import tdt4140.gr1800.app.core.GeoLocations; -import tdt4140.gr1800.app.core.LatLong; public class GeoLocationsJsonSerializer extends StdSerializer<GeoLocations> { @@ -30,10 +30,10 @@ public class GeoLocationsJsonSerializer extends StdSerializer<GeoLocations> { jsonGen.writeBoolean(geoLocations.isPath()); jsonGen.writeFieldName(LOCATIONS_FIELD_NAME); jsonGen.writeStartArray(); - for (LatLong latLon : geoLocations) { + for (GeoLocated geoLoc : geoLocations) { jsonGen.writeStartArray(); - jsonGen.writeNumber(latLon.latitude); - jsonGen.writeNumber(latLon.longitude); + jsonGen.writeNumber(geoLoc.getLatitude()); + jsonGen.writeNumber(geoLoc.getLongitude()); jsonGen.writeEndArray(); } jsonGen.writeEndArray(); diff --git a/tdt4140-gr1800/app.core/src/test/java/tdt4140/gr1800/app/core/GeoLocationsTest.java b/tdt4140-gr1800/app.core/src/test/java/tdt4140/gr1800/app/core/GeoLocationsTest.java index 1cdd829..654d377 100644 --- a/tdt4140-gr1800/app.core/src/test/java/tdt4140/gr1800/app/core/GeoLocationsTest.java +++ b/tdt4140-gr1800/app.core/src/test/java/tdt4140/gr1800/app/core/GeoLocationsTest.java @@ -29,11 +29,11 @@ public class GeoLocationsTest { } public static void assertGeoLocations(GeoLocations geoLocations, LatLong...latLongs) { - Iterator<LatLong> it = geoLocations.iterator(); + Iterator<GeoLocated> it = geoLocations.iterator(); Assert.assertEquals(latLongs.length, geoLocations.size()); int pos = 0; while (it.hasNext()) { - Assert.assertEquals(latLongs[pos], it.next()); + checkGeoLocated(latLongs[pos], it.next()); pos++; } } @@ -53,14 +53,18 @@ public class GeoLocationsTest { assertGeoLocations(latLong1, latLong1, latLong2); } + static void checkGeoLocated(GeoLocated geoLoc1, GeoLocated geoLoc2) { + Assert.assertTrue(geoLoc1.equalsLatLong(geoLoc2)); + } + @Test public void testFindLocationsNearby() { LatLong latLong = new LatLong(0, 0); Assert.assertTrue(geoLocations.findLocationsNearby(latLong, 0).isEmpty()); geoLocations.addLocation(latLong); - Collection<LatLong> locationsNearby = geoLocations.findLocationsNearby(latLong, 0); + Collection<GeoLocated> locationsNearby = geoLocations.findLocationsNearby(latLong, 0); Assert.assertEquals(1, locationsNearby.size()); - Assert.assertEquals(latLong, geoLocations.iterator().next()); + checkGeoLocated(latLong, geoLocations.iterator().next()); } @Test @@ -68,8 +72,8 @@ public class GeoLocationsTest { LatLong latLong = new LatLong(0, 0); Assert.assertNull(geoLocations.findNearestLocation(latLong)); geoLocations.addLocation(latLong); - LatLong nearestlocations = geoLocations.findNearestLocation(latLong); - Assert.assertEquals(latLong, nearestlocations); + GeoLocated nearestlocations = geoLocations.findNearestLocation(latLong); + checkGeoLocated(latLong, nearestlocations); } @Test diff --git a/tdt4140-gr1800/app.core/src/test/java/tdt4140/gr1800/app/core/GpxPersistenceTest.java b/tdt4140-gr1800/app.core/src/test/java/tdt4140/gr1800/app/core/GpxPersistenceTest.java new file mode 100644 index 0000000..2850cc9 --- /dev/null +++ b/tdt4140-gr1800/app.core/src/test/java/tdt4140/gr1800/app/core/GpxPersistenceTest.java @@ -0,0 +1,93 @@ +package tdt4140.gr1800.app.core; + +import java.util.Collection; +import java.util.Iterator; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import io.jenetics.jpx.GPX; +import tdt4140.gr1800.app.gpx.GpxDocumentConverter; +import tdt4140.gr1800.app.gpx.GpxDocumentLoader; + +public class GpxPersistenceTest { + + private GpxDocumentLoader loader; + private GpxDocumentConverter converter; + + @Before + public void setUp() { + loader = new GpxDocumentLoader(); + converter = new GpxDocumentConverter(); + } + + @Test + public void testLoadDocument() { + try { + GPX gpx = loader.loadDocument(getClass().getResource("sample1.gpx")); + Assert.assertEquals(1, gpx.getTracks().size()); + Assert.assertEquals(1, gpx.getTracks().get(0).getSegments().size()); + Assert.assertEquals(3, gpx.getTracks().get(0).getSegments().get(0).getPoints().size()); + } catch (Exception e) { + Assert.fail(e.getMessage()); + } + } + + @Test + public void testConvertSample1() { + try { + GPX gpx = loader.loadDocument(getClass().getResource("sample1.gpx")); + Collection<GeoLocations> geoLocations = converter.convert(gpx); + Assert.assertEquals(1, geoLocations.size()); + GeoLocations track = geoLocations.iterator().next(); + Assert.assertEquals("Example GPX Document", track.getName()); + Assert.assertEquals(3, track.size()); + Assert.assertTrue(track.isPath()); + } catch (Exception e) { + System.err.println(e); + Assert.fail(e.getMessage()); + } + } + + @Test + public void testConvertAchterbroekRoute() { + try { + GPX gpx = loader.loadDocument(getClass().getResource("Achterbroek-route.gpx")); + Collection<GeoLocations> geoLocations = converter.convert(gpx); + Assert.assertEquals(2, geoLocations.size()); + Iterator<GeoLocations> iterator = geoLocations.iterator(); + GeoLocations route = iterator.next(); + Assert.assertEquals("Achterbroek naar De Maatjes 13 km RT", route.getName()); + Assert.assertEquals(19, route.size()); + Assert.assertTrue(route.isPath()); + GeoLocations waypoints = iterator.next(); + Assert.assertEquals("Achterbroek naar De Maatjes 13 km RT", route.getName()); + Assert.assertEquals(1, waypoints.size()); + Assert.assertFalse(waypoints.isPath()); + } catch (Exception e) { + System.err.println(e); + Assert.fail(e.getMessage()); + } + } + + @Test + public void testConvertAchterbroekTrack() { + try { + GPX gpx = loader.loadDocument(getClass().getResource("Achterbroek-track.gpx")); + Collection<GeoLocations> geoLocations = converter.convert(gpx); + Assert.assertEquals(2, geoLocations.size()); + Iterator<GeoLocations> iterator = geoLocations.iterator(); + GeoLocations track = iterator.next(); + Assert.assertEquals("Achterbroek naar De Maatjes 13 km TR", track.getName()); + Assert.assertEquals(26, track.size()); + Assert.assertTrue(track.isPath()); + GeoLocations waypoints = iterator.next(); + Assert.assertEquals(1, waypoints.size()); + Assert.assertFalse(waypoints.isPath()); + } catch (Exception e) { + System.err.println(e); + Assert.fail(e.getMessage()); + } + } +} diff --git a/tdt4140-gr1800/app.core/src/test/resources/tdt4140/gr1800/app/core/Achterbroek-route.gpx b/tdt4140-gr1800/app.core/src/test/resources/tdt4140/gr1800/app/core/Achterbroek-route.gpx new file mode 100644 index 0000000..e6ce496 --- /dev/null +++ b/tdt4140-gr1800/app.core/src/test/resources/tdt4140/gr1800/app/core/Achterbroek-route.gpx @@ -0,0 +1,92 @@ +<?xml version="1.0"?> +<gpx + version="1.0" + creator="VB Net GPS: vermeiren-willy@pandora.be" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://www.topografix.com/GPX/1/0" + xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd"> + +<wpt lat="51.39709" lon="4.501519"> + <name>START</name> +</wpt> + +<rte> + <name>Achterbroek naar De Maatjes 13 km RT</name> + <rtept lat="51.39709" lon="4.501519"> + <name>1</name> + <cmt>1</cmt> + </rtept> + <rtept lat="51.398635" lon="4.502678"> + <name>2</name> + <cmt>2</cmt> + </rtept> + <rtept lat="51.405072" lon="4.498043"> + <name>3</name> + <cmt>3</cmt> + </rtept> + <rtept lat="51.417346" lon="4.515038"> + <name>4</name> + <cmt>4</cmt> + </rtept> + <rtept lat="51.41932" lon="4.513879"> + <name>5</name> + <cmt>5</cmt> + </rtept> + <rtept lat="51.421552" lon="4.523535"> + <name>6</name> + <cmt>6</cmt> + </rtept> + <rtept lat="51.423011" lon="4.522848"> + <name>7</name> + <cmt>7</cmt> + </rtept> + <rtept lat="51.423826" lon="4.5295"> + <name>8</name> + <cmt>8</cmt> + </rtept> + <rtept lat="51.427002" lon="4.528513"> + <name>9</name> + <cmt>9</cmt> + </rtept> + <rtept lat="51.430402" lon="4.552363"> + <name>10</name> + <cmt>10</cmt> + </rtept> + <rtept lat="51.426229" lon="4.555399"> + <name>11</name> + <cmt>11</cmt> + </rtept> + <rtept lat="51.426015" lon="4.554734"> + <name>12</name> + <cmt>12</cmt> + </rtept> + <rtept lat="51.423569" lon="4.556108"> + <name>13</name> + <cmt>13</cmt> + </rtept> + <rtept lat="51.41932" lon="4.546237"> + <name>14</name> + <cmt>14</cmt> + </rtept> + <rtept lat="51.410952" lon="4.534092"> + <name>15</name> + <cmt>15</cmt> + </rtept> + <rtept lat="51.406231" lon="4.543018"> + <name>16</name> + <cmt>16</cmt> + </rtept> + <rtept lat="51.396695" lon="4.515966"> + <name>17</name> + <cmt>17</cmt> + </rtept> + <rtept lat="51.396017" lon="4.515853"> + <name>18</name> + <cmt>18</cmt> + </rtept> + <rtept lat="51.397133" lon="4.502635"> + <name>19</name> + <cmt>19</cmt> + </rtept> +</rte> +</gpx> diff --git a/tdt4140-gr1800/app.core/src/test/resources/tdt4140/gr1800/app/core/Achterbroek-track.gpx b/tdt4140-gr1800/app.core/src/test/resources/tdt4140/gr1800/app/core/Achterbroek-track.gpx new file mode 100644 index 0000000..279b657 --- /dev/null +++ b/tdt4140-gr1800/app.core/src/test/resources/tdt4140/gr1800/app/core/Achterbroek-track.gpx @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<gpx + version="1.0" + creator="VB Net GPS: vermeiren-willy@pandora.be" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://www.topografix.com/GPX/1/0" + xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd"> + +<wpt lat="51.39709" lon="4.501519"> + <name>START</name> +</wpt> + +<trk> + <name>Achterbroek naar De Maatjes 13 km TR</name> + <trkseg> + <trkpt lat="51.39709" lon="4.501519"> + <name>1</name> + </trkpt> + <trkpt lat="51.397705" lon="4.501452"> + <name>2</name> + </trkpt> + <trkpt lat="51.398635" lon="4.502678"> + <name>3</name> + </trkpt> + <trkpt lat="51.404042" lon="4.498472"> + <name>4</name> + </trkpt> + <trkpt lat="51.405072" lon="4.498043"> + <name>5</name> + </trkpt> + <trkpt lat="51.405456" lon="4.499259"> + <name>6</name> + </trkpt> + <trkpt lat="51.417346" lon="4.515038"> + <name>7</name> + </trkpt> + <trkpt lat="51.41932" lon="4.513879"> + <name>8</name> + </trkpt> + <trkpt lat="51.421552" lon="4.523535"> + <name>9</name> + </trkpt> + <trkpt lat="51.423011" lon="4.522848"> + <name>10</name> + </trkpt> + <trkpt lat="51.423826" lon="4.5295"> + <name>11</name> + </trkpt> + <trkpt lat="51.427002" lon="4.528513"> + <name>12</name> + </trkpt> + <trkpt lat="51.427131" lon="4.534478"> + <name>13</name> + </trkpt> + <trkpt lat="51.426819" lon="4.53744"> + <name>14</name> + </trkpt> + <trkpt lat="51.430402" lon="4.552363"> + <name>15</name> + </trkpt> + <trkpt lat="51.426229" lon="4.555399"> + <name>16</name> + </trkpt> + <trkpt lat="51.426015" lon="4.554734"> + <name>17</name> + </trkpt> + <trkpt lat="51.423569" lon="4.556108"> + <name>18</name> + </trkpt> + <trkpt lat="51.423097" lon="4.553981"> + <name>19</name> + </trkpt> + <trkpt lat="51.41932" lon="4.546237"> + <name>20</name> + </trkpt> + <trkpt lat="51.410952" lon="4.534092"> + <name>21</name> + </trkpt> + <trkpt lat="51.406231" lon="4.543018"> + <name>22</name> + </trkpt> + <trkpt lat="51.396695" lon="4.515966"> + <name>23</name> + </trkpt> + <trkpt lat="51.396017" lon="4.515853"> + <name>24</name> + </trkpt> + <trkpt lat="51.396858" lon="4.506495"> + <name>25</name> + </trkpt> + <trkpt lat="51.397133" lon="4.502635"> + <name>26</name> + </trkpt> + </trkseg> +</trk> +</gpx> diff --git a/tdt4140-gr1800/app.core/src/test/resources/tdt4140/gr1800/app/core/sample1.gpx b/tdt4140-gr1800/app.core/src/test/resources/tdt4140/gr1800/app/core/sample1.gpx new file mode 100644 index 0000000..79b840d --- /dev/null +++ b/tdt4140-gr1800/app.core/src/test/resources/tdt4140/gr1800/app/core/sample1.gpx @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no" ?> +<gpx xmlns="http://www.topografix.com/GPX/1/1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" creator="Oregon 400t" version="1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd"> + <metadata> + <link href="http://www.garmin.com"> + <text>Garmin International</text> + </link> + <time>2009-10-17T22:58:43Z</time> + </metadata> + <trk> + <name>Example GPX Document</name> + <trkseg> + <trkpt lat="47.644548" lon="-122.326897"> + <ele>4.46</ele> + <time>2009-10-17T18:37:26Z</time> + </trkpt> + <trkpt lat="47.644548" lon="-122.33"> + <ele>4.94</ele> + <time>2009-10-17T18:37:31Z</time> + </trkpt> + <trkpt lat="47.65" lon="-122.33"> + <ele>6.87</ele> + <time>2009-10-17T18:37:34Z</time> + </trkpt> + </trkseg> + </trk> +</gpx> diff --git a/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/FileMenuController.java b/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/FileMenuController.java index f63754c..8575711 100644 --- a/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/FileMenuController.java +++ b/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/FileMenuController.java @@ -11,6 +11,7 @@ import javafx.fxml.FXML; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import javafx.stage.FileChooser; +import tdt4140.gr1800.app.core.IDocumentImporter; import tdt4140.gr1800.app.core.IDocumentStorage; public class FileMenuController { @@ -126,4 +127,20 @@ public class FileMenuController { documentStorage.setDocumentLocation(oldStorage); } } + + @FXML + public void handleImportAction() { + FileChooser fileChooser = getFileChooser(); + File selection = fileChooser.showOpenDialog(null); +// String path = selection.getPath(); +// int pos = path.lastIndexOf('.'); +// String ext = (pos > 0 ? path.substring(pos + 1) : null); + for (IDocumentImporter<File> importer : documentStorage.getDocumentImporters()) { + try { + importer.importDocument(selection); + break; + } catch (Exception e) { + } + } + } } diff --git a/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/FxAppController.java b/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/FxAppController.java index 22dc763..20c6562 100644 --- a/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/FxAppController.java +++ b/tdt4140-gr1800/app.ui/src/main/java/tdt4140/gr1800/app/ui/FxAppController.java @@ -2,6 +2,7 @@ package tdt4140.gr1800.app.ui; import java.util.Iterator; +import fxmapcontrol.Location; import fxmapcontrol.MapBase; import fxmapcontrol.MapItemsControl; import fxmapcontrol.MapNode; @@ -10,10 +11,12 @@ import javafx.fxml.FXML; import javafx.scene.control.ComboBox; import javafx.scene.control.Slider; import tdt4140.gr1800.app.core.App; +import tdt4140.gr1800.app.core.GeoLocated; import tdt4140.gr1800.app.core.GeoLocations; +import tdt4140.gr1800.app.core.IGeoLocationsListener; import tdt4140.gr1800.app.core.LatLong; -public class FxAppController { +public class FxAppController implements IGeoLocationsListener { @FXML private FileMenuController fileMenuController; @@ -36,12 +39,17 @@ public class FxAppController { app = new App(); fileMenuController.setDocumentStorage(app.getDocumentStorage()); fileMenuController.setOnDocumentChanged(documentStorage -> initMapMarkers()); + geoLocationsSelector.getSelectionModel().selectedItemProperty().addListener((stringProperty, oldValue, newValue) -> updateGeoLocations()); mapView.getChildren().add(MapTileLayer.getOpenStreetMapLayer()); - mapView.zoomLevelProperty().bind(zoomSlider.valueProperty()); + zoomSlider.valueProperty().addListener((prop, oldValue, newValue) -> { + mapView.setZoomLevel(zoomSlider.getValue()); + }); markersParent = new MapItemsControl<MapNode>(); mapView.getChildren().add(markersParent); + + app.addGeoLocationsListener(this); } private Object updateGeoLocations() { @@ -50,11 +58,12 @@ public class FxAppController { private void initMapMarkers() { markersParent.getItems().clear(); + geoLocationsSelector.getItems().clear(); for (String geoLocationName : app.getGeoLocationNames()) { GeoLocations geoLocations = app.getGeoLocations(geoLocationName); MapMarker lastMarker = null; - for (LatLong latLong : geoLocations) { - MapMarker mapMarker = new MapMarker(latLong); + for (GeoLocated geoLoc : geoLocations) { + MapMarker mapMarker = new MapMarker(geoLoc.getLatLong()); markersParent.getItems().add(mapMarker); if (geoLocations.isPath() && lastMarker != null) { MapPathLine pathLine = new MapPathLine(lastMarker, mapMarker); @@ -62,10 +71,10 @@ public class FxAppController { } lastMarker = mapMarker; } - geoLocationsSelector.getItems().add(geoLocationName); + geoLocationsSelector.getItems().add(geoLocationName + " (" + geoLocations.size() + ")"); } LatLong center = getCenter(null); - System.out.println("Map markers initialized"); + mapView.setCenter(new Location(center.latitude, center.longitude)); } private LatLong getCenter(GeoLocations geoLocations) { @@ -79,8 +88,8 @@ public class FxAppController { if (names != null) { geoLocations = app.getGeoLocations(names.next()); } - for (LatLong latLong : geoLocations) { - double lat = latLong.latitude, lon = latLong.longitude; + for (GeoLocated geoLoc : geoLocations) { + double lat = geoLoc.getLatitude(), lon = geoLoc.getLongitude(); latSum += lat; lonSum += lon; num++; @@ -91,4 +100,9 @@ public class FxAppController { } return new LatLong(latSum / num, lonSum / num); } + + @Override + public void geoLocationsUpdated(GeoLocations geoLocations) { + initMapMarkers(); + } } diff --git a/tdt4140-gr1800/app.ui/src/main/resources/tdt4140/gr1800/app/ui/FileMenu.fxml b/tdt4140-gr1800/app.ui/src/main/resources/tdt4140/gr1800/app/ui/FileMenu.fxml index 9b1209b..9515c49 100644 --- a/tdt4140-gr1800/app.ui/src/main/resources/tdt4140/gr1800/app/ui/FileMenu.fxml +++ b/tdt4140-gr1800/app.ui/src/main/resources/tdt4140/gr1800/app/ui/FileMenu.fxml @@ -3,14 +3,18 @@ <?import java.lang.*?> <?import javafx.scene.control.Menu?> <?import javafx.scene.control.MenuItem?> +<?import javafx.scene.control.SeparatorMenuItem?> <Menu xmlns:fx="http://javafx.com/fxml" text="File" fx:controller="tdt4140.gr1800.app.ui.FileMenuController"> <items> <MenuItem text="New" onAction="#handleNewAction"/> <MenuItem text="Open..." onAction="#handleOpenAction"/> <Menu fx:id="recentMenu" text="Open Recent"/> + <SeparatorMenuItem/> <MenuItem text="Save" onAction="#handleSaveAction"/> <MenuItem text="Save As..." onAction="#handleSaveAsAction"/> <MenuItem text="Save Copy As..." onAction="#handleSaveCopyAsAction"/> + <SeparatorMenuItem/> + <MenuItem text="Import..." onAction="#handleImportAction"/> </items> </Menu> -- GitLab