/* * Copyright 2013 Hannes Janetzek * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with * this program. If not, see <http://www.gnu.org/licenses/>. */ package org.oscim.map; import org.oscim.core.MapPosition; import org.oscim.event.Event; import org.oscim.event.EventDispatcher; import org.oscim.event.EventListener; import org.oscim.event.Gesture; import org.oscim.event.GestureDetector; import org.oscim.event.MotionEvent; import org.oscim.layers.MapEventLayer; import org.oscim.layers.tile.TileLayer; import org.oscim.layers.tile.vector.OsmTileLayer; import org.oscim.layers.tile.vector.VectorTileLayer; import org.oscim.renderer.MapRenderer; import org.oscim.theme.IRenderTheme; import org.oscim.theme.ThemeFile; import org.oscim.theme.ThemeLoader; import org.oscim.tiling.TileSource; import org.oscim.utils.async.AsyncExecutor; import org.oscim.utils.async.TaskQueue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class Map implements TaskQueue { static final Logger log = LoggerFactory.getLogger(Map.class); /** * Listener interface for map update notifications. * Layers implementing this interface they will be automatically register * when the layer is added to the map and unregistered when the layer is * removed. Otherwise use map.events.bind(UpdateListener). */ public interface UpdateListener extends EventListener { void onMapEvent(Event e, MapPosition mapPosition); } /** * Listener interface for input events. * Layers implementing this interface they will be automatically register * when the layer is added to the map and unregistered when the layer is * removed. */ public interface InputListener extends EventListener { void onInputEvent(Event e, MotionEvent motionEvent); } /** * UpdateListener event. Map position has changed. */ public static Event POSITION_EVENT = new Event(); /** * UpdateLister event. Delivered on main-thread when updateMap() was called * and no CLEAR_EVENT or POSITION_EVENT was triggered. */ public static Event UPDATE_EVENT = new Event(); /** * UpdateListerner event. Map state has changed in a way that all layers * should clear their state e.g. the theme or the TilesSource has changed. * TODO should have an event-source to only clear affected layers. */ public static Event CLEAR_EVENT = new Event(); public final EventDispatcher<InputListener, MotionEvent> input; public final EventDispatcher<UpdateListener, MapPosition> events; private final Layers mLayers; private final ViewController mViewport; private final Animator mAnimator; private final MapPosition mMapPosition; private final AsyncExecutor mAsyncExecutor; protected final MapEventLayer mEventLayer; protected GestureDetector mGestureDetector; private TileLayer mBaseLayer; protected boolean mClearMap = true; public Map() { mViewport = new ViewController(); mAnimator = new Animator(this); mLayers = new Layers(this); input = new EventDispatcher<InputListener, MotionEvent>() { @Override public void tell(InputListener l, Event e, MotionEvent d) { l.onInputEvent(e, d); } }; events = new EventDispatcher<UpdateListener, MapPosition>() { @Override public void tell(UpdateListener l, Event e, MapPosition d) { l.onMapEvent(e, d); } }; mAsyncExecutor = new AsyncExecutor(4, this); mMapPosition = new MapPosition(); mEventLayer = new MapEventLayer(this); mLayers.add(0, mEventLayer); } public MapEventLayer getEventLayer() { return mEventLayer; } /** * Create OsmTileLayer with given TileSource and * set as base map (layer 1) * * TODO deprecate */ public VectorTileLayer setBaseMap(TileSource tileSource) { VectorTileLayer l = new OsmTileLayer(this); l.setTileSource(tileSource); setBaseMap(l); return l; } public TileLayer setBaseMap(TileLayer tileLayer) { mLayers.add(1, tileLayer); mBaseLayer = tileLayer; return tileLayer; } /** * Utility function to set theme of base vector-layer and * use map background color from theme. */ public void setTheme(ThemeFile theme) { if (mBaseLayer == null) { log.error("No base layer set"); throw new IllegalStateException(); } setTheme(ThemeLoader.load(theme)); } public void setTheme(IRenderTheme theme) { if (theme == null) { throw new IllegalArgumentException("Theme cannot be null."); } if (mBaseLayer == null) { log.warn("No base layer set."); } else if (mBaseLayer instanceof VectorTileLayer) { ((VectorTileLayer) mBaseLayer).setRenderTheme(theme); } MapRenderer.setBackgroundColor(theme.getMapBackground()); clearMap(); } public void destroy() { mLayers.destroy(); mAsyncExecutor.dispose(); } /** * Request call to onUpdate for all layers. This function can * be called from any thread. Request will be handled on main * thread. * * @param redraw pass true to render next frame afterwards */ public abstract void updateMap(boolean redraw); /** * Request to render a frame. Request will be handled on main * thread. Use this for animations in RenderLayers. */ public abstract void render(); /** * Post a runnable to be executed on main-thread */ @Override public abstract boolean post(Runnable action); /** * Post a runnable to be executed on main-thread. Execution is delayed for * at least 'delay' milliseconds. */ public abstract boolean postDelayed(Runnable action, long delay); /** * Post a task to run on a shared worker-thread. Shoul only use for * tasks running less than a second. */ @Override public void addTask(Runnable task) { mAsyncExecutor.post(task); } /** * Return screen width in pixel. */ public abstract int getWidth(); /** * Return screen height in pixel. */ public abstract int getHeight(); /** * Request to clear all layers before rendering next frame */ public void clearMap() { mClearMap = true; updateMap(true); } /** * Set {@link MapPosition} of {@link Viewport} and trigger a redraw. */ public void setMapPosition(MapPosition mapPosition) { mViewport.setMapPosition(mapPosition); updateMap(true); } public void setMapPosition(double latitude, double longitude, double scale) { mViewport.setMapPosition(new MapPosition(latitude, longitude, scale)); updateMap(true); } /** * Get current {@link MapPosition}. * * @return true when MapPosition was updated (has changed) */ public boolean getMapPosition(MapPosition mapPosition) { return mViewport.getMapPosition(mapPosition); } /** * Get current {@link MapPosition}. Consider using * getViewport.getMapPosition(pos) instead to reuse * MapPosition instance. */ public MapPosition getMapPosition() { MapPosition pos = new MapPosition(); mViewport.getMapPosition(pos); return pos; } /** * @return Viewport instance */ public ViewController viewport() { return mViewport; } /** * @return Layers instance */ public Layers layers() { return mLayers; } /** * @return MapAnimator instance */ public Animator animator() { return mAnimator; } /** * This function is run on main-loop before rendering a frame. * Caution: Do not call directly! */ protected void updateLayers() { boolean changed = false; MapPosition pos = mMapPosition; changed |= mViewport.getMapPosition(pos); if (mClearMap) events.fire(CLEAR_EVENT, pos); else if (changed) events.fire(POSITION_EVENT, pos); else events.fire(UPDATE_EVENT, pos); mClearMap = false; } public boolean handleGesture(Gesture g, MotionEvent e) { return mLayers.handleGesture(g, e); } }