api: UrlTileSource

- use replacement string for tilePath
- move 'low-level' formatTilePath to LwHttp
- implement LwHttpFactory
This commit is contained in:
Hannes Janetzek
2014-04-01 03:25:00 +02:00
parent d709d7f39a
commit 9c4e04c4d6
12 changed files with 178 additions and 188 deletions

View File

@@ -22,27 +22,29 @@ import java.util.Map;
import org.oscim.core.MapElement; import org.oscim.core.MapElement;
import org.oscim.core.Tag; import org.oscim.core.Tag;
import org.oscim.tiling.ITileDataSource; import org.oscim.tiling.ITileDataSource;
import org.oscim.tiling.source.LwHttp;
import org.oscim.tiling.source.UrlTileDataSource; import org.oscim.tiling.source.UrlTileDataSource;
import org.oscim.tiling.source.UrlTileSource; import org.oscim.tiling.source.UrlTileSource;
public abstract class GeoJsonTileSource extends UrlTileSource { public abstract class GeoJsonTileSource extends UrlTileSource {
public GeoJsonTileSource(String url) { public GeoJsonTileSource(String url) {
super(url); super(url, "/{Z}/{X}/{Y}.json");
setExtension(".json"); Map<String, String> opt = new HashMap<String, String>();
opt.put("Accept-Encoding", "gzip");
setHttpRequestHeaders(opt);
} }
public GeoJsonTileSource(String url, int zoomMin, int zoomMax) { public GeoJsonTileSource(String url, int zoomMin, int zoomMax) {
super(url, zoomMin, zoomMax); super(url, "/{Z}/{X}/{Y}.json", zoomMin, zoomMax);
setExtension(".json"); Map<String, String> opt = new HashMap<String, String>();
opt.put("Accept-Encoding", "gzip");
setHttpRequestHeaders(opt);
} }
@Override @Override
public ITileDataSource getDataSource() { public ITileDataSource getDataSource() {
Map<String, String> opt = new HashMap<String, String>();
opt.put("Accept-Encoding", "gzip"); return new UrlTileDataSource(this, new GeoJsonTileDecoder(this), getHttpEngine());
return new UrlTileDataSource(this, new GeoJsonTileDecoder(this), new LwHttp(getUrl(), opt));
} }
public Tag getFeatureTag() { public Tag getFeatureTag() {

View File

@@ -18,22 +18,21 @@ package org.oscim.tiling.source.mapnik;
import org.oscim.core.Tile; import org.oscim.core.Tile;
import org.oscim.tiling.ITileDataSource; import org.oscim.tiling.ITileDataSource;
import org.oscim.tiling.source.LwHttp;
import org.oscim.tiling.source.UrlTileDataSource; import org.oscim.tiling.source.UrlTileDataSource;
import org.oscim.tiling.source.UrlTileSource; import org.oscim.tiling.source.UrlTileSource;
public class MapnikVectorTileSource extends UrlTileSource { public class MapnikVectorTileSource extends UrlTileSource {
public MapnikVectorTileSource() { public MapnikVectorTileSource() {
super("http://d1s11ojcu7opje.cloudfront.net/dev/764e0b8d"); super("http://d1s11ojcu7opje.cloudfront.net/dev/764e0b8d", "");
} }
@Override @Override
public ITileDataSource getDataSource() { public ITileDataSource getDataSource() {
return new UrlTileDataSource(this, new TileDecoder(), new LwHttp(getUrl())); return new UrlTileDataSource(this, new TileDecoder(), getHttpEngine());
} }
public int formatTilePath(Tile tile, byte[] path, int pos) { public String formatTilePath(Tile tile) {
// url formatter for mapbox streets // url formatter for mapbox streets
byte[] hexTable = { byte[] hexTable = {
'0', '1', '2', '3', '0', '1', '2', '3',
@@ -41,17 +40,17 @@ public class MapnikVectorTileSource extends UrlTileSource {
'8', '9', 'a', 'b', '8', '9', 'a', 'b',
'c', 'd', 'e', 'f' 'c', 'd', 'e', 'f'
}; };
StringBuilder sb = new StringBuilder();
sb.append('/');
sb.append(hexTable[(tile.tileX) % 16]);
sb.append(hexTable[(tile.tileY) % 16]);
sb.append('/');
sb.append(tile.zoomLevel);
sb.append('/');
sb.append(tile.tileX);
sb.append('/');
sb.append(tile.tileY);
path[pos++] = '/'; return sb.toString();
path[pos++] = hexTable[(tile.tileX) % 16];
path[pos++] = hexTable[(tile.tileY) % 16];
path[pos++] = '/';
pos = LwHttp.writeInt(tile.zoomLevel, pos, path);
path[pos++] = '/';
pos = LwHttp.writeInt(tile.tileX, pos, path);
path[pos++] = '/';
pos = LwHttp.writeInt(tile.tileY, pos, path);
return pos;
} }
} }

View File

@@ -17,7 +17,6 @@
package org.oscim.tiling.source.oscimap; package org.oscim.tiling.source.oscimap;
import org.oscim.tiling.ITileDataSource; import org.oscim.tiling.ITileDataSource;
import org.oscim.tiling.source.LwHttp;
import org.oscim.tiling.source.UrlTileDataSource; import org.oscim.tiling.source.UrlTileDataSource;
import org.oscim.tiling.source.UrlTileSource; import org.oscim.tiling.source.UrlTileSource;
@@ -28,13 +27,11 @@ import org.oscim.tiling.source.UrlTileSource;
public class OSciMap1TileSource extends UrlTileSource { public class OSciMap1TileSource extends UrlTileSource {
public OSciMap1TileSource(String url) { public OSciMap1TileSource(String url) {
super(url); super(url, "/{Z}/{X}/{Y}.osmtile");
setExtension(".osmtile");
setMimeType("application/osmtile");
} }
@Override @Override
public ITileDataSource getDataSource() { public ITileDataSource getDataSource() {
return new UrlTileDataSource(this, new TileDecoder(), new LwHttp(getUrl())); return new UrlTileDataSource(this, new TileDecoder(), getHttpEngine());
} }
} }

View File

@@ -27,7 +27,6 @@ import org.oscim.core.TagSet;
import org.oscim.core.Tile; import org.oscim.core.Tile;
import org.oscim.tiling.ITileDataSink; import org.oscim.tiling.ITileDataSink;
import org.oscim.tiling.ITileDataSource; import org.oscim.tiling.ITileDataSource;
import org.oscim.tiling.source.LwHttp;
import org.oscim.tiling.source.PbfDecoder; import org.oscim.tiling.source.PbfDecoder;
import org.oscim.tiling.source.UrlTileDataSource; import org.oscim.tiling.source.UrlTileDataSource;
import org.oscim.tiling.source.UrlTileSource; import org.oscim.tiling.source.UrlTileSource;
@@ -37,14 +36,12 @@ import org.slf4j.LoggerFactory;
public class OSciMap2TileSource extends UrlTileSource { public class OSciMap2TileSource extends UrlTileSource {
public OSciMap2TileSource(String url) { public OSciMap2TileSource(String url) {
super(url); super(url, "/{Z}/{X}/{Y}.osmtile");
setExtension(".osmtile");
setMimeType("application/osmtile");
} }
@Override @Override
public ITileDataSource getDataSource() { public ITileDataSource getDataSource() {
return new UrlTileDataSource(this, new TileDecoder(), new LwHttp(getUrl())); return new UrlTileDataSource(this, new TileDecoder(), getHttpEngine());
} }
static class TileDecoder extends PbfDecoder { static class TileDecoder extends PbfDecoder {

View File

@@ -10,7 +10,7 @@ public interface HttpEngine {
InputStream read() throws IOException; InputStream read() throws IOException;
boolean sendRequest(UrlTileSource tileSource, Tile tile) throws IOException; boolean sendRequest(Tile tile) throws IOException;
void close(); void close();
@@ -19,6 +19,7 @@ public interface HttpEngine {
boolean requestCompleted(boolean success); boolean requestCompleted(boolean success);
public interface Factory { public interface Factory {
public HttpEngine create(); HttpEngine create(UrlTileSource tileSource);
} }
} }

View File

@@ -24,7 +24,6 @@ import java.net.InetSocketAddress;
import java.net.Socket; import java.net.Socket;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.net.URL; import java.net.URL;
import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import org.oscim.core.Tile; import org.oscim.core.Tile;
@@ -41,6 +40,8 @@ public class LwHttp implements HttpEngine {
static final Logger log = LoggerFactory.getLogger(LwHttp.class); static final Logger log = LoggerFactory.getLogger(LwHttp.class);
static final boolean dbg = false; static final boolean dbg = false;
private final UrlTileSource mTileSource;
private final static byte[] HEADER_HTTP_OK = "200 OK".getBytes(); private final static byte[] HEADER_HTTP_OK = "200 OK".getBytes();
//private final static byte[] HEADER_CONTENT_TYPE = "Content-Type".getBytes(); //private final static byte[] HEADER_CONTENT_TYPE = "Content-Type".getBytes();
private final static byte[] HEADER_CONTENT_LENGTH = "Content-Length".getBytes(); private final static byte[] HEADER_CONTENT_LENGTH = "Content-Length".getBytes();
@@ -69,16 +70,9 @@ public class LwHttp implements HttpEngine {
private final byte[] REQUEST_GET_END; private final byte[] REQUEST_GET_END;
private final byte[] mRequestBuffer; private final byte[] mRequestBuffer;
/** private LwHttp(UrlTileSource tileSource) {
* @param url mTileSource = tileSource;
* Base url for tiles URL url = tileSource.getUrl();
*/
public LwHttp(URL url) {
this(url, null);
}
public LwHttp(URL url, Map<String, String> header) {
int port = url.getPort(); int port = url.getPort();
if (port < 0) if (port < 0)
port = 80; port = 80;
@@ -90,9 +84,9 @@ public class LwHttp implements HttpEngine {
REQUEST_GET_START = ("GET " + path).getBytes(); REQUEST_GET_START = ("GET " + path).getBytes();
String addRequest = ""; String addRequest = "";
if (header != null) { if (tileSource.getRequestHeader() != null) {
StringBuffer sb = new StringBuffer(); StringBuffer sb = new StringBuffer();
for (Entry<String, String> l : header.entrySet()) for (Entry<String, String> l : tileSource.getRequestHeader().entrySet())
sb.append('\n').append(l.getKey()).append(": ").append(l.getValue()); sb.append('\n').append(l.getKey()).append(": ").append(l.getValue());
addRequest = sb.toString(); addRequest = sb.toString();
} }
@@ -330,7 +324,7 @@ public class LwHttp implements HttpEngine {
} }
@Override @Override
public boolean sendRequest(UrlTileSource tileSource, Tile tile) throws IOException { public boolean sendRequest(Tile tile) throws IOException {
if (mSocket != null) { if (mSocket != null) {
if (mMaxReq-- <= 0) if (mMaxReq-- <= 0)
@@ -359,7 +353,7 @@ public class LwHttp implements HttpEngine {
byte[] request = mRequestBuffer; byte[] request = mRequestBuffer;
int pos = REQUEST_GET_START.length; int pos = REQUEST_GET_START.length;
pos = tileSource.formatTilePath(tile, request, pos); pos = formatTilePath(mTileSource, tile, request, pos);
int len = REQUEST_GET_END.length; int len = REQUEST_GET_END.length;
System.arraycopy(REQUEST_GET_END, 0, request, pos, len); System.arraycopy(REQUEST_GET_END, 0, request, pos, len);
@@ -497,4 +491,46 @@ public class LwHttp implements HttpEngine {
return true; return true;
} }
/**
* Write tile url - the low level, no-allocations method,
*
* override getTileUrl() for custom url formatting using
* Strings
*
* @param tile the Tile
* @param buf to write url string
* @param pos current position
* @return new position
*/
public int formatTilePath(UrlTileSource tileSource, Tile tile, byte[] buf, int pos) {
String p = tileSource.formatTilePath(tile);
log.debug("path {}", p);
//if (p != null) {
byte[] b = p.getBytes();
System.arraycopy(b, 0, buf, pos, b.length);
return pos + b.length;
//}
//
// buf[pos++] = '/';
// pos = LwHttp.writeInt(tile.zoomLevel, pos, buf);
// buf[pos++] = '/';
// pos = LwHttp.writeInt(tile.tileX, pos, buf);
// buf[pos++] = '/';
// pos = LwHttp.writeInt(tile.tileY, pos, buf);
// byte[] ext = tileSource.mExtBytes;
// if (ext == null)
// return pos;
//
// System.arraycopy(ext, 0, buf, pos, ext.length);
// return pos + ext.length;
}
public static class LwHttpFactory implements HttpEngine.Factory {
@Override
public HttpEngine create(UrlTileSource tileSource) {
return new LwHttp(tileSource);
}
}
} }

View File

@@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import org.oscim.core.Tile; import org.oscim.core.Tile;
@@ -11,25 +12,27 @@ import org.oscim.core.Tile;
import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.OkHttpClient;
public class OkHttpEngine implements HttpEngine { public class OkHttpEngine implements HttpEngine {
private final OkHttpClient client; private final OkHttpClient mClient;
private final UrlTileSource mTileSource;
public static class OkHttpFactory implements HttpEngine.Factory { public static class OkHttpFactory implements HttpEngine.Factory {
private final OkHttpClient client; private final OkHttpClient mClient;
public OkHttpFactory() { public OkHttpFactory() {
this.client = new OkHttpClient(); mClient = new OkHttpClient();
} }
@Override @Override
public HttpEngine create() { public HttpEngine create(UrlTileSource tileSource) {
return new OkHttpEngine(client); return new OkHttpEngine(mClient, tileSource);
} }
} }
private InputStream inputStream; private InputStream inputStream;
public OkHttpEngine(OkHttpClient client) { public OkHttpEngine(OkHttpClient client, UrlTileSource tileSource) {
this.client = client; mClient = client;
mTileSource = tileSource;
} }
@Override @Override
@@ -37,28 +40,20 @@ public class OkHttpEngine implements HttpEngine {
return inputStream; return inputStream;
} }
HttpURLConnection openConnection(Tile tile) throws MalformedURLException {
return mClient.open(new URL(mTileSource.getUrl() +
mTileSource.formatTilePath(tile)));
}
@Override @Override
public boolean sendRequest(UrlTileSource tileSource, Tile tile) throws IOException { public boolean sendRequest(Tile tile) throws IOException {
if (tile == null) { if (tile == null) {
throw new IllegalArgumentException("Tile cannot be null."); throw new IllegalArgumentException("Tile cannot be null.");
} }
final URL requestUrl = new URL(tileSource.getUrl() final HttpURLConnection connection = openConnection(tile);
+ "/"
+ Byte.toString(tile.zoomLevel)
+ "/"
+ tile.tileX
+ "/"
+ tile.tileY
+ ".vtm");
final HttpURLConnection connection = client.open(requestUrl); inputStream = connection.getInputStream();
try {
inputStream = connection.getInputStream();
} catch (Exception e) {
e.printStackTrace();
}
return true; return true;
} }

View File

@@ -75,7 +75,7 @@ public class UrlTileDataSource implements ITileDataSource {
TileWriter cacheWriter = null; TileWriter cacheWriter = null;
try { try {
InputStream is; InputStream is;
if (!mConn.sendRequest(mTileSource, tile)) { if (!mConn.sendRequest(tile)) {
log.debug("{} Request failed", tile); log.debug("{} Request failed", tile);
} else if ((is = mConn.read()) == null) { } else if ((is = mConn.read()) == null) {
log.debug("{} Network Error", tile); log.debug("{} Network Error", tile);

View File

@@ -18,30 +18,44 @@ package org.oscim.tiling.source;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.Map;
import org.oscim.core.Tile; import org.oscim.core.Tile;
import org.oscim.tiling.TileSource; import org.oscim.tiling.TileSource;
import org.oscim.tiling.source.LwHttp.LwHttpFactory;
public abstract class UrlTileSource extends TileSource { public abstract class UrlTileSource extends TileSource {
private final URL mUrl; private final URL mUrl;
private byte[] mExt; private final String[] mTilePath;
private HttpEngine.Factory mHttpFactory;
private HttpEngine.Factory mHttpFactory;
private Map<String, String> mRequestHeaders;
public UrlTileSource(String url, String tilePath, int zoomMin, int zoomMax) {
this(url, tilePath);
mZoomMin = zoomMin;
mZoomMax = zoomMax;
}
/**
* @param urlString 'http://example.com/'
* @param tilePath replacement string for tile coordinates,
* e.g. '{Z}/{X}/{Y}.png'
*/
public UrlTileSource(String urlString, String tilePath) {
if (tilePath == null)
throw new IllegalArgumentException("tilePath cannot be null.");
public UrlTileSource(String urlString) {
URL url = null; URL url = null;
try { try {
url = new URL(urlString); url = new URL(urlString);
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
e.printStackTrace(); throw new IllegalArgumentException(e);
} }
mUrl = url; mUrl = url;
} mTilePath = tilePath.split("\\{|\\}");
public UrlTileSource(String url, int zoomMin, int zoomMax) {
this(url);
mZoomMin = zoomMin;
mZoomMax = zoomMax;
} }
@Override @Override
@@ -54,70 +68,52 @@ public abstract class UrlTileSource extends TileSource {
} }
protected void setExtension(String ext) {
if (ext == null) {
mExt = null;
return;
}
mExt = ext.getBytes();
}
protected void setMimeType(String string) {
}
/**
* Create url path for tile
*/
protected String getTileUrl(Tile tile) {
return null;
}
/**
* Write tile url - the low level, no-allocations method,
*
* override getTileUrl() for custom url formatting using
* Strings
*
* @param tile the Tile
* @param buf to write url string
* @param pos current position
* @return new position
*/
public int formatTilePath(Tile tile, byte[] buf, int pos) {
String p = getTileUrl(tile);
if (p != null) {
byte[] b = p.getBytes();
System.arraycopy(b, 0, buf, pos, b.length);
return pos + b.length;
}
buf[pos++] = '/';
pos = LwHttp.writeInt(tile.zoomLevel, pos, buf);
buf[pos++] = '/';
pos = LwHttp.writeInt(tile.tileX, pos, buf);
buf[pos++] = '/';
pos = LwHttp.writeInt(tile.tileY, pos, buf);
if (mExt == null)
return pos;
System.arraycopy(mExt, 0, buf, pos, mExt.length);
return pos + mExt.length;
}
public URL getUrl() { public URL getUrl() {
return mUrl; return mUrl;
} }
public String formatTilePath(Tile tile) {
// TODO only use the actual replacement positions.
StringBuilder sb = new StringBuilder();
for (String b : mTilePath) {
if (b.length() == 1) {
switch (b.charAt(0)) {
case 'X':
sb.append(tile.tileX);
continue;
case 'Y':
sb.append(tile.tileY);
continue;
case 'Z':
sb.append(tile.zoomLevel);
continue;
default:
break;
}
}
sb.append(b);
}
return sb.toString();
}
public void setHttpEngine(HttpEngine.Factory httpFactory) { public void setHttpEngine(HttpEngine.Factory httpFactory) {
mHttpFactory = httpFactory; mHttpFactory = httpFactory;
} }
public void setHttpRequestHeaders(Map<String, String> options) {
mRequestHeaders = options;
}
protected Map<String, String> getRequestHeader() {
return mRequestHeaders;
}
public HttpEngine getHttpEngine() { public HttpEngine getHttpEngine() {
if (mHttpFactory == null) { if (mHttpFactory == null) {
return new LwHttp(getUrl()); mHttpFactory = new LwHttpFactory();
} }
return mHttpFactory.create(); return mHttpFactory.create(this);
} }
} }

View File

@@ -26,8 +26,15 @@ public class BitmapTileSource extends UrlTileSource {
* implement getUrlString() for custom formatting. * implement getUrlString() for custom formatting.
*/ */
public BitmapTileSource(String url, int zoomMin, int zoomMax) { public BitmapTileSource(String url, int zoomMin, int zoomMax) {
super(url, zoomMin, zoomMax); super(url, "/{Z}/{X}/{Y}.png", zoomMin, zoomMax);
setExtension(".png"); }
public BitmapTileSource(String url, int zoomMin, int zoomMax, String extension) {
super(url, "/{Z}/{X}/{Y}" + extension, zoomMin, zoomMax);
}
public BitmapTileSource(String url, String tilePath, int zoomMin, int zoomMax) {
super(url, tilePath, zoomMin, zoomMax);
} }
@Override @Override

View File

@@ -1,6 +1,5 @@
package org.oscim.tiling.source.bitmap; package org.oscim.tiling.source.bitmap;
import org.oscim.core.Tile;
import org.oscim.layers.tile.bitmap.BitmapTileLayer.FadeStep; import org.oscim.layers.tile.bitmap.BitmapTileLayer.FadeStep;
/** /**
@@ -35,15 +34,13 @@ public class DefaultSources {
public static class ImagicoLandcover extends BitmapTileSource { public static class ImagicoLandcover extends BitmapTileSource {
public ImagicoLandcover() { public ImagicoLandcover() {
super("http://www.imagico.de/map/tiles/landcover", 0, 6); super("http://www.imagico.de/map/tiles/landcover", 0, 6, ".jpg");
setExtension(".jpg");
} }
} }
public static class MapQuestAerial extends BitmapTileSource { public static class MapQuestAerial extends BitmapTileSource {
public MapQuestAerial() { public MapQuestAerial() {
super("http://otile1.mqcdn.com/tiles/1.0.0/sat", 0, 8); super("http://otile1.mqcdn.com/tiles/1.0.0/sat", 0, 8, ".jpg");
setExtension(".jpg");
} }
@Override @Override
@@ -64,38 +61,17 @@ public class DefaultSources {
} }
public static class ArcGISWorldShaded extends BitmapTileSource { public static class ArcGISWorldShaded extends BitmapTileSource {
private final StringBuilder sb = new StringBuilder(32);
public ArcGISWorldShaded() { public ArcGISWorldShaded() {
super("http://server.arcgisonline.com/ArcGIS/rest/services", 0, 13); super("http://server.arcgisonline.com/ArcGIS/rest/services" +
} "/World_Shaded_Relief/MapServer/tile/",
"{Z}/{Y}/{X}", 0, 13);
@Override
public synchronized String getTileUrl(Tile tile) {
sb.setLength(0);
//sb.append("/World_Imagery/MapServer/tile/");
sb.append("/World_Shaded_Relief/MapServer/tile/");
sb.append(tile.zoomLevel);
sb.append('/').append(tile.tileY);
sb.append('/').append(tile.tileX);
return sb.toString();
} }
} }
public static class HillShadeHD extends BitmapTileSource { public static class HillShadeHD extends BitmapTileSource {
private final StringBuilder sb = new StringBuilder(32);
public HillShadeHD() { public HillShadeHD() {
super("http://129.206.74.245:8004/tms_hs.ashx", 2, 16); super("http://129.206.74.245:8004/tms_hs.ashx",
} "?x={X}&y={Y}&z={Z}", 2, 16);
@Override
public synchronized String getTileUrl(Tile tile) {
sb.setLength(0);
sb.append("?x=").append(tile.tileX);
sb.append("&y=").append(tile.tileY);
sb.append("&z=").append(tile.zoomLevel);
return sb.toString();
} }
} }
@@ -104,23 +80,8 @@ public class DefaultSources {
* https://developers.google.com/maps/faq * https://developers.google.com/maps/faq
*/ */
public static class GoogleMaps extends BitmapTileSource { public static class GoogleMaps extends BitmapTileSource {
private final StringBuilder sb = new StringBuilder(60);
public GoogleMaps(String hostName) { public GoogleMaps(String hostName) {
super(hostName, 1, 20); //jpeg for sat super(hostName, "/vt/x={X}&y={Y}&z={Z}&s=Galileo&scale=2", 1, 20); //jpeg for sat
}
@Override
public synchronized String getTileUrl(Tile tile) {
sb.setLength(0);
sb.append("/vt/x="); //lyrs=y&
sb.append(tile.tileX);
sb.append("&y=");
sb.append(tile.tileY);
sb.append("&z=");
sb.append(tile.zoomLevel);
sb.append("&s=Galileo&scale=2");
return sb.toString();
} }
} }

View File

@@ -27,8 +27,7 @@ public class OSciMap4TileSource extends UrlTileSource {
} }
public OSciMap4TileSource(String url) { public OSciMap4TileSource(String url) {
super(url); super(url, "/{Z}/{X}/{Y}.vtm");
setExtension(".vtm");
} }
@Override @Override