Java tutorial
/* * Copyright 20132016 Michael von Glasow. * * This file is part of LSRN Tools. * * LSRN Tools 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 3 of the License, or * (at your option) any later version. * * LSRN Tools 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 LSRN Tools. If not, see <http://www.gnu.org/licenses/>. */ package com.vonglasow.michael.satstat; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import org.mapsforge.core.graphics.Bitmap; import org.mapsforge.core.graphics.Paint; import org.mapsforge.core.graphics.Style; import org.mapsforge.core.model.BoundingBox; import org.mapsforge.core.model.Dimension; import org.mapsforge.core.model.LatLong; import org.mapsforge.core.model.Point; import org.mapsforge.core.util.LatLongUtils; import org.mapsforge.map.android.graphics.AndroidGraphicFactory; import org.mapsforge.map.android.view.MapView; import org.mapsforge.map.layer.LayerManager; import org.mapsforge.map.layer.Layers; import org.mapsforge.map.layer.cache.TileCache; import org.mapsforge.map.layer.download.TileDownloadLayer; import org.mapsforge.map.layer.download.tilesource.OnlineTileSource; import org.mapsforge.map.layer.overlay.Circle; import org.mapsforge.map.layer.overlay.Marker; import org.mapsforge.map.layer.renderer.TileRendererLayer; import org.mapsforge.map.util.MapViewProjection; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.location.GpsSatellite; import android.location.GpsStatus; import android.location.Location; import android.location.LocationManager; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.Fragment; import android.util.Log; import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.View.OnClickListener; import android.widget.FrameLayout; import android.widget.ImageButton; import com.vonglasow.michael.satstat.mapsforge.PersistentTileCache; /** * The fragment which displays the map view. */ public class MapSectionFragment extends Fragment { public static final String TAG = "MapSectionFragment"; /** * The fragment argument representing the section number for this * fragment. */ public static final String ARG_SECTION_NUMBER = "section_number"; public static double EARTH_CIRCUMFERENCE = 40000000; // meters /* * Indices into style arrays */ private static final int STYLE_MARKER = 0; private static final int STYLE_STROKE = 1; private static final int STYLE_FILL = 2; /* * Styles for location providers */ private static final String[] LOCATION_PROVIDER_STYLES = { "location_provider_blue", "location_provider_green", "location_provider_orange", "location_provider_purple", "location_provider_red" }; /* * Blue style: default for network location provider */ private static final String LOCATION_PROVIDER_BLUE = "location_provider_blue"; /* * Red style: default for GPS location provider */ private static final String LOCATION_PROVIDER_RED = "location_provider_red"; /* * Gray style for inactive location providers */ private static final String LOCATION_PROVIDER_GRAY = "location_provider_gray"; private static final String KEY_LOCATION_STALE = "isStale"; private static final int PROVIDER_EXPIRATION_DELAY = 6000; // the time after which a location is considered stale private MainActivity mainActivity = null; OnlineTileSource onlineTileSource; private MapView mapMap; private TileDownloadLayer mapDownloadLayer = null; private TileCache mapTileCache = null; private ImageButton mapReattach; private boolean isMapViewAttached = true; private HashMap<String, Circle> mapCircles; private HashMap<String, Marker> mapMarkers; /** * Cached map of locations reported by the providers. * * The keys correspond to the provider names as defined by LocationManager. * The entries are {@link Location} instances. For valid and recent * locations these are copies of the locations supplied by * {@link LocationManager}. Invalid locations, intended as placeholders, * have an empty provider string and should not be processed. Stale * locations have isStale entry in their extras set to true. They can be * processed but may require special handling. */ private HashMap<String, Location> providerLocations; private HashMap<String, String> providerStyles; private HashMap<String, String> providerAppliedStyles; private List<String> mAvailableProviderStyles; private Handler providerInvalidationHandler = null; private HashMap<String, Runnable> providerInvalidators; public MapSectionFragment() { } /** * Applies a style to the map overlays associated with a given location provider. * * This method changes the style (effectively, the color) of the circle and * marker overlays. Its main purpose is to switch the color of the overlays * between gray and the provider color. * * @param context The context of the caller * @param provider The name of the location provider, as returned by * {@link LocationProvider.getName()}. * @param styleName The name of the style to apply. If it is null, the * default style for the provider as returned by * assignLocationProviderStyle() is applied. */ protected void applyLocationProviderStyle(Context context, String provider, String styleName) { String sn = (styleName != null) ? styleName : assignLocationProviderStyle(provider); Boolean isStyleChanged = !sn.equals(providerAppliedStyles.get(provider)); Boolean needsRedraw = false; Resources res = context.getResources(); TypedArray style = res.obtainTypedArray(res.getIdentifier(sn, "array", context.getPackageName())); // Circle layer Circle circle = mapCircles.get(provider); if (circle != null) { circle.getPaintFill().setColor(style.getColor(STYLE_FILL, R.color.circle_gray_fill)); circle.getPaintStroke().setColor(style.getColor(STYLE_STROKE, R.color.circle_gray_stroke)); needsRedraw = isStyleChanged && circle.isVisible(); } //Marker layer Marker marker = mapMarkers.get(provider); if (marker != null) { Drawable drawable = style.getDrawable(STYLE_MARKER); Bitmap bitmap = AndroidGraphicFactory.convertToBitmap(drawable); marker.setBitmap(bitmap); needsRedraw = needsRedraw || (isStyleChanged && marker.isVisible()); } if (needsRedraw) mapMap.getLayerManager().redrawLayers(); providerAppliedStyles.put(provider, sn); style.recycle(); } /** * Returns the map overlay style to use for a given location provider. * * This method first checks if a style has already been assigned to the * location provider. In that case the already assigned style is returned. * Otherwise a new style is assigned and the assignment is stored * internally and written to SharedPreferences. * @param provider * @return The style to use for non-stale locations */ protected String assignLocationProviderStyle(String provider) { String styleName = providerStyles.get(provider); if (styleName == null) { /* * Not sure if this ever happens but I can't rule it out. Scenarios I can think of: * - A custom location provider which identifies itself as "passive" * - A combination of the following: * - Passive location provider is selected * - A new provider is added while we're running (so it's not in our list) * - Another app starts using the new provider * - The passive location provider forwards us an update from the new provider */ if (mAvailableProviderStyles.isEmpty()) mAvailableProviderStyles.addAll(Arrays.asList(LOCATION_PROVIDER_STYLES)); styleName = mainActivity.mSharedPreferences.getString( SettingsActivity.KEY_PREF_LOC_PROV_STYLE + provider, mAvailableProviderStyles.get(0)); providerStyles.put(provider, styleName); SharedPreferences.Editor spEditor = mainActivity.mSharedPreferences.edit(); spEditor.putString(SettingsActivity.KEY_PREF_LOC_PROV_STYLE + provider, styleName); spEditor.commit(); } return styleName; } /** * Determines if a location is stale. * * A location is considered stale if its Extras have an isStale key set to * True. A location without this key is not considered stale. * * @param location * @return True if stale, False otherwise */ public static boolean isLocationStale(Location location) { Bundle extras = location.getExtras(); if (extras == null) return false; return extras.getBoolean(KEY_LOCATION_STALE); } public static void markLocationAsStale(Location location) { if (location.getExtras() == null) location.setExtras(new Bundle()); location.getExtras().putBoolean(KEY_LOCATION_STALE, true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mainActivity = (MainActivity) this.getContext(); View rootView = inflater.inflate(R.layout.fragment_main_map, container, false); mapReattach = (ImageButton) rootView.findViewById(R.id.mapReattach); mapReattach.setVisibility(View.GONE); isMapViewAttached = true; OnClickListener clis = new OnClickListener() { @Override public void onClick(View v) { if (v == mapReattach) { isMapViewAttached = true; mapReattach.setVisibility(View.GONE); updateMap(); } } }; mapReattach.setOnClickListener(clis); // Initialize controls mapMap = new MapView(rootView.getContext()); ((FrameLayout) rootView).addView(mapMap, 0); mapMap.setClickable(true); mapMap.getMapScaleBar().setVisible(true); mapMap.setBuiltInZoomControls(true); mapMap.getMapZoomControls().setZoomLevelMin((byte) 10); mapMap.getMapZoomControls().setZoomLevelMax((byte) 20); providerLocations = new HashMap<String, Location>(); mAvailableProviderStyles = new ArrayList<String>(Arrays.asList(LOCATION_PROVIDER_STYLES)); providerStyles = new HashMap<String, String>(); providerAppliedStyles = new HashMap<String, String>(); providerInvalidationHandler = new Handler(); providerInvalidators = new HashMap<String, Runnable>(); if (mapTileCache == null) mapTileCache = PersistentTileCache.createTileCache(rootView.getContext(), "MapQuest", mapMap.getModel().displayModel.getTileSize(), 1f, mapMap.getModel().frameBufferModel.getOverdrawFactor()); onlineTileSource = new OnlineTileSource( new String[] { "otile1.mqcdn.com", "otile2.mqcdn.com", "otile3.mqcdn.com", "otile4.mqcdn.com" }, 80); onlineTileSource.setName("MapQuest").setAlpha(false).setBaseUrl("/tiles/1.0.0/map/").setExtension("png") .setParallelRequestsLimit(8).setProtocol("http").setTileSize(256).setZoomLevelMax((byte) 18) .setZoomLevelMin((byte) 0); GestureDetector gd = new GestureDetector(rootView.getContext(), new GestureDetector.SimpleOnGestureListener() { public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { mapReattach.setVisibility(View.VISIBLE); isMapViewAttached = false; return false; } }); mapMap.setGestureDetector(gd); mainActivity.mapSectionFragment = this; return rootView; } @Override public void onDestroyView() { if (mainActivity.mapSectionFragment == this) mainActivity.mapSectionFragment = null; if (mapTileCache != null) mapTileCache.destroy(); if (mapMap != null) { mapMap.getModel().mapViewPosition.destroy(); mapMap.destroy(); } super.onDestroyView(); } /** * Called by {@link MainActivity} when the status of the GPS changes. Updates GPS display. */ public void onGpsStatusChanged(GpsStatus status, int satsInView, int satsUsed, Iterable<GpsSatellite> sats) { if (satsUsed == 0) { Location location = providerLocations.get(LocationManager.GPS_PROVIDER); if (location != null) markLocationAsStale(location); applyLocationProviderStyle(this.getContext(), LocationManager.GPS_PROVIDER, LOCATION_PROVIDER_GRAY); } } /** * Called when a new location is found by a registered location provider. * Stores the location and updates GPS display and map view. */ public void onLocationChanged(Location location) { // some providers may report NaN for latitude and longitude: // if that happens, do not process this location and mark any previous // location from that provider as stale if (Double.isNaN(location.getLatitude()) || Double.isNaN(location.getLongitude())) { markLocationAsStale(providerLocations.get(location.getProvider())); applyLocationProviderStyle(this.getContext(), location.getProvider(), LOCATION_PROVIDER_GRAY); return; } if (providerLocations.containsKey(location.getProvider())) providerLocations.put(location.getProvider(), new Location(location)); LatLong latLong = new LatLong(location.getLatitude(), location.getLongitude()); Circle circle = mapCircles.get(location.getProvider()); Marker marker = mapMarkers.get(location.getProvider()); if (circle != null) { circle.setLatLong(latLong); if (location.hasAccuracy()) { circle.setVisible(true); circle.setRadius(location.getAccuracy()); } else { Log.d("MainActivity", "Location from " + location.getProvider() + " has no accuracy"); circle.setVisible(false); } } if (marker != null) { marker.setLatLong(latLong); marker.setVisible(true); } applyLocationProviderStyle(this.getContext(), location.getProvider(), null); Runnable invalidator = providerInvalidators.get(location.getProvider()); if (invalidator != null) { providerInvalidationHandler.removeCallbacks(invalidator); providerInvalidationHandler.postDelayed(invalidator, PROVIDER_EXPIRATION_DELAY); } // redraw, move locations into view and zoom out as needed if ((circle != null) || (marker != null) || (invalidator != null)) updateMap(); } /** * Updates internal data structures when the user's selection of location providers has changed. * @param providers The new set of location providers */ public void onLocationProvidersChanged(Set<String> providers) { Context context = this.getContext(); List<String> allProviders = mainActivity.locationManager.getAllProviders(); ArrayList<String> removedProviders = new ArrayList<String>(); for (String pr : providerLocations.keySet()) if (!providers.contains(pr)) removedProviders.add(pr); // remove cached locations and invalidators for providers which are no longer selected for (String pr : removedProviders) { providerLocations.remove(pr); providerInvalidators.remove(pr); } // ensure there is a cached location for each chosen provider (can be null) for (String pr : providers) { if ((allProviders.indexOf(pr) >= 0) && !providerLocations.containsKey(pr)) { Location location = new Location(""); providerLocations.put(pr, location); } } // add overlays updateLocationProviderStyles(); mapCircles = new HashMap<String, Circle>(); mapMarkers = new HashMap<String, Marker>(); Log.d(TAG, "Provider location cache: " + providerLocations.keySet().toString()); Layers layers = mapMap.getLayerManager().getLayers(); // remove all layers other than tile render layer from map for (int i = 0; i < layers.size();) if ((layers.get(i) instanceof TileRendererLayer) || (layers.get(i) instanceof TileDownloadLayer)) { i++; } else { layers.remove(i); } for (String pr : providers) { // no invalidator for GPS, which is invalidated through GPS status if ((!pr.equals(LocationManager.GPS_PROVIDER)) && (providerInvalidators.get(pr)) == null) { final String provider = pr; final Context ctx = context; providerInvalidators.put(pr, new Runnable() { private String mProvider = provider; @Override public void run() { Location location = providerLocations.get(mProvider); if (location != null) markLocationAsStale(location); applyLocationProviderStyle(ctx, mProvider, LOCATION_PROVIDER_GRAY); } }); } String styleName = assignLocationProviderStyle(pr); LatLong latLong; float acc; boolean visible; if ((providerLocations.get(pr) != null) && (providerLocations.get(pr).getProvider() != "")) { latLong = new LatLong(providerLocations.get(pr).getLatitude(), providerLocations.get(pr).getLongitude()); if (providerLocations.get(pr).hasAccuracy()) acc = providerLocations.get(pr).getAccuracy(); else acc = 0; visible = true; if (isLocationStale(providerLocations.get(pr))) styleName = LOCATION_PROVIDER_GRAY; Log.d("MainActivity", pr + " has " + latLong.toString()); } else { latLong = new LatLong(0, 0); acc = 0; visible = false; Log.d("MainActivity", pr + " has no location, hiding"); } // Circle layer Resources res = context.getResources(); TypedArray style = res .obtainTypedArray(res.getIdentifier(styleName, "array", context.getPackageName())); Paint fill = AndroidGraphicFactory.INSTANCE.createPaint(); float density = context.getResources().getDisplayMetrics().density; fill.setColor(style.getColor(STYLE_FILL, R.color.circle_gray_fill)); fill.setStyle(Style.FILL); Paint stroke = AndroidGraphicFactory.INSTANCE.createPaint(); stroke.setColor(style.getColor(STYLE_STROKE, R.color.circle_gray_stroke)); stroke.setStrokeWidth(Math.max(1.5f * density, 1)); stroke.setStyle(Style.STROKE); Circle circle = new Circle(latLong, acc, fill, stroke); mapCircles.put(pr, circle); layers.add(circle); circle.setVisible(visible); // Marker layer Drawable drawable = style.getDrawable(STYLE_MARKER); Bitmap bitmap = AndroidGraphicFactory.convertToBitmap(drawable); Marker marker = new Marker(latLong, bitmap, 0, -bitmap.getHeight() * 9 / 20); mapMarkers.put(pr, marker); layers.add(marker); marker.setVisible(visible); style.recycle(); } // move layers into view updateMap(); } @Override public void onPause() { super.onPause(); mapDownloadLayer.onPause(); } @Override public void onResume() { super.onResume(); mapDownloadLayer.onResume(); } @Override public void onStart() { super.onStart(); LayerManager layerManager = mapMap.getLayerManager(); Layers layers = layerManager.getLayers(); layers.clear(); float lat = mainActivity.mSharedPreferences.getFloat(SettingsActivity.KEY_PREF_MAP_LAT, 360.0f); float lon = mainActivity.mSharedPreferences.getFloat(SettingsActivity.KEY_PREF_MAP_LON, 360.0f); if ((lat < 360.0f) && (lon < 360.0f)) { mapMap.getModel().mapViewPosition.setCenter(new LatLong(lat, lon)); } int zoom = mainActivity.mSharedPreferences.getInt(SettingsActivity.KEY_PREF_MAP_ZOOM, 16); mapMap.getModel().mapViewPosition.setZoomLevel((byte) zoom); /* TileRendererLayer tileRendererLayer = new TileRendererLayer(tileCache, mapMap.getModel().mapViewPosition, false, AndroidGraphicFactory.INSTANCE); //FIXME: have user select map file tileRendererLayer.setMapFile(new File(Environment.getExternalStorageDirectory(), "org.openbmap/maps/germany.map")); tileRendererLayer.setXmlRenderTheme(InternalRenderTheme.OSMARENDER); //tileRendererLayer.setTextScale(1.5f); layers.add(tileRendererLayer); */ mapDownloadLayer = new TileDownloadLayer(mapTileCache, mapMap.getModel().mapViewPosition, onlineTileSource, AndroidGraphicFactory.INSTANCE); layers.add(mapDownloadLayer); //parse list of location providers onLocationProvidersChanged(mainActivity.mSharedPreferences.getStringSet(SettingsActivity.KEY_PREF_LOC_PROV, new HashSet<String>(Arrays .asList(new String[] { LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER })))); } @Override public void onStop() { LatLong center = mapMap.getModel().mapViewPosition.getCenter(); byte zoom = mapMap.getModel().mapViewPosition.getZoomLevel(); SharedPreferences.Editor spEditor = mainActivity.mSharedPreferences.edit(); spEditor.putFloat(SettingsActivity.KEY_PREF_MAP_LAT, (float) center.latitude); spEditor.putFloat(SettingsActivity.KEY_PREF_MAP_LON, (float) center.longitude); spEditor.putInt(SettingsActivity.KEY_PREF_MAP_ZOOM, zoom); spEditor.commit(); super.onStop(); if (mapMap != null) mapMap.getLayerManager().getLayers().remove(mapDownloadLayer); if (mapDownloadLayer != null) mapDownloadLayer.onDestroy(); } /** * Updates the list of styles to use for the location providers. * * This method updates the internal list of styles to use for displaying * locations on the map, assigning a style to each location provider. * Styles that are defined in {@link SharedPreferences} are preserved. If * none are defined, the GPS location provider is assigned the red style * and the network location provider is assigned the blue style. The * passive location provider is not assigned a style, as it does not send * any locations of its own. Other location providers are assigned one of * the following styles: green, orange, purple. If there are more location * providers than styles, the same style (including red and blue) can be * assigned to multiple providers. The mapping is written to * SharedPreferences so that it will be preserved even as available * location providers change. */ public void updateLocationProviderStyles() { //FIXME: move code into assignLocationProviderStyle and use that List<String> allProviders = mainActivity.locationManager.getAllProviders(); allProviders.remove(LocationManager.PASSIVE_PROVIDER); if (allProviders.contains(LocationManager.GPS_PROVIDER)) { providerStyles.put(LocationManager.GPS_PROVIDER, mainActivity.mSharedPreferences.getString( SettingsActivity.KEY_PREF_LOC_PROV_STYLE + LocationManager.GPS_PROVIDER, LOCATION_PROVIDER_RED)); mAvailableProviderStyles.remove(LOCATION_PROVIDER_RED); allProviders.remove(LocationManager.GPS_PROVIDER); } if (allProviders.contains(LocationManager.NETWORK_PROVIDER)) { providerStyles.put(LocationManager.NETWORK_PROVIDER, mainActivity.mSharedPreferences.getString( SettingsActivity.KEY_PREF_LOC_PROV_STYLE + LocationManager.NETWORK_PROVIDER, LOCATION_PROVIDER_BLUE)); mAvailableProviderStyles.remove(LOCATION_PROVIDER_BLUE); allProviders.remove(LocationManager.NETWORK_PROVIDER); } for (String prov : allProviders) { if (mAvailableProviderStyles.isEmpty()) mAvailableProviderStyles.addAll(Arrays.asList(LOCATION_PROVIDER_STYLES)); providerStyles.put(prov, mainActivity.mSharedPreferences .getString(SettingsActivity.KEY_PREF_LOC_PROV_STYLE + prov, mAvailableProviderStyles.get(0))); mAvailableProviderStyles.remove(providerStyles.get(prov)); } ; SharedPreferences.Editor spEditor = mainActivity.mSharedPreferences.edit(); for (String prov : providerStyles.keySet()) spEditor.putString(SettingsActivity.KEY_PREF_LOC_PROV_STYLE + prov, providerStyles.get(prov)); spEditor.commit(); } /** * Updates the map view so that all markers are visible. */ public void updateMap() { boolean needsRedraw = false; Dimension dimension = mapMap.getModel().mapViewDimension.getDimension(); // just trigger a redraw if we're not going to pan or zoom if ((dimension == null) || (!isMapViewAttached)) { mapMap.getLayerManager().redrawLayers(); return; } // move locations into view and zoom out as needed int tileSize = mapMap.getModel().displayModel.getTileSize(); BoundingBox bb = null; BoundingBox bb2 = null; for (Location l : providerLocations.values()) if ((l != null) && (l.getProvider() != "")) { double lat = l.getLatitude(); double lon = l.getLongitude(); double yRadius = l.hasAccuracy() ? ((l.getAccuracy() * 360.0f) / EARTH_CIRCUMFERENCE) : 0; double xRadius = l.hasAccuracy() ? (yRadius * Math.abs(Math.cos(lat))) : 0; double minLon = Math.max(lon - xRadius, -180); double maxLon = Math.min(lon + xRadius, 180); double minLat = Math.max(lat - yRadius, -90); double maxLat = Math.min(lat + yRadius, 90); if (!isLocationStale(l)) { // location is up to date, add to main BoundingBox if (bb != null) { minLat = Math.min(bb.minLatitude, minLat); maxLat = Math.max(bb.maxLatitude, maxLat); minLon = Math.min(bb.minLongitude, minLon); maxLon = Math.max(bb.maxLongitude, maxLon); } bb = new BoundingBox(minLat, minLon, maxLat, maxLon); } else { // location is stale, add to stale BoundingBox if (bb2 != null) { minLat = Math.min(bb2.minLatitude, minLat); maxLat = Math.max(bb2.maxLatitude, maxLat); minLon = Math.min(bb2.minLongitude, minLon); maxLon = Math.max(bb2.maxLongitude, maxLon); } bb2 = new BoundingBox(minLat, minLon, maxLat, maxLon); } } if (bb == null) bb = bb2; // all locations are stale, center to them if (bb == null) { needsRedraw = true; } else { byte newZoom = LatLongUtils.zoomForBounds(dimension, bb, tileSize); if (newZoom < 0) newZoom = 0; if (newZoom < mapMap.getModel().mapViewPosition.getZoomLevel()) { mapMap.getModel().mapViewPosition.setZoomLevel(newZoom); } else { needsRedraw = true; } MapViewProjection proj = new MapViewProjection(mapMap); Point nw = proj.toPixels(new LatLong(bb.maxLatitude, bb.minLongitude)); Point se = proj.toPixels(new LatLong(bb.minLatitude, bb.maxLongitude)); // move only if bb is not entirely visible if ((nw.x < 0) || (nw.y < 0) || (se.x > dimension.width) || (se.y > dimension.height)) { mapMap.getModel().mapViewPosition.setCenter(bb.getCenterPoint()); } else { needsRedraw = true; } } if (needsRedraw) mapMap.getLayerManager().redrawLayers(); } }