-
Notifications
You must be signed in to change notification settings - Fork 75
Google maps replacement
Here we replace Google Map v2 for Android (with SupportMapFragment) to Nutiteq SDK. It took no more than about one man-day work from us, with quite common map functionality:
- show interactive background map with OpenStreetMap
- show user (GPS) location on map
- show markers on map
- select a marker, show Info Window with object name
- show a line on map
Additional features provided by Nutiteq SDK:
- offline map: users will have city map bundled with application, therefore app does not require online connection for basic use. This is important for certain user groups who do not have data plan.
This application had map functionality in own class, MapFragment.java, this is usually good pattern and it makes also Google Maps replacing easier.
Note that Nutiteq SDK does not provide direct compatibility layer to Google Maps API, and some API principles are a bit different. Therefore, API calls need to be changed, but very basic things are still the same: you have map view with interactive browsing built-in, your application-specific objects (markers and others) to be added on it and events to be handled.
1. Remove imports to Google Maps
- remove imports from java file, all import com.google.android.gms.maps...
- this will reveal quickly all the lines which need to be changed in your IDE
2. Change project configuration
- Remove Google Maps references from AndroidManifest.xml file, no need to add anything about Nutiteq there. Only requirement is INTERNET uses-permission.
- Your app will be installable to Android devices without Google Maps / Google Services.
- Change project target from "Google API" to generic Android. Anything above version 9 (2.3) is fine.
- Add Nutiteq SDK to your project. See Downloads how you can do it with dependency management (Maven or Gradle/Android Studio) or directly with jar file.
3. Remove extends SupportMapFragment (or MapActivity) from your map fragment/activity
- Nutiteq SDK does not require you to extend specific Fragment or Activity, you can just base on general Activity of Fragment, or use your own base class. Nutiteq MapView is like just any View and you can define it in Layout.
- In this case we replace "extends SupportMapFragment" with plain "extends Fragment".
4. Initialize map
Here I start with online map source to show fast results. I'll change it with offline map layer source later. Nutiteq map setup needs some explicit definitions, which are hidden in Google Maps. Map setup is in own method setupMapView:
// class member for map view
MapView mapView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View mainView = inflater.inflate(R.layout.fragment_map, container, false);
setupMapView(mainView);
}
private void setupMapView(View parentView) {
// Nutiteq SDK
// enable logging for troubleshooting - optional
Log.enableAll();
Log.setTag("myMapApp");
// Nutiteq MapView initialization and adding to layout
// You could also define MapView with a Layout as any other View
mapView = new MapView(getActivity());
mapView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
// create internal settings for MapView
mapView.setComponents(new Components());
// Create base layer, online source here from MapQuest Open Tiles
RasterDataSource dataSource = new HTTPRasterDataSource(new EPSG3857(), 0, 18, "http://otile1.mqcdn.com/tiles/1.0.0/osm/{zoom}/{x}/{y}.png");
RasterLayer baseLayer = new RasterLayer(dataSource, 1);
mapView.getLayers().setBaseLayer(baseLayer);
// Activate some mapview options to make map smoother
mapView.getOptions().setPreloading(true);
mapView.getOptions().setSeamlessHorizontalPan(true);
mapView.getOptions().setTileFading(true);
mapView.getOptions().setKineticPanning(true);
mapView.getOptions().setDoubleClickZoomIn(true);
mapView.getOptions().setDualClickZoomOut(true);
// set sky bitmap
// NB! you need background_plane.png and sky_small.png files in res/drawable folder
// start with samples from https://github.com/nutiteq/hellomap3d/tree/master/HelloMap3D/res/drawable
mapView.getOptions().setSkyDrawMode(Options.DRAW_BITMAP);
mapView.getOptions().setSkyOffset(4.86f);
mapView.getOptions().setSkyBitmap(
UnscaledBitmapLoader.decodeResource(getResources(),
R.drawable.sky_small));
// Map background, visible if no map tiles loaded - optional, default - white
mapView.getOptions().setBackgroundPlaneDrawMode(Options.DRAW_BITMAP);
mapView.getOptions().setBackgroundPlaneBitmap(
UnscaledBitmapLoader.decodeResource(getResources(),
R.drawable.background_plane));
mapView.getOptions().setClearColor(Color.WHITE);
// add MapView in code as child of a LinearLayout container
// as alternative you could define MapView directly in your layout
final LinearLayout mapContainer = (LinearLayout) parentView.findViewById(R.id.map_container);
mapContainer.addView(mapView);
}
// you have to start and stop map processes with the Fragment:
@Override
public void onStart() {
super.onStart();
mapView.startMapping();
}
@Override
public void onStop() {
mapView.stopMapping();
super.onStop();
}
5. Adding Markers to map. This does not require any bigger structural changes, just method signatures and parameters are different. Note that Google Maps InfoWindow is called Label in Nutiteq terms.
a) Google code has a lot of default values, so the code will look shorter:
MarkerOptions options = new MarkerOptions();
options.anchor(0.5f, 0.5f).title("London").snippet("Textual data").position(new LatLng(57.2, -0.1)).icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_city)).draggable(false);
Marker marker = getMap().addMarker(options);
// make the added Marker InfoWindow popup visible
marker.showInfoWindow();
b) Nutiteq SDK, with samples how to customize styles:
// add a layer for Markers, do only once during map initialization
MarkerLayer markerLayer = new MarkerLayer(mapView.getLayers().getBaseProjection());
mapView.getLayers().addLayer(markerLayer);
// define style for Marker. Here one style for all Markers, so need to do only once
MarkerStyle markerStyle = MarkerStyle.builder().setSize(0.5f).setBitmap(
UnscaledBitmapLoader.decodeResource(getResources(), R.drawable.ic_city)
).build();
// style for marker labels, scale it based on device DPI, and recolor to be similar to Google default black
final float scale = getResources().getDisplayMetrics().density;
labelStyle =
LabelStyle.builder()
.setBackgroundColor(Color.BLACK)
.setTitleColor(Color.WHITE)
.setEdgePadding((int) (12 * scale))
.setLinePadding((int) (12 * scale))
.setTipSize((int) (15 * scale))
.setTitleFont(Typeface.create("Arial", Typeface.BOLD), 19 * scale)
.setDescriptionFont(Typeface.create("Arial", Typeface.NORMAL), 17 * scale) // not used here
.build();
// following for each added markers:
// define location in real layer projection. NB! coordinates are here X and Y, which are swapped from to Google lat,long order
MapPos pos = mapView.getLayers().getBaseProjection().fromWgs84(-0.1f, 57.2f);
// define Label, which is same as InfoWindow in Google terms
// second line string can be null, or multi-line
DefaultLabel label = new DefaultLabel("London", "This is a city", labelStyle);
// create marker and add to map
Marker marker = new Marker(pos, label, markerStyle, "Any Object as extra data, can be String or null");
markerLayer.add(marker);
// finally open Label for one of the markers, for this the Marker has to be selected. Do this for one Marker only:
mapView.selectVectorElement(marker);
Performance note: Markers in Nutiteq SDK are Billboards, standing bitmaps, which will "stand up" when you tilt the map view. On non-tilted it looks just same as Point object, which lays down on the ground. Rendering of Markers is technically significantly more expensive, as you can define that Markers will not be overlapping. So if you want just points on map, and do not use map tilting (2.5D view), then we suggest to use Point object instead of Markers. Also Points will look more similar to Google Maps which does not have Billboard-style Marker display at all. It is easy to switch them:
- The styles and object constructors are very similar to Marker and Point objects, so search-replace Marker with Point
- Use GeometryLayer instead of MarkerLayer where to put Points (there is no PointLayer)
6. Adding Line to map
a) Google maps code:
// line coordinates are in ArrayList<LatLng> lineCoords
PolylineOptions options = new PolylineOptions();
options.color(getResources().getColor(R.color.map_route_color));
for(LatLng latLng : lineCoords) {
options.add(latLng);
}
getMap().addPolyline(options);
b) Nutiteq SDK code:
// define style for line
LineStyle routeLineStyle = LineStyle.builder().setColor(getResources().getColor(R.color.map_route_color)).setWidth(0.1f).build();
// Create layer for Line geometries and add it to MapView
GeometryLayer routeLineLayer = new GeometryLayer(mapView.getLayers().getBaseProjection());
mapView.getLayers().addLayer(routeLineLayer);
// Create line and add it to layer
// line coordinates are in ArrayList<MapPos> lineCoords
Line routeLine = new Line(lineCoords, null, routeLineStyle, null);
routeLineLayer.add(routeLine);
7. Map event listeners: touches and map movements
- This is most complex part here, the only place where you need a bit structural changes. Nutiteq SDK MapListener is not Interface, it is Abstract Class, so you cannot implement it, and you have to extend this.
- In our sample case MapFragment itself was implementing MapListeners (GoogleMap.OnMarkerClickListener etc). As MapFragment extends Fragment, then it cannot extend Nutiteq MapListener and we need to use separate class MapEventListener (which extends MapListener). It can a private inner class, inside same .java file.
- If your new listener is separate public Class, then you do not have direct access to the class members, as you would have with interface implementation. Passing needed members (including Fragment itself) as constructor parameters would be standard way. If it is inner (nested) class, then you can use fully qualified name: MapFragment.this.variableName to access outer class members, instead of just variableName as you would have with Interface implementation.
Example:
a) Map Marker click event listener with Google maps:
public class MapFragment extends SupportMapFragment implements GoogleMap.OnMarkerClickListener {
…
map.setOnMarkerClickListener(this);
…
@Override
public boolean onMarkerClick(Marker marker) {
// do something with clicked Marker
...
return false;
}
b) Corresponding Map Marker event listener with Nutiteq SDK:
public class MapFragment extends Fragment{
…
// Add map event listener
MyMapEventListener mapListener = new MyMapEventListener();
mapView.getOptions().setMapListener(mapListener);
…
// implement map event listener as nested class in same MapFragment
class MyMapEventListener extends MapListener {
public MyMapEventListener() {
}
@Override
public void onVectorElementClicked(VectorElement vectorElement, double x, double y, boolean longClick) {
// vectorElement - clicked object, could be Marker, Point etc
// x, y - point on where click was done. Same as vectorElement location for Points and Markers, useful for Line and Polygon
// longClick - false if short click, true if touch lasts more than 400ms
if(vectorElement instanceof Marker){
// do something specific to Marker
}
}
} // end class MyMapEventListener
} // end of MapFragment
8. Set map center location (camera)
a) In Google code you would use moveCamera or animateCamera:
getMap().moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(latitude, longitude), ZOOM_STREET_LEVEL));
b) In Nutiteq code there is setFocusPoint:
// Note: longitude and latitude (x and y) are swapped in position constructors compared to Google!
mapView.setFocusPoint(mapView.getLayers().getBaseProjection().fromWgs84(longitude, latitude));
mapView.setZoom(ZOOM_STREET_LEVEL);
- If you want to animate Camera then add additional (last one) duration parameter value to mapView.setFocusPoint or mapView.setZoom methods. Value is in milliseconds, 500 would be similar to default Google speed.
- If you want to animate camera to given area (to newLatLngBounds in Google terms) instead of center point, then find out bound coordinates in map layer projection which are (usually) not latitude and longitude - use projection.fromWgs84() as given above and then:
mapView.setBoundingBox(new Bounds(xMin, yMax, xMax, yMin), true, 500);
9. Enable offline map
Key advantage of Nutiteq SDK is that you can select from wide variety of base map sources: you can use online or offline maps, different API-s (tiles, WMS), vector or raster sources and files, in-house, free OpenStreetMap or commercial map providers, you can use and even mix different map projections. With Google API you always must use their maps, with their mandatory branding and advertising. And their map source does not support offline maps.
Above we used a free OpenStreetMap online map data source, in sample above the map tiles are provided by MapQuest. This is simple to get started, and in fact you can use same source for free, even for commercial apps.
Offline maps involves additional map data which needs to be added to the application. Nutiteq SDK supports different caching levels for this, and you can pick according to your needs:
- no map data is pre-loaded, online tiles are persistently cached
- map data is bundled with application APK, as raw image resources. APK has size limit of 50MB
- map data is bundled as asset file, in MBTiles or MapsForge format. Also APK total size limit applies.
- map data is APK extension file. With this there is no APK size limit.
- map data is downloaded by application with user request, or automatically during first app startup.
If the map data is single package (options 3. to 5. above) then application needs to copy data to device, to application data folder or SDCARD (so-called external storage). Nutiteq SDK itself does not provide functions for this, this needs to be handled by application. Also you would need to make sure that the data is deleted with application uninstall.
If map data package is not too big (say about 20MB and 2000 map tiles) then the easiest way would be to bundle maps as raw image resources, this is option 2. above. This increases APK size, but there is no extra copy of data as the map tiles are loaded directly from APK. If the map tiles are with name pattern t{zoom}{x}{y}.png, then copy them to /res/raw folder and change map data source definition line to use PackagedRasterDataSource:
// Replace online datasource with Packaged raster
// RasterDataSource dataSource = new HTTPRasterDataSource(new EPSG3857(), 0, 18, "http://otile1.mqcdn.com/tiles/1.0.0/osm/{zoom}/{x}/{y}.png");
PackagedRasterDataSource dataSource = new PackagedRasterDataSource(new EPSG3857(), 0, 17, "t{zoom}_{x}_{y}", getActivity());
baseLayer = new RasterLayer(dataSource, 1);
mapView.getLayers().setBaseLayer(baseLayer);
Files in project res/raw should look like following:
See following pages for other map sources:
- Tile Sources - various online map types
- Offline map tiles - more info about offline map options
10. My Location indicator a) Google has built-in, simple to use, but not much configurable My Location animated indicator, and built-in button for it. You would enable this with just one line of code:
map.setMyLocationEnabled(true);
b) Nutiteq SDK does not have it, as by design it gives full control to the application. This means own decisions about layout and styles, and more code. Here are two features: animated location circle, and a UI button which jumps map to GPS location.
- Animated location circle. Here are relevant lines of code, from HelloMap3D project. Basically it draws a circle-shaped Line to a GeometryLayer, and changes vertex coordinates based on user location and location accuracy.
onCreate{
// 1. Create MapView layer for location circle
locationLayer = new GeometryLayer(mapView.getLayers().getBaseProjection());
mapView.getComponents().layers.addLayer(locationLayer);
// 2. add GPS My Location functionality as a separate class
final MyLocationCircle locationCircle = new MyLocationCircle(locationLayer);
initGps(locationCircle);
// 3. Run animation to update location circle
locationTimer = new Timer();
locationTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
locationCircle.update(mapView.getZoom());
}
}, 0, 50);
}
protected void initGps(final MyLocationCircle locationCircle) {
final Projection proj = mapView.getLayers().getBaseLayer().getProjection();
locationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
locationCircle.setLocation(proj, location);
locationCircle.setVisible(true);
// recenter automatically to GPS point
// TODO in real app this can be annoying for user, you should turn it off when user has moved map after recenter
mapView.setFocusPoint(mapView.getLayers().getBaseProjection().fromWgs84(location.getLongitude(), location.getLatitude()), 500);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
Log.debug("GPS onStatusChanged "+provider+" to "+status);
}
@Override
public void onProviderEnabled(String provider) {
Log.debug("GPS onProviderEnabled");
}
@Override
public void onProviderDisabled(String provider) {
Log.debug("GPS onProviderDisabled");
}
};
LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
List<String> providers = locationManager.getProviders(true);
for(String provider : providers){
locationManager.requestLocationUpdates(provider, 10000, 100, locationListener);
}
}
The referred MyLocationCircle.java can be taken from HelloMap3D project: https://github.com/nutiteq/hellomap3d/blob/master/HelloMap3D/src/com/nutiteq/hellomap/MyLocationCircle.java
You probably want to add a "My Location" button to the top of map. You can use Android usual view system, and RelativeLayout to overlay two views, with a Button on top of MapView. Click on button initializes GPS.
11. Troubleshoot and have fun
If you have issues, comments and improvement ideas, please post to nutiteq-dev list, all Nutiteq SDK users are welcome here.
- email: [email protected]
- web forum: https://groups.google.com/forum/#!forum/nutiteq-dev
We also private support and customisation services. Please email [email protected] for info.