diff --git a/maps/src/main/java/com/gluonhq/impl/maps/BaseMap.java b/maps/src/main/java/com/gluonhq/impl/maps/BaseMap.java index 79125d4..aa5e4c5 100644 --- a/maps/src/main/java/com/gluonhq/impl/maps/BaseMap.java +++ b/maps/src/main/java/com/gluonhq/impl/maps/BaseMap.java @@ -29,6 +29,7 @@ import com.gluonhq.maps.MapPoint; import com.gluonhq.maps.MapView; +import com.gluonhq.maps.tile.TileRetriever; import javafx.application.Platform; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; @@ -95,7 +96,10 @@ public class BaseMap extends Group { private final ChangeListener resizeListener = (o, oldValue, newValue) -> markDirty(); private ChangeListener sceneListener; - public BaseMap() { + public TileRetriever tileRetriever; + + public BaseMap(TileRetriever tileRetriever) { + this.tileRetriever = tileRetriever; for (int i = 0; i < tiles.length; i++) { tiles[i] = new HashMap<>(); } diff --git a/maps/src/main/java/com/gluonhq/impl/maps/MapTile.java b/maps/src/main/java/com/gluonhq/impl/maps/MapTile.java index 3ec06cb..1ed27d5 100644 --- a/maps/src/main/java/com/gluonhq/impl/maps/MapTile.java +++ b/maps/src/main/java/com/gluonhq/impl/maps/MapTile.java @@ -76,7 +76,7 @@ public boolean isCovering() { getTransforms().add(scale); debug("[JVDBG] load image [" + myZoom + "], i = " + i + ", j = " + j); - final TileImageView imageView = new TileImageView(myZoom, i, j); + final TileImageView imageView = new TileImageView(baseMap.tileRetriever,myZoom, i, j); imageView.exceptionProperty().addListener((obs, ov, nv) -> logger.info("Error: " + nv.getMessage())); imageView.setMouseTransparent(true); progress = imageView.progressProperty(); @@ -165,4 +165,4 @@ public void invalidated(Observable o) { private void debug(String s) { logger.fine("LOG " + System.currentTimeMillis() % 10000 + ": " + s); } -} +} \ No newline at end of file diff --git a/maps/src/main/java/com/gluonhq/impl/maps/TileImageView.java b/maps/src/main/java/com/gluonhq/impl/maps/TileImageView.java index 8d1f1a6..c76f5ba 100644 --- a/maps/src/main/java/com/gluonhq/impl/maps/TileImageView.java +++ b/maps/src/main/java/com/gluonhq/impl/maps/TileImageView.java @@ -28,7 +28,7 @@ package com.gluonhq.impl.maps; import com.gluonhq.maps.tile.TileRetriever; -import com.gluonhq.maps.tile.TileRetrieverProvider; + import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyDoubleProperty; @@ -46,14 +46,14 @@ public class TileImageView extends ImageView { private static final Logger logger = Logger.getLogger(TileImageView.class.getName()); - private static final TileRetriever TILE_RETRIEVER = TileRetrieverProvider.getInstance().load(); + - public TileImageView(int zoom, long i, long j) { + public TileImageView(TileRetriever tileRetriever,int zoom, long i, long j) { setFitHeight(256); setFitWidth(256); setPreserveRatio(true); setProgress(0); - CompletableFuture future = TILE_RETRIEVER.loadTile(zoom, i, j); + CompletableFuture future = tileRetriever.loadTile(zoom, i, j); if (!future.isDone()) { Optional.ofNullable(placeholderImageSupplier).ifPresent(s -> setImage(s.get())); logger.fine("start downloading tile " + zoom + "/" + i + "/" + j); @@ -137,4 +137,4 @@ private ReadOnlyDoubleWrapper progressPropertyImpl() { return progress; } -} +} \ No newline at end of file diff --git a/maps/src/main/java/com/gluonhq/impl/maps/tile/osm/CachedOsmTileRetriever.java b/maps/src/main/java/com/gluonhq/impl/maps/tile/osm/CachedOsmTileRetriever.java index 62becd4..49ce589 100644 --- a/maps/src/main/java/com/gluonhq/impl/maps/tile/osm/CachedOsmTileRetriever.java +++ b/maps/src/main/java/com/gluonhq/impl/maps/tile/osm/CachedOsmTileRetriever.java @@ -44,15 +44,17 @@ import java.util.logging.Level; import java.util.logging.Logger; -public class CachedOsmTileRetriever extends OsmTileRetriever { +public abstract class CachedOsmTileRetriever extends OsmTileRetriever { private static final Logger logger = Logger.getLogger(CachedOsmTileRetriever.class.getName()); private static final int TIMEOUT = 10000; private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(2, new DaemonThreadFactory()); - static File cacheRoot; - static boolean hasFileCache; - static { + File cacheRoot; + boolean hasFileCache; + CacheThread cacheThread = null; + + public CachedOsmTileRetriever() { try { File storageRoot = StorageService.create() .flatMap(StorageService::getPrivateStorage) @@ -65,6 +67,9 @@ public class CachedOsmTileRetriever extends OsmTileRetriever { } else { hasFileCache = true; } + if (hasFileCache) { + cacheThread = new CacheThread(); + } logger.fine("hasfilecache = " + hasFileCache); } catch (IOException ex) { hasFileCache = false; @@ -81,7 +86,7 @@ public CompletableFuture loadTile(int zoom, long i, long j) { return CompletableFuture.supplyAsync(() -> { logger.fine("start downloading tile " + zoom + "/" + i + "/" + j); try { - return CacheThread.cacheImage(zoom, i, j); + return cacheThread.cacheImage(zoom, i, j); } catch (IOException e) { throw new RuntimeException("Error " + e.getMessage()); } @@ -89,7 +94,7 @@ public CompletableFuture loadTile(int zoom, long i, long j) { } - static private Image fromFileCache(int zoom, long i, long j) { + private Image fromFileCache(int zoom, long i, long j) { if (!hasFileCache) { return null; } @@ -98,14 +103,14 @@ static private Image fromFileCache(int zoom, long i, long j) { return f.exists() ? new Image(f.toURI().toString(), true) : null; } - private static class CacheThread { + private class CacheThread { - public static Image cacheImage(int zoom, long i, long j) throws IOException { + public Image cacheImage(int zoom, long i, long j) throws IOException { File file = doCache(buildImageUrlString(zoom, i, j), zoom, i, j); return new Image(new FileInputStream(file)); } - private static File doCache(String urlString, int zoom, long i, long j) throws IOException { + private File doCache(String urlString, int zoom, long i, long j) throws IOException { final URLConnection openConnection; URL url = new URL(urlString); openConnection = url.openConnection(); @@ -115,7 +120,7 @@ private static File doCache(String urlString, int zoom, long i, long j) throws I try (InputStream inputStream = openConnection.getInputStream()) { String enc = File.separator + zoom + File.separator + i + File.separator + j + ".png"; logger.fine("retrieve " + urlString + " and store " + enc); - File candidate = new File(cacheRoot, enc); + File candidate = new File(cacheRoot.getPath(), enc); candidate.getParentFile().mkdirs(); try (FileOutputStream fos = new FileOutputStream(candidate)) { byte[] buff = new byte[4096]; @@ -140,4 +145,4 @@ public Thread newThread(final Runnable r) { } } -} +} \ No newline at end of file diff --git a/maps/src/main/java/com/gluonhq/impl/maps/tile/osm/OsmTileRetriever.java b/maps/src/main/java/com/gluonhq/impl/maps/tile/osm/OsmTileRetriever.java index 88c738d..511c0d2 100644 --- a/maps/src/main/java/com/gluonhq/impl/maps/tile/osm/OsmTileRetriever.java +++ b/maps/src/main/java/com/gluonhq/impl/maps/tile/osm/OsmTileRetriever.java @@ -29,12 +29,10 @@ import com.gluonhq.maps.tile.TileRetriever; import javafx.scene.image.Image; - import java.util.concurrent.CompletableFuture; -public class OsmTileRetriever implements TileRetriever { - - private static final String host = "http://tile.openstreetmap.org/"; +public abstract class OsmTileRetriever implements TileRetriever { + static final String httpAgent; static { @@ -46,10 +44,8 @@ public class OsmTileRetriever implements TileRetriever { System.setProperty("http.agent", httpAgent); } - static String buildImageUrlString(int zoom, long i, long j) { - return host + zoom + "/" + i + "/" + j + ".png"; - } - + public abstract String buildImageUrlString(int zoom, long i, long j); + @Override public CompletableFuture loadTile(int zoom, long i, long j) { String urlString = buildImageUrlString(zoom, i, j); diff --git a/maps/src/main/java/com/gluonhq/maps/MapView.java b/maps/src/main/java/com/gluonhq/maps/MapView.java index 705117f..7887c4f 100644 --- a/maps/src/main/java/com/gluonhq/maps/MapView.java +++ b/maps/src/main/java/com/gluonhq/maps/MapView.java @@ -29,6 +29,7 @@ import com.gluonhq.attach.util.Platform; import com.gluonhq.impl.maps.BaseMap; +import com.gluonhq.maps.tile.TileRetriever; import com.gluonhq.impl.maps.TileImageView; import javafx.animation.Animation.Status; import javafx.animation.Interpolator; @@ -36,11 +37,15 @@ import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.geometry.Point2D; +import javafx.scene.control.Label; import javafx.scene.image.Image; import javafx.scene.layout.Region; import javafx.scene.shape.Rectangle; +import javafx.scene.text.Font; import javafx.util.Duration; - +import javafx.scene.layout.BackgroundFill; +import javafx.scene.paint.Color; +import javafx.scene.layout.Background; import java.util.LinkedList; import java.util.List; import java.util.function.Supplier; @@ -61,12 +66,19 @@ public class MapView extends Region { private boolean zooming = false; private boolean enableDragging = false; + private Label label; + /** * Create a MapView component. */ - public MapView() { - baseMap = new BaseMap(); + public MapView(TileRetriever tileRetriever) { + baseMap = new BaseMap(tileRetriever); getChildren().add(baseMap); + label = new Label(tileRetriever.copyright()); + label.getStyleClass().add("label-license"); + getChildren().add(label); + getStylesheets().add(MapView.class.getResource("maps.css").toExternalForm()); + registerInputListeners(); baseMap.centerLat().addListener(o -> markDirty()); @@ -252,6 +264,10 @@ protected void layoutChildren() { } } super.layoutChildren(); + + label.setLayoutX(w - label.getWidth()); + label.setLayoutY(h - label.getHeight()); + dirty = false; // we need to get these values or we won't be notified on new changes diff --git a/maps/src/main/java/com/gluonhq/maps/tile/TileRetriever.java b/maps/src/main/java/com/gluonhq/maps/tile/TileRetriever.java index 6c90eee..23937e7 100644 --- a/maps/src/main/java/com/gluonhq/maps/tile/TileRetriever.java +++ b/maps/src/main/java/com/gluonhq/maps/tile/TileRetriever.java @@ -32,7 +32,12 @@ import java.util.concurrent.CompletableFuture; public interface TileRetriever { - + + /** + * @return Copyright/Attribution info to be overlayed on the map + */ + String copyright(); + /** * Loads a tile at the specified zoom level and coordinates and returns it * as an {@link Image}. @@ -43,5 +48,4 @@ public interface TileRetriever { * @return a completableFuture with the image representing the tile */ CompletableFuture loadTile(int zoom, long i, long j); - } diff --git a/maps/src/main/java/com/gluonhq/maps/tile/TileRetrieverProvider.java b/maps/src/main/java/com/gluonhq/maps/tile/TileRetrieverProvider.java deleted file mode 100644 index 810dd84..0000000 --- a/maps/src/main/java/com/gluonhq/maps/tile/TileRetrieverProvider.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2018, Gluon - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package com.gluonhq.maps.tile; - -import com.gluonhq.impl.maps.tile.osm.CachedOsmTileRetriever; - -import java.util.Iterator; -import java.util.ServiceLoader; - -public class TileRetrieverProvider { - - private static TileRetrieverProvider provider; - public static synchronized TileRetrieverProvider getInstance() { - if (provider == null) { - provider = new TileRetrieverProvider(); - } - return provider; - } - - private final ServiceLoader loader; - - private TileRetrieverProvider() { - loader = ServiceLoader.load(TileRetriever.class); - } - - public TileRetriever load() { - Iterator tileRetrievers = loader.iterator(); - if (tileRetrievers.hasNext()) { - return tileRetrievers.next(); - } else { - return new CachedOsmTileRetriever(); - } - } -} diff --git a/maps/src/main/java/module-info.java b/maps/src/main/java/module-info.java index 1851bdd..48f603a 100644 --- a/maps/src/main/java/module-info.java +++ b/maps/src/main/java/module-info.java @@ -37,4 +37,5 @@ exports com.gluonhq.maps; exports com.gluonhq.maps.tile; + exports com.gluonhq.impl.maps.tile.osm; } \ No newline at end of file diff --git a/maps/src/main/resources/com/gluonhq/maps/maps.css b/maps/src/main/resources/com/gluonhq/maps/maps.css new file mode 100644 index 0000000..9cb026a --- /dev/null +++ b/maps/src/main/resources/com/gluonhq/maps/maps.css @@ -0,0 +1,4 @@ +.label-license { + -fx-background-color: rgba(126, 126, 126, 0.5); + -fx-font-size: 0.9em; +} diff --git a/samples/mobile/src/main/java/com/gluonhq/maps/samples/mobile/MobileSample.java b/samples/mobile/src/main/java/com/gluonhq/maps/samples/mobile/MobileSample.java index f98fa90..88e8b2e 100644 --- a/samples/mobile/src/main/java/com/gluonhq/maps/samples/mobile/MobileSample.java +++ b/samples/mobile/src/main/java/com/gluonhq/maps/samples/mobile/MobileSample.java @@ -52,6 +52,8 @@ import javafx.scene.paint.Color; import javafx.scene.shape.Circle; +import com.gluonhq.impl.maps.tile.osm.CachedOsmTileRetriever; +import com.gluonhq.maps.tile.TileRetriever; public class MobileSample extends Application { private static final Logger LOGGER = Logger.getLogger(MobileSample.class.getName()); @@ -68,7 +70,49 @@ public class MobileSample extends Application { @Override public void start(Stage stage) { - MapView view = new MapView(); + + /* + * In order to display OSM tiles you have to choose a tile provider. Here are some lists: + * + * -http://leaflet-extras.github.io/leaflet-providers/preview/ (fairly complete list with previews) + * -https://wiki.openstreetmap.org/wiki/Tile_servers + * -https://switch2osm.org/providers/#tile-hosting (both free tiers and commercial only) + */ + + /* + * Here is an example for accessing OpenStreetMap server's tiles. + * Please only use for "very limited testing purposes" - @see https://operations.osmfoundation.org/policies/tiles/ + * + */ + TileRetriever osm = new CachedOsmTileRetriever() { + public String buildImageUrlString(int zoom, long i, long j) { + return "http://tile.openstreetmap.org/" +zoom+"/"+ i + "/" + j + ".png"; + } + public String copyright() { + return "Map data © OpenStreetMap contributors, CC-BY-SA. Imagery © OpenStreetMap, for demo only."; + } + }; + /* + * Another example, using MapBox tiles free tier. + * To access MapBox you will need to create an account at https://www.mapbox.com/maps/ and generate an access token. + * + * Please note that the access token here is for demo only and will be replaced at some point - you need to get your own + * + * Styles known to work: satellite-v9,streets-v8 + */ + TileRetriever mapBox = new CachedOsmTileRetriever() { + public String buildImageUrlString(int zoom, long i, long j) { + String token = "pk.eyJ1IjoiYnJ1bmVzdG8iLCJhIjoiY2tpYjRpcWVrMDk3bDJ5azBibGZmYjJ2NyJ9.5mKw_JV1w9-VoAxjn2f9LA"; + String url = "https://api.mapbox.com/styles/v1/mapbox/satellite-v9/tiles/256/"+zoom+"/"+i+"/"+j+"?access_token="+token; + return url; + } + public String copyright() { + return "Map data © OpenStreetMap contributors CC-BY-SA, Imagery © Mapbox"; + } + }; + + MapView view = new MapView(mapBox); + view.addLayer(positionLayer()); view.setZoom(3); Scene scene;