Java tutorial
/****************************************************************************** * Project: NextGIS mobile * Purpose: Mobile GIS for Android. * Author: Dmitry Baryshnikov (aka Bishop), polimax@mail.ru ****************************************************************************** * Copyright (C) 2014 NextGIS * * 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 2 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 <http://www.gnu.org/licenses/>. ****************************************************************************/ package com.nextgis.mobile.map; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Canvas; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; import android.util.Log; import android.view.View; import android.widget.Toast; import com.nextgis.mobile.R; import com.nextgis.mobile.datasource.GeoPoint; import com.nextgis.mobile.display.GISDisplay; import com.nextgis.mobile.util.FileUtil; import com.nextgis.mobile.util.NetworkUtil; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import static com.nextgis.mobile.util.Constants.*; public class MapBase extends View { protected String mName; protected List<Layer> mLayers; protected List<MapEventListener> mListeners; protected GISDisplay mDisplay; protected File mMapPath; protected Handler mHandler; protected short mNewId; protected long mStartDrawTime; protected boolean mContinueDrawing; protected final BlockingQueue<Runnable> mDrawWorkQueue; protected ThreadPoolExecutor mDrawThreadPool; protected static int mCPUTotalCount; protected NetworkUtil newtworkUtil; /** * The base map class */ public MapBase(Context context) { super(context); mName = context.getString(R.string.default_map_name); mNewId = 0; mListeners = new ArrayList<MapEventListener>(); mLayers = new ArrayList<Layer>(); mCPUTotalCount = Runtime.getRuntime().availableProcessors() - 1; if (mCPUTotalCount < 1) mCPUTotalCount = 1; mContinueDrawing = false; newtworkUtil = new NetworkUtil(context); createHandler(); //initialise display mDisplay = new GISDisplay(context); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); File defaultPath = context.getExternalFilesDir(PREFS_MAP); mMapPath = new File(sharedPreferences.getString(KEY_PREF_MAP_PATH, defaultPath.getPath())); mDrawWorkQueue = new LinkedBlockingQueue<Runnable>(); mDrawThreadPool = new ThreadPoolExecutor(1, mCPUTotalCount, KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, mDrawWorkQueue); setKeepScreenOn(true); } /** * Get map name * * @return map name */ public String getName() { return mName; } /** * Set map name * * @param newName A new map name */ public void setName(String newName) { this.mName = newName; } /** * Get the map layers * * @return map layers list */ public List<Layer> getLayers() { return mLayers; } @Override protected void onDraw(Canvas canvas) { if (mDisplay != null) { canvas.drawBitmap(mDisplay.getDisplay(true), 0, 0, null); } else { super.onDraw(canvas); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (mDisplay != null) { mDisplay.setSize(w, h); onExtentChanged((int) mDisplay.getZoomLevel(), mDisplay.getCenter()); } } protected synchronized void runDrawThread() { cancelDrawThread(); mDisplay.clearBackground(); mDisplay.clearLayer(); mStartDrawTime = System.currentTimeMillis(); for (Layer layer : mLayers) { if (layer.isVisible()) { mDrawThreadPool.execute(layer); } } } protected synchronized void cancelDrawThread() { for (Layer layer : mLayers) { if (layer.isVisible()) { layer.cancelDraw(); } } /* * Creates an array of Runnables that's the same size as the * thread pool work queue */ /*Runnable[] runnableArray = new Runnable[mDrawWorkQueue.size()]; // Populates the array with the Runnables in the queue mDrawWorkQueue.toArray(runnableArray); // Iterates over the array of tasks for (Runnable runnable : runnableArray) { Layer layer = (Layer) runnable; layer.cancelDraw(); } //mDrawWorkQueue.clear(); */ } /** * Create handler for messages */ protected void createHandler() { mHandler = new Handler() { public void handleMessage(Message msg) { super.handleMessage(msg); Bundle resultData = msg.getData(); boolean bHasErr = resultData.getBoolean(BUNDLE_HASERROR_KEY); if (bHasErr) { reportError(resultData.getString(BUNDLE_MSG_KEY)); } else { processMessage(resultData); } } }; } /** * Standard error report function * * @param errMsg An error message */ protected void reportError(String errMsg) { Log.d(TAG, errMsg); Toast.makeText(getContext(), errMsg, Toast.LENGTH_SHORT).show(); } /** * Process message received by handler * * @param bundle A message payload */ protected void processMessage(Bundle bundle) { switch (bundle.getInt(BUNDLE_TYPE_KEY)) { case MSGTYPE_DRAWING_DONE: onLayerDrawFinished(bundle.getFloat(BUNDLE_DONE_KEY)); break; default: break; } } /** * Create existed layer from path and add it to the map * * @param path A path to layer directory */ protected void addLayer(File path) { File config_file = new File(path, LAYER_CONFIG); try { String sData = FileUtil.readFromFile(config_file); JSONObject rootObject = new JSONObject(sData); int nType = rootObject.getInt(JSON_TYPE_KEY); Layer layer = null; switch (nType) { case LAYERTYPE_LOCAL_TMS: layer = new LocalTMSLayer(this, path, rootObject); break; case LAYERTYPE_LOCAL_GEOJSON: layer = new LocalGeoJsonLayer(this, path, rootObject); break; case LAYERTYPE_LOCAL_RASTER: break; case LAYERTYPE_TMS: layer = new RemoteTMSLayer(this, path, rootObject); break; case LAYERTYPE_NGW: break; } if (layer != null) { mLayers.add(layer); saveMap(); onLayerAdded(layer); } } catch (IOException e) { reportError(e.getLocalizedMessage()); } catch (JSONException e) { reportError(e.getLocalizedMessage()); } } /** * Set new map extent according zoom level and center * @param zoom A zoom level * @param center A map center coordinates */ protected void setZoomAndCenter(final double zoom, final GeoPoint center) { if (mDisplay != null) { double newZoom = zoom; if (zoom < mDisplay.getMinZoomLevel()) newZoom = mDisplay.getMinZoomLevel(); else if (zoom > mDisplay.getMaxZoomLevel()) newZoom = mDisplay.getMaxZoomLevel(); mDisplay.setZoomAndCenter(newZoom, center); onExtentChanged((int) newZoom, center); } } public boolean canZoomIn() { if (mDisplay == null) return false; return mDisplay.getZoomLevel() < mDisplay.getMaxZoomLevel(); } public boolean canZoomOut() { if (mDisplay == null) return false; return mDisplay.getZoomLevel() > mDisplay.getMinZoomLevel(); } public final GeoPoint getMapCenter() { if (mDisplay != null) return mDisplay.getCenter(); return new GeoPoint(); } /** * Load map properties and layers from map.json file */ protected synchronized void loadMap() { Log.d(TAG, "load map"); File config_file = new File(mMapPath, MAP_CONFIG); try { String sData = FileUtil.readFromFile(config_file); JSONObject rootObject = new JSONObject(sData); mName = rootObject.getString(JSON_NAME_KEY); final JSONArray jsonArray = rootObject.getJSONArray(JSON_LAYERS_KEY); for (int i = 0; i < jsonArray.length(); i++) { JSONObject jsonLayer = jsonArray.getJSONObject(i); String sPath = jsonLayer.getString(JSON_PATH_KEY); File inFile = new File(mMapPath, sPath); if (inFile.exists()) addLayer(inFile); } //let's draw the map runDrawThread(); } catch (IOException e) { reportError(e.getLocalizedMessage()); } catch (JSONException e) { reportError(e.getLocalizedMessage()); } //load map without the order of layers /*File[] files = mMapPath.listFiles(); for (File inFile : files) { if (inFile.isDirectory() && inFile.toString().startsWith(LAYER_PREFIX)) { addLayer(inFile); } }*/ } /** * Save map properties and layers to map.json file */ public synchronized void saveMap() { Log.d(TAG, "save map"); try { JSONObject rootObject = new JSONObject(); rootObject.put(JSON_NAME_KEY, mName); JSONArray jsonArray = new JSONArray(); rootObject.put(JSON_LAYERS_KEY, jsonArray); for (Layer layer : mLayers) { JSONObject layerObject = new JSONObject(); layerObject.put(JSON_PATH_KEY, layer.getRelativePath()); jsonArray.put(layerObject); layer.save(); } File config_file = new File(mMapPath, MAP_CONFIG); FileUtil.writeToFile(config_file, rootObject.toString()); } catch (IOException e) { reportError(e.getLocalizedMessage()); } catch (JSONException e) { reportError(e.getLocalizedMessage()); } } /** * The identificator generator * * @return new id */ public short getNewId() { return mNewId++; } /** * Add new listener for map events * * @param listener A listener class implements MapEventListener */ public void addListener(MapEventListener listener) { if (mListeners != null) { mListeners.add(listener); } } /** * Send layer added event to all listeners * * @param layer A new layer */ protected void onLayerAdded(Layer layer) { runDrawThread(); if (mListeners == null) return; for (MapEventListener listener : mListeners) listener.onLayerAdded(layer); } /** * Send layer changed event to all listeners * * @param layer A changed layer */ protected void onLayerChanged(Layer layer) { runDrawThread(); if (mListeners == null) return; for (MapEventListener listener : mListeners) listener.onLayerChanged(layer); } /** * Send layer delete event to all listeners * * @param id A deleted layer identificator */ protected void onLayerDeleted(int id) { runDrawThread(); if (mListeners == null) return; for (MapEventListener listener : mListeners) listener.onLayerDeleted(id); } /** * Send extent change event to all listeners * @param zoom A zoom level * @param center A map center coordinates */ protected void onExtentChanged(int zoom, GeoPoint center) { runDrawThread(); if (mListeners == null) return; for (MapEventListener listener : mListeners) listener.onExtentChanged(zoom, center); } /** * onPauses should be called from parent activity */ public void onPause() { } /** * onResume should be called from parent activity */ public void onResume() { } /** * onStop should be called from parent activity */ public void onStop() { saveMap(); clearMap(); } /** * onStart should be called from parent activity */ public void onStart() { loadMap(); } /** * Remove all layers */ protected void clearMap() { mLayers.clear(); //TODO: do we need onClearMap event? } /** * Delete layer by identifictor * * @param id An identificator * @return true on success or false */ public boolean deleteLayerById(int id) { boolean bRes = false; for (Layer layer : mLayers) { if (layer.getId() == id) { layer.delete(); bRes = mLayers.remove(layer); if (bRes) { onLayerDeleted(id); } break; } } return bRes; } /** * Get layer by identificator * * @param id Layer identificator * @return Layer or null */ public Layer getLayerById(int id) { for (Layer layer : mLayers) { if (layer.getId() == id) return layer; } return null; } /** * Create new folder in map directory to store layer data */ protected File cretateLayerStorage() { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); String layerDir = LAYER_PREFIX + sdf.format(new Date()); return new File(mMapPath, layerDir); } /** * Get internal map events handler * * @return handler */ public Handler getMapEventsHandler() { return mHandler; } protected void onLayerDrawFinished(float percent) { if (System.currentTimeMillis() - mStartDrawTime > DISPLAY_REDRAW_TIMEOUT || percent >= 100) { mStartDrawTime = System.currentTimeMillis(); invalidate(); } } public final GISDisplay getGISDisplay() { return mDisplay; } public final double getZoomLevel() { if (mDisplay != null) return mDisplay.getZoomLevel(); return 0; } public boolean isNetworkAvaliable() { return newtworkUtil.isNetworkAvailible(); } }