package com.androidol.layer; import java.util.HashMap; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import com.androidol.Map; import com.androidol.basetypes.Pixel; import com.androidol.basetypes.Size; import com.androidol.constants.UtilConstants; import com.androidol.events.Event; import com.androidol.events.LayerEvents; import com.androidol.events.MapEvents; import com.androidol.util.Util; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Envelope; public class Layer extends View implements UtilConstants { // =========================================================== // fields related to layer itself // =========================================================== protected Context context = null; protected String name = ""; protected boolean isBaseLayer = false; public boolean isVector = false; protected boolean alwaysInRange = false; protected boolean visible = true; protected int gutter = 0; protected boolean displayOutsideMaxExtent = false; protected boolean wrapDateLine = false; protected double opacity = 1.0; protected int buffer = 0; protected boolean isOfflineMode = false; protected Map map = null; protected Size imageSize = null; protected Pixel imageOffset = new Pixel(0.0, 0.0); protected boolean inRange = false; protected Bitmap previousSnapshot = null; // =========================================================== // fields related to Map // =========================================================== protected Envelope maxExtent = null; protected double maxResolution = Double.NEGATIVE_INFINITY; protected double minResolution = Double.NEGATIVE_INFINITY; protected int numZoomLevels = 0; protected double[] scales = null; protected double[] resolutions = null; protected String projection = "EPSG:3857"; protected String units = "meters"; protected double maxScale = Double.NEGATIVE_INFINITY; protected double minScale = Double.NEGATIVE_INFINITY; public LayerEvents events = new LayerEvents(); protected LayerEventsHandler layerEventsHandler = new LayerEventsHandler(); protected Canvas canvas = null; protected final Paint paint = new Paint(); public int dragDx = 0; public int dragDy = 0; protected boolean dragging = false; // =========================================================== // Constructors // =========================================================== /** * */ public Layer(Context context) { super(context); this.context = context; // // TODO: give it a random default name this.name = "defaut_name"; // TODO: give default value to other map related parameters if necessary if(this.wrapDateLine == true) { this.displayOutsideMaxExtent = true; } // to register callback handlers on this.events this.events.registerAll(this.layerEventsHandler); // } /** * * @param context * @param attrs */ public Layer(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; // 'name' this.name = attrs.getAttributeValue(ANDROIDOL_NAMESPACE, "name"); //Util.printDebugMessage("...layer name: " + this.name); // 'isBaseLayer' this.isBaseLayer = attrs.getAttributeBooleanValue(ANDROIDOL_NAMESPACE, "isBaseLayer", false); // 'isOfflineMode' this.isOfflineMode = attrs.getAttributeBooleanValue(ANDROIDOL_NAMESPACE, "isOfflineMode", false); //Util.printDebugMessage("...is baselayer: " + this.isBaseLayer); // 'transparency' double opacity = attrs.getAttributeFloatValue(ANDROIDOL_NAMESPACE, "opacity", 1.0f); this.paint.setAlpha((int)(opacity*255)); //Util.printDebugMessage("...layer opacity: " + (int)(opacity*255)); // '' /* this.alwaysInRange = attrs.getAttributeBooleanValue(ANDROIDOL_NAMESPACE, "alwaysInRange", true); Util.printDebugMessage("...is alwaysInRange: " + this.alwaysInRange); */ // // configuration options user could set through constructor: // isBaseLayer // isVector // alwaysInRange // visible // gutter // displayOutsideMaxExtent // wrapDateLine // opacity // buffer // // TODO: apply configurations in options to layer // if(this.wrapDateLine == true) { this.displayOutsideMaxExtent = true; } // to register callback handlers on this.events this.events.registerAll(this.layerEventsHandler); this.setDrawingCacheEnabled(true); } /** * */ @Override public void onDraw(final Canvas canvas) { //Util.printDebugMessage("...layer onDraw() called..."); } // =========================================================== // Public API and private methods // =========================================================== /** * API Method: destroy * * @param setNewBaseLayer */ public void destroy(boolean setNewBaseLayer) { if(this.map != null) { this.map.removeLayer(this, setNewBaseLayer); } this.events.unregisterAll(this.layerEventsHandler); this.map = null; } /** * API Method: onMapResize */ public void onMapResize() { // TODO: this method can be implemented by sub-classes } /** * API Method: redraw * * @return redrawn */ public boolean redraw() { //Util.printDebugMessage("@...Layer.redraw() is called..."); boolean redrawn = false; if(this.map != null) { // min/max range may have changed, check if layer is still in range this.inRange = this.calculateInRange(); // map's center might not yet be set Envelope extent = this.getExtent(); if(extent!=null && this.inRange==true && this.visible==true) { this.moveTo(extent, false, false); // always set 'zoomChanged' to false redrawn = true; } } return redrawn; } /** * API Method: setMap * Set the map property for the layer. subclasses can override this * and take special action once they have their map variable set. * * Here we take care to bring over any of the necessary default * properties from the map. * * @param map * */ public void setMap(Map map) { //Util.printDebugMessage("@...Layer.setMap() is called..."); if(this.map == null) { this.map = map; if(this.maxExtent == null) { this.maxExtent = this.map.getMaxExtent(); } if(this.projection == null) { this.projection = this.map.getProjection(); } if(this.units == null) { this.units = this.map.getUnits(); } if(this.imageSize == null) { this.imageSize = this.map.getSize(); } // initialize the resolutions and scales this.initResolutions(); if(this.isBaseLayer == false) { this.inRange = this.calculateInRange(); boolean show = ((this.visible) && (this.inRange)); this.visible = (show ? true : false); } } } /** * APIMethod: removeMap * Just as setMap() allows each layer the possibility to take a * personalized action on being added to the map, removeMap() allows * each layer to take a personalized action on being removed from it. * For now, this will be mostly unused, except for the EventPane layer, * which needs this hook so that it can remove the special invisible * pane. * * @param map */ // TODO: public void removeMap(Map map) {} /** * APIMethod: initResolutions * This method's responsibility is to set up the 'resolutions' array * for the layer -- this array is what the layer will use to interface * between the zoom levels of the map and the resolution display * of the layer. * * The user has several options that determine how the array is set up. * * */ public void initResolutions() { //Util.printDebugMessage("@...Layer.initResolutions() is called..."); int numZoomLevels = this.map.getNumZoomLevels(); int maxZoomLevel = this.map.getMaxZoomLevel(); double[] scales = this.map.getScales(); double[] resolutions = this.map.getResolutions(); String units = this.map.getUnits(); double maxScale = this.map.getMaxScale(); double minScale = this.map.getMinScale(); double maxResolution = this.map.getMaxResolution(); double minResolution = this.map.getMinResolution(); Envelope maxExtent = this.map.getMaxExtent(); Envelope minExtent = this.map.getMinExtent(); if(numZoomLevels<0 && maxZoomLevel>=0) { numZoomLevels = maxZoomLevel + 1; } if(scales!=null || resolutions!=null) { if(scales!=null && scales.length>0) { // if map has scales, calculate resolutions based on scales Util.printDebugMessage(" ...resolutions are calculated based on scales..."); resolutions = new double[scales.length]; for(int i=0; i<scales.length; i++) { double scale = scales[i]; resolutions[i] = Util.getResolutionFromScale(scale, units); } } numZoomLevels = resolutions.length; } else { if(minScale > 0) { Util.printDebugMessage(" ...maxResolution calculated based on minScale..."); maxResolution = Util.getResolutionFromScale(minScale, units); } else if(maxResolution <= 0) { Util.printDebugMessage(" ...maxResolution calculated based on map max extent..."); Size viewSize = this.map.getSize(); double wRes = maxExtent.getWidth() / viewSize.getWidth(); double hRes = maxExtent.getHeight()/ viewSize.getHeight(); maxResolution = Math.max(wRes, hRes); } if(maxScale > 0) { // calculate minResolution Util.printDebugMessage(" ...minResolution calculated based on maxScale..."); minResolution = Util.getResolutionFromScale(maxScale, units); } else if(minResolution<0 && minExtent != null) { // calculate based on this.map.minExtent Util.printDebugMessage(" ...minResolution calculated based on map min extent..."); Size viewSize = this.map.getSize(); double wRes = minExtent.getWidth() / viewSize.getWidth(); double hRes = minExtent.getHeight()/ viewSize.getHeight(); minResolution = Math.max(wRes, hRes); } if(minResolution > 0) { double ratio = maxResolution / minResolution; numZoomLevels = (int)Math.round(Math.floor(Math.log(ratio)/Math.log(2)) + 1); } resolutions = new double[numZoomLevels]; for(int i=0; i<numZoomLevels; i++) { double res = maxResolution / Math.pow(2, i); resolutions[i] = res; } } // TODO: sort resolutions descending, is it necessary? this.resolutions = resolutions; this.maxResolution = resolutions[0]; this.minResolution = resolutions[resolutions.length-1]; this.scales = new double[resolutions.length]; for(int i=0; i<resolutions.length; i++) { this.scales[i] = Util.getScaleFromResolution(resolutions[i], units); } this.minScale = this.scales[0]; this.maxScale = this.scales[this.scales.length - 1]; this.numZoomLevels = numZoomLevels; // print out resolutions and scales information /* for(int i=0; i<this.numZoomLevels; i++) { Util.printDebugMessage(" ...zoom level " + i + ": " + "resolution: " + this.resolutions[i] + " scale: " + this.scales[i] + "..."); } */ } /** * API Method: clone * * @return layer * a clone of the current layer */ public Layer clone() { // TODO: return null; } /** * API Method: getExtent * * @return extent * get the current visible extent from map */ public Envelope getExtent() { return this.map.calculateBounds(); } /** * API Method: getResolution * * @return the resolution * get the current resolution from map */ public double getResolution() { int zoom = this.map.getZoom(); return this.map.getResolutionForZoom(zoom); } /** * API Method: getZoomForExtent * * @param extent * @return zoom * calculate zoom based on current extent */ public int getZoomForExtent(Envelope extent) { Size viewSize = this.map.getSize(); double idealResolution = Math.max(extent.getWidth()/viewSize.getWidth(), extent.getHeight()/viewSize.getHeight()); return this.getZoomForResolution(idealResolution); } /** * API Method: getZoomForResolution * * @param resolution * @return zoom * calculate zoom based on current resolution */ public int getZoomForResolution(double resolution) { int zoom; // TODO: deal with fractional zoom int i; for(i=1; i<this.resolutions.length; i++) { if(this.resolutions[i] < resolution) { break; } } zoom = i - 1; return zoom; } /** * API Method: getLonLatFromViewPortPx * Returns a map location given a pixel location. * * @param viewPortPx * @return lonlat * calculate the map coordinate from a screen location. */ public Coordinate getCoordinateFromViewPortPx(Pixel viewPortPx) { Coordinate coord = null; if(viewPortPx != null) { Size size = this.map.getSize(); Coordinate center = this.map.getCenter(); if(center != null) { double res = this.map.getResolution(); double dx = viewPortPx.getX() - (size.getWidth()/2); double dy = viewPortPx.getY() - (size.getHeight()/2); coord = new Coordinate(center.x+dx*res, center.y-dy*res); } } if(this.wrapDateLine == true) { // TODO: deal with wrapDateLine } return coord; } /** * API Method: getViewPortPxFromLonLat * calculate the screen location from map coordinate. * * @param lonlat * @return viewPortPx * calculate the screen location from map coordinate. */ public Pixel getViewPortPxFromCoordinate(Coordinate coord) { Pixel pixel = null; if(coord != null) { double res = this.map.getResolution(); Envelope extent = this.map.getExtent(); pixel = new Pixel( (1/res * (coord.x - extent.getMinX())), (1/res * (extent.getMaxY() - coord.y)) ); } return pixel; } /** * API Method: getResolutionForZoom * * @param zoom * @return resolution * calculate current resolution based on zoom level */ public double getResolutionForZoom(int zoom) { zoom = Math.max(0, Math.min(zoom, this.resolutions.length - 1)); double res; if(this.map.isFractionalZoom()) { // TODO: deal with fractional zoom res = this.resolutions[Math.round(zoom)]; } else { res = this.resolutions[Math.round(zoom)]; } return res; } /** * API Method: moveTo * * @param bounds * @param zoomChanged * @param dragging * */ public void moveTo(Envelope bounds, boolean zoomChanged) { // it's always called by layer.redraw() so always set 'centerChanged' to false moveTo(bounds, zoomChanged, false); } /** * API Method: moveTo * * @param bounds * @param zoomChanged * @param centerChanged * @param dragging */ public void moveTo(Envelope bounds, boolean zoomChanged, boolean centerChanged) { // just re-check if layer is still in range and visible after move // leave the real move to subclass of layer //Util.printDebugMessage("@...Layer.moveTo() is called..."); boolean display = this.visible; if(this.isBaseLayer == false) { display = display && this.inRange; } this.visible = display; //Util.printDebugMessage(" ...is layer visible: " + this.visible + "..."); } /** * * @param bounds * @param zoomChanged * @param centerChanged * @param dragging */ public void drag(int dx, int dy) { // just re-check if layer is still in range and visible after move // leave the real move to subclass of layer boolean display = this.visible; if(this.isBaseLayer == false) { display = display && this.inRange; } this.visible = display; } /** * API Method: calculateInRange * * @return * whether the resolution of layer is between max and min resolution level */ public boolean calculateInRange() { boolean inRange = false; if(this.alwaysInRange == true) { inRange = true; } else { if(this.map != null) { // TODO: this has problem when map.baselayer is not set, resolution is returned as -1 - // - which causes the inRange is always false for non baselayer double resolution = this.map.getResolution(); inRange = ((resolution >= this.minResolution) && (resolution <= this.maxResolution)); } } return inRange; } /** * API Method: adjustBoundsByGutter * * @param bounds * @return * add gutter margin to the actual extent */ public Envelope adjustBoundsByGutter(Envelope bounds) { double mapGutter = this.gutter * this.map.getResolution(); bounds = new Envelope( bounds.getMinX() - mapGutter, bounds.getMaxX() + mapGutter, bounds.getMinY() - mapGutter, bounds.getMaxY() + mapGutter); return bounds; } /** * public void cancelLoadingThreads() * */ public void cancelLoadingThreads() { // do nothing in Layer // must be implemented by subclass to apply thread canceling } // =========================================================== // Getters & Setters // =========================================================== /** * API Method: getUrl */ public String getUrl(Envelope bounds) { // for subclass to override return ""; } /** * @return the map */ public Map getMap() { return this.map; } /** * @return the units */ public String getUnits() { return this.units; } /** * @return the inRange */ public boolean isInRange() { return inRange; } /** * @param inRange the inRange to set */ public void setInRange(boolean inRange) { this.inRange = inRange; } /** * @return the visible */ public boolean isVisible() { return visible; } /** * @param visible the visible to set */ public void setVisible(boolean visible) { if(this.visible != visible) { this.visible = visible; this.redraw(); if(this.map != null) { Event event = new Event(); event.properties.put("data", this); event.properties.put("incident", "visibility"); this.map.getEvents().triggerEvent(MapEvents.LAYER_CHANGED, event); } this.events.triggerEvent(LayerEvents.VISIBILITY_CHANGED, new Event(LayerEvents.VISIBILITY_CHANGED, null)); } } /** * @return the isBaseLayer */ public boolean isBaseLayer() { return isBaseLayer; } /** * @param isBaseLayer the isBaseLayer to set */ public void setBaseLayer(boolean isBaseLayer) { if(this.isBaseLayer != isBaseLayer) { this.isBaseLayer = isBaseLayer; if(this.map != null) { // in case layer is already added into map, and being switched to a baselayer this.map.getEvents().triggerEvent(MapEvents.BASELAYER_CHANGED, new Event(MapEvents.BASELAYER_CHANGED, this)); } } } /** * @return the name */ public String getName() { return name; } /** * @param name the name to set */ public void setName(String name) { if(this.name.equals(name) == false) { this.name = name; if(this.map != null) { Event event = new Event(); event.properties.put("data", this); event.properties.put("incident", "name"); this.map.getEvents().triggerEvent(MapEvents.LAYER_CHANGED, event); } } } /** * @return the alwaysInRange */ public boolean isAlwaysInRange() { return alwaysInRange; } /** * @param alwaysInRange the alwaysInRange to set */ public void setAlwaysInRange(boolean alwaysInRange) { this.alwaysInRange = alwaysInRange; } /** * @return the imageOffset */ public Pixel getImageOffset() { return imageOffset; } /** * @param imageOffset the imageOffset to set */ public void setImageOffset(Pixel imageOffset) { this.imageOffset = imageOffset; } /** * @return the gutter */ public int getGutter() { return gutter; } /** * @param gutter the gutter to set */ public void setGutter(int gutter) { this.gutter = gutter; } /** * @return the displayOutsideMaxExtent */ public boolean isDisplayOutsideMaxExtent() { return displayOutsideMaxExtent; } /** * @param displayOutsideMaxExtent the displayOutsideMaxExtent to set */ public void setDisplayOutsideMaxExtent(boolean displayOutsideMaxExtent) { this.displayOutsideMaxExtent = displayOutsideMaxExtent; } /** * @return the buffer */ public int getBuffer() { return buffer; } /** * @param buffer the buffer to set */ public void setBuffer(int buffer) { this.buffer = buffer; } /** * @return the imageSize */ public Size getImageSize() { return imageSize; } /** * @param imageSize the imageSize to set */ public void setImageSize(Size imageSize) { this.imageSize = imageSize; } /** * @return the maxExtent */ public Envelope getMaxExtent() { return maxExtent; } /** * @param maxExtent the maxExtent to set */ public void setMaxExtent(Envelope maxExtent) { this.maxExtent = maxExtent; } /** * @return the maxResolution */ public double getMaxResolution() { return maxResolution; } /** * @param maxResolution the maxResolution to set */ public void setMaxResolution(double maxResolution) { this.maxResolution = maxResolution; } /** * @return the minResolution */ public double getMinResolution() { return minResolution; } /** * @param minResolution the minResolution to set */ public void setMinResolution(double minResolution) { this.minResolution = minResolution; } /** * @return the numZoomLevels */ public int getNumZoomLevels() { return numZoomLevels; } /** * @param numZoomLevels the numZoomLevels to set */ public void setNumZoomLevels(int numZoomLevels) { this.numZoomLevels = numZoomLevels; } /** * @return the scales */ public double[] getScales() { return scales; } /** * @param scales the scales to set */ public void setScales(double[] scales) { this.scales = scales; } /** * @return the resolutions */ public double[] getResolutions() { return resolutions; } /** * @param resolutions the resolutions to set */ public void setResolutions(double[] resolutions) { this.resolutions = resolutions; } /** * @return the projection */ public String getProjection() { return projection; } /** * @param projection the projection to set */ public void setProjection(String projection) { this.projection = projection; } /** * @return the maxScale */ public double getMaxScale() { return maxScale; } /** * @param maxScale the maxScale to set */ public void setMaxScale(double maxScale) { this.maxScale = maxScale; } /** * @return the minScale */ public double getMinScale() { return minScale; } /** * @param minScale the minScale to set */ public void setMinScale(double minScale) { this.minScale = minScale; } /** * @param units the units to set */ public void setUnits(String units) { this.units = units; } /** * @return the opacity */ public double getOpacity() { return opacity; } /** * @param opacity the opacity to set */ public void setOpacity(double opacity) { this.opacity = opacity; } /** * @return the wrapDateLine */ public boolean isWrapDateLine() { return wrapDateLine; } /** * @param wrapDateLine the wrapDateLine to set */ public void setWrapDateLine(boolean wrapDateLine) { this.wrapDateLine = wrapDateLine; } /** * * @return */ public Paint getPaint() { return paint; } public Bitmap getPreviousSnapshot() { return previousSnapshot; } public void setPreviousSnapshot(Bitmap previousSnapshot) { this.previousSnapshot = previousSnapshot; } /** * * @author ying4682 * */ private class LayerEventsHandler extends Handler { @Override public void handleMessage(final Message msg) { switch(msg.what) { case LayerEvents.FEATURE_ADDED: //Util.printDebugMessage(" ...LayerEventsHandler: feature added..."); break; case LayerEvents.FEATURES_ADDED: //Util.printDebugMessage(" ...LayerEventsHandler: features added..."); break; case LayerEvents.LOAD_CANCELED: //Util.printDebugMessage(" ...LayerEventsHandler: load canceled..."); break; case LayerEvents.LOAD_END: //Util.printDebugMessage(" ...LayerEventsHandler: load end..."); break; case LayerEvents.LOAD_START: //Util.printDebugMessage(" ...LayerEventsHandler: load start..."); break; case LayerEvents.MOVE_END: //Util.printDebugMessage(" ...LayerEventsHandler: move end..."); break; case LayerEvents.TILE_LOADED: //Util.printDebugMessage(" ...LayerEventsHandler: tile loaded..."); break; case LayerEvents.VISIBILITY_CHANGED: //Util.printDebugMessage(" ...LayerEventsHandler: visibility changed..."); break; } } } }