/* * This is part of Geomajas, a GIS framework, http://www.geomajas.org/. * * Copyright 2008-2015 Geosparc nv, http://www.geosparc.com/, Belgium. * * The program is available in open source according to the GNU Affero * General Public License. All contributions in this program are covered * by the Geomajas Contributors License Agreement. For full licensing * details, see LICENSE.txt in the project root. */ package org.geomajas.gwt.client.map.store; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.geomajas.command.CommandResponse; import org.geomajas.command.dto.GetRasterTilesRequest; import org.geomajas.command.dto.GetRasterTilesResponse; import org.geomajas.gwt.client.command.CommandCallback; import org.geomajas.gwt.client.command.Deferred; import org.geomajas.gwt.client.command.GwtCommand; import org.geomajas.gwt.client.command.GwtCommandDispatcher; import org.geomajas.gwt.client.map.MapViewState; import org.geomajas.gwt.client.map.cache.tile.RasterTile; import org.geomajas.gwt.client.map.cache.tile.TileFunction; import org.geomajas.gwt.client.map.layer.RasterLayer; import org.geomajas.gwt.client.spatial.Bbox; import org.geomajas.gwt.client.spatial.Matrix; import org.geomajas.layer.tile.TileCode; /** * A raster layer store that keeps tiles in cache while panning, only clearing them when a non-panning move occurs, see * {@link org.geomajas.gwt.client.map.MapView}. * * @author Jan De Moerloose */ public class DefaultRasterLayerStore implements RasterLayerStore { private RasterLayer rasterLayer; private Map<TileCode, RasterTile> tiles = new HashMap<TileCode, RasterTile>(); private boolean dirty; private MapViewState lastViewState; private Bbox tileBounds; private Deferred deferred; public DefaultRasterLayerStore(RasterLayer rasterLayer) { this.rasterLayer = rasterLayer; } public void applyAndSync(Bbox bounds, TileFunction<RasterTile> onDelete, TileFunction<RasterTile> onUpdate) { MapViewState viewState = rasterLayer.getMapModel().getMapView().getViewState(); boolean panning = lastViewState == null || viewState.isPannableFrom(lastViewState); if (!panning || isDirty()) { if (deferred != null) { deferred.cancel(); } for (RasterTile tile : tiles.values()) { onDelete.execute(tile); } tiles.clear(); tileBounds = null; dirty = false; } lastViewState = rasterLayer.getMapModel().getMapView().getViewState(); if (tileBounds == null || !tileBounds.contains(bounds)) { fetchAndUpdateTiles(bounds, onUpdate); } else { updateTiles(bounds, onUpdate); } } public RasterLayer getLayer() { return rasterLayer; } public void clear() { if (tiles.size() > 0 || deferred != null) { dirty = true; if (deferred != null) { deferred.cancel(); } } } public boolean isDirty() { return dirty; } public Collection<RasterTile> getTiles() { return tiles.values(); } private void fetchAndUpdateTiles(Bbox bounds, final TileFunction<RasterTile> onUpdate) { // fetch a bigger area to avoid server requests while panning tileBounds = bounds.scale(3); GetRasterTilesRequest request = new GetRasterTilesRequest(); request.setBbox(new org.geomajas.geometry.Bbox(tileBounds.getX(), tileBounds.getY(), tileBounds.getWidth(), tileBounds.getHeight())); request.setCrs(getLayer().getMapModel().getCrs()); request.setLayerId(getLayer().getServerLayerId()); request.setScale(getLayer().getMapModel().getMapView().getCurrentScale()); GwtCommand command = new GwtCommand(GetRasterTilesRequest.COMMAND); command.setCommandRequest(request); RasterCallBack callBack = new RasterCallBack(worldToPan(bounds), onUpdate); deferred = GwtCommandDispatcher.getInstance().execute(command, callBack); } private void updateTiles(Bbox bounds, final TileFunction<RasterTile> onUpdate) { Bbox panBounds = worldToPan(bounds); for (RasterTile tile : tiles.values()) { if (panBounds.intersects(tile.getBounds())) { onUpdate.execute(tile); } } } private Bbox worldToPan(Bbox bounds) { Matrix t = rasterLayer.getMapModel().getMapView().getWorldToPanTransformation(); return bounds.transform(t); } private void addTiles(List<org.geomajas.layer.tile.RasterTile> images) { Matrix t = rasterLayer.getMapModel().getMapView().getWorldToPanTranslation(); Bbox cacheBounds = null; // flag and reference tile to realign the grid when new tiles come in (transformation shift!) boolean newTiles = false; RasterTile referenceTile = null; for (org.geomajas.layer.tile.RasterTile image : images) { TileCode code = image.getCode().clone(); if (!tiles.containsKey(code)) { Bbox panBounds = new Bbox(image.getBounds()); panBounds.translate(Math.round(t.getDx()), Math.round(t.getDy())); if (cacheBounds == null) { cacheBounds = panBounds; } else { cacheBounds = cacheBounds.union(panBounds); } RasterTile tile = new RasterTile(code, panBounds, image.getUrl(), this); tiles.put(code, tile); newTiles = true; referenceTile = tile; } } // This realigns the grid of tiles based on their code if (newTiles) { for (RasterTile tile : tiles.values()) { if (!tile.getCode().equals(referenceTile.getCode())) { Bbox aligned = new Bbox(referenceTile.getBounds()); aligned.setX(referenceTile.getBounds().getX() + (tile.getCode().getX() - referenceTile.getCode().getX()) * aligned.getWidth()); if (tile.getCode().getY() != referenceTile.getCode().getY()) { aligned.setY(referenceTile.getBounds().getY() + getOrientedJDiff(referenceTile, tile) * aligned.getHeight()); } tile.setBounds(aligned); } } } deferred = null; } /** * Returns the difference in j index, taking orientation of y-axis into account. Some layers (WMS 1.8.0) have * different j-index orientation than screen coordinates (lower-left = (0,0) vs upper-left = (0,0)). * * @param tile1 tile * @param tile2 tile * @return +/-(j2-j1) */ private int getOrientedJDiff(RasterTile tile1, RasterTile tile2) { double dy = tile2.getBounds().getY() - tile1.getBounds().getY(); int dj = tile2.getCode().getY() - tile1.getCode().getY(); return (dj * dy) > 0 ? dj : -dj; } /** * Raster layer callback for fetching tiles. */ private final class RasterCallBack implements CommandCallback { private TileFunction<RasterTile> callback; private Bbox bounds; private RasterCallBack(Bbox bounds, TileFunction<RasterTile> callback) { this.callback = callback; this.bounds = bounds; } public void execute(CommandResponse response) { GetRasterTilesResponse r = (GetRasterTilesResponse) response; addTiles(r.getRasterData()); for (RasterTile tile : tiles.values()) { if (bounds.intersects(tile.getBounds())) { callback.execute(tile); } } } } }