Jaak Laineste, head of mobile, CARTO
Aare Undo, mobile developer, CARTO
Link to presentation: https://docs.google.com/presentation/d/1I1yIHg0WlkbnKcodz3MMEcd6eMxHovcV6vcRJfMNFxk
A computer with:
- Linux, Mac or Windows. Simplest for everyday use would be Mac
- Additional permissions to install software (Java JDK and USB Drivers) are needed
- Workshop computers are Linux OSGEO Live, so we use this as base
- Android Studio, includes everything needed for Android app development (Eclipse is deprecated)
Required skills:
- Java development knowledge basics
- CARTO homepage: https://carto.com/docs/
- Support: [email protected]
- Public discussions: http://gis.stackexchange.com/ - tag with #carto
Download Android studio installer. Several ways to get it:
- Go to Android Developer site, download .zip file or installer for your platform.
- Google “Android Studio 2.1.2”
- Firefox does not want to show download in Ubuntu. Alternative download: https://goo.gl/Xl36n3
Unzip android studio zip file
sudo apt-get update
sudo apt-get install lib32stdc++6
And if you plan on using the emulator, you also need to install the following:
sudo apt-get install lib64stdc++6
cd Android/Sdk/tools/lib64/libstdc++
mv libstdc++.so.6 libstdc++.so.6.bak
ln -s /usr/lib64/libstdc++.so.6 ./
cd Downloads/android-studio/bin
./studio.sh
In broad terms, the general (basic) user interface setup of Android consists of two components: the activity the view (it’s a bit more complicated, but let’s leave it at that for now).
An Activity represents a single screen with a user interface. For example, an email app might have one activity that shows a list of new emails, another activity to compose an email, and another activity for reading emails. Activities are in charge of updating the data a certain view displays.
Example of a basic activity with view inflation and click-handlers:
A view in android is, generally, written in XML and inflated when a certain activity becomes “active” (is visible on the screen).
Example of a basic view file in xml (here you can see the id “toolbar” that we inflate in the previous image):
Now, when working with the CARTO, we use the same approach, but we use a custom class to indicate that a map should be shown instead of a button or a text field:
However, in order to be able to make use of CARTO’s map, we first need to import the SDK. Download CARTO’s latest mobile SDK from the following link: https://nutifront.s3.amazonaws.com/sdk_snapshots/sdk4-android-snapshot-latest.zip
Now we’ve, finally, reached the point where you should open Android Studio and starting coding! After opening Android Studio, create a new project and set a unique Application name and Company domain (this is important when registering for a license on carto.com).
We don’t really want much example code in our example, so choose an Empty Activity:
We use Maven dependency mechanism to include our SDK. To download the library as such, add the following lines to your build.gradle file and press Sync now
allprojects {
repositories {
jcenter()
maven {
url "http://repository-nutiteq.forge.cloudbees.com/release/"
}
maven {
url "http://repository-nutiteq.forge.cloudbees.com/snapshot/"
}
}
}
dependencies {
compile 'com.carto:carto-mobile-sdk:snapshot@aar'
}
Now you should be ready to start using CARTO’s SDK. Go ahead and replace your activity’s main view with the following snippet:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minWidth="25dp"
android:minHeight="25dp">
<com.carto.ui.MapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
And in your activity’s onCreate method reference that view as such:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MapView mapView = (MapView)findViewById(R.id.map_view);
}
It should show a light-bluepopup prompting you to import a package. Import it and the red squiggly lines should disappear :).
Finally, now comes the time where you make use of the license you received when you registered on carto.com and created a new application.
Registering your license requires internet access and android requires that you add a permission beforehand. Be sure add the following to your AndroidManifest.xml right after the tag:
<uses-permission android:name="android.permission.INTERNET"/>
Normally you get a license key by logging in to https://carto.com/ and registering a new mobile application, but we have created a temporary license key for this event:
XTUN3Q0ZDc1BGVmtzSng1bjVMdHNJbmFrc0I3d2psT3hBaFFTTUF1L21NSCt1M3FQcnYrYkxOWnJqTFRUbnc9PQoKcHJvZHVjdHM9c2RrLWFuZHJvaWQtNC4qLHNkay1pb3MtNC4qCnBhY2thZ2VOYW1lPSoKd2F0ZXJtYXJrPWN1c3RvbQp2YWxpZFVudGlsPTIwMTYtMDktMDEKdXNlcktleT0zNjNmMTc3Y2YzNjUwMzBmMWVlOGI5M2NiZjU2NzhkYQo=
Now that you’ve got your key, add the following line to your activity:
static final String LICENSE = "<YOUR-LICENSE-HERE>";
Now, in your activity’s onCreate, register your license:
MapView.registerLicense(LICENSE, getApplicationContext());
Once the license is registered, you can initialize your map and add a base layer to it (mapView was declared as a class variable):
setContentView(R.layout.activity_hello_map);
mapView = (MapView) this.findViewById(R.id.map_view);
CartoOnlineVectorTileLayer layer =
new CartoOnlineVectorTileLayer(CartoBaseMapStyle.CARTO_BASEMAP_STYLE_DEFAULT);
mapView.getLayers().add(layer);
At this point you should run your application and see what happens. If everything is correct, you should see something like this:
For our next example, let’s try zooming in to Berlin. First, we find out the latitude and longitude of Berlin, which happen to be 13.38933 and 52.51704, then add the following snippet to your code and it should zoom in to Berlin:
Projection projection = mapView.getOptions().getBaseProjection();
MapPos berlin = projection.fromWgs84(new MapPos(13.38933, 52.51704));
mapView.setFocusPos(berlin, 0);
mapView.setZoom(10, 0);
The result should look something like this:
That’s pretty cool, isn’t it? But we’re not done just yet. Now we finally get to use the sample vis.json url and see what happens. Add the following method to your to your activity:
protected void updateVis(final String url) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
mapView.getLayers().clear();
// Create overlay layer for popups
Projection proj = mapView.getOptions().getBaseProjection();
LocalVectorDataSource dataSource = new LocalVectorDataSource(proj);
VectorLayer vectorLayer = new VectorLayer(dataSource);
// Create VIS loader
CartoVisLoader loader = new CartoVisLoader();
loader.setDefaultVectorLayerMode(true);
MyCartoVisBuilder visBuilder = new MyCartoVisBuilder(vectorLayer);
try {
loader.loadVis(visBuilder, url);
}
catch (IOException e) {
Log.e("EXCEPTION", "Exception: " + e);
}
// Add the created popup overlay layer on top of all visJSON layers
mapView.getLayers().add(vectorLayer);
}
});
thread.start(); // TODO: should serialize execution
}
And invoke it in your activity's onCreate (after zooming in to Berlin):
String url = "http://documentation.carto.com/api/v2/viz/2b13c956-e7c1-11e2-806b-5404a6a683d5/viz.json";
updateVis(url);
You may notice that you can’t seem to be able to “fix” that one squiggly red line under MyCartoVisBuilder. That’s because it’s a custom class we create ourselves, inheriting from CartoVisBuilder. Here it is, copy it into your activity class:
private class MyCartoVisBuilder extends CartoVisBuilder {
private VectorLayer vectorLayer; // vector layer for popups
public MyCartoVisBuilder(VectorLayer vectorLayer) {
this.vectorLayer = vectorLayer;
}
@Override
public void setCenter(MapPos mapPos) {
MapPos position = mapView.getOptions().getBaseProjection().fromWgs84(mapPos);
mapView.setFocusPos(position, 1.0f);
}
@Override
public void setZoom(float zoom) {
mapView.setZoom(zoom, 1.0f);
}
@Override
public void addLayer(Layer layer, Variant attributes) {
// Add the layer to the map view
mapView.getLayers().add(layer);
}
}
If you used the sample url we provided, your screen should look like this:
Log in to https://carto.com/ and go to your dashboard (left click on your avatar) and press NEW MAP. Then you have to option to connect a data set or use an existing one and create a map.
Then you have the option to manipulate with the data to enhance the map. Try different options from the tabs on the right.
When you’re done, press publish and replace the sample url of the application with your CartoDB.js url.
Now it’s time to add a marker to our custom map. Copy the following method to your activity:
private void addMarkerToPosition(MapView map, MapPos wgsPosition)
{
// Create a new layer
Projection projection = map.getOptions().getBaseProjection();
LocalVectorDataSource datasource = new LocalVectorDataSource(projection);
VectorLayer layer = new VectorLayer(datasource);
// Add layer to map
map.getLayers().add(layer);
MarkerStyleBuilder builder = new MarkerStyleBuilder();
builder.setSize(30);
builder.setColor(new Color(android.graphics.Color.GREEN));
// Set marker position and style
MapPos position = projection.fromWgs84(wgsPosition);
MarkerStyle style = builder.buildStyle();
// Create marker and add it to the source
Marker marker = new Marker(position, style);
datasource.add(marker);
}
And invoke it right after adding the vis.json layer to your map as such:
MapPos tallinn = new MapPos(24.646469, 59.426939);
addMarkerToPosition(mapView, tallinn);
The final result should look like this:
Now, how about we make the map interactive?
CARTO Mobile SDK can also listen to map clicks. Copy the following class to your activity:
private class MyMapEventListener extends MapEventListener {
private MapView mapView;
private LocalVectorDataSource vectorDataSource;
private BalloonPopup oldClickLabel;
public MyMapEventListener(MapView mapView, LocalVectorDataSource vectorDataSource) {
this.mapView = mapView;
this.vectorDataSource = vectorDataSource;
}
@Override
public void onMapMoved() {
}
@Override
public void onMapClicked(MapClickInfo mapClickInfo) {
// Remove old click label
if (oldClickLabel != null) {
vectorDataSource.remove(oldClickLabel);
oldClickLabel = null;
}
BalloonPopupStyleBuilder styleBuilder = new BalloonPopupStyleBuilder();
// Make sure this label is shown on top all other labels
styleBuilder.setPlacementPriority(10);
MapPos position = mapClickInfo.getClickPos();
BalloonPopupStyle style = styleBuilder.buildStyle();
MapPos wgs84Position = mapView.getOptions().getBaseProjection().toWgs84(position);
String title = "You just clicked at:";
String description = String.format(Locale.US, "%.4f, %.4f", wgs84Position.getY(), wgs84Position.getX());
BalloonPopup clickPopup = new BalloonPopup(position, style, title, description);
vectorDataSource.add(clickPopup);
oldClickLabel = clickPopup;
}
}
And then invoke it after adding your first marker:
LocalVectorDataSource clickSource = new LocalVectorDataSource(proj);
VectorLayer clickLayer = new VectorLayer(clickSource);
mapView.getLayers().add(clickLayer);
mapView.setMapEventListener(new MyMapEventListener(mapView, clickSource));
Now, if you click on your map, it should look something like this:
So far we've covered what we can do online, but our base map also works offline. Let’s download a package. To do that, we must first create a package manager. But first, let’s clean up our current activity. Just remove or comment out the following lines:
CartoOnlineVectorTileLayer layer =
new CartoOnlineVectorTileLayer(CartoBaseMapStyle.CARTO_BASEMAP_STYLE_DEFAULT);
mapView.getLayers().add(layer);
Projection projection = mapView.getOptions().getBaseProjection();
MapPos berlin = projection.fromWgs84(new MapPos(13.38933, 52.51704));
mapView.setFocusPos(berlin, 0);
mapView.setZoom(10, 0);
updateVis();
Then add the following class variables:
static final String bonn = "bbox(7.0082,50.7284,7.1582,50.7454)";
CartoPackageManager manager;
Now we’re going to create a directory for map packages, if it doesn’t already exist:
File folder = new File(getApplicationContext().getExternalFilesDir(null), "map_packages");
if (!folder.isDirectory()) {
folder.mkdir();
}
Then we initialize our package manager and start package download, if it’s not already downloaded:
try {
manager = new CartoPackageManager("nutiteq.osm", folder.getAbsolutePath());
}
catch (IOException e) {
System.out.println("Exception: " + e.getMessage());
}
if (manager == null) {
Toast.makeText(this, "Unable to initialize package manager", Toast.LENGTH_LONG).show();
return;
}
manager.start();
if (manager.getLocalPackage(bonn) == null) {
manager.startPackageDownload(bonn);
}
However, before we can start using the offline map, we need to create an asset folder:
Now download nutibright-v3.zip from https://nutifront.s3.amazonaws.com/releases/nutibright-v3.zip and add copy it to your asset folder.
And finally we need to create and add the actual layer and zoom in to Bonn. Copy the following snippet to your activity’s onCreate:
PackageManagerTileDataSource source = new PackageManagerTileDataSource(manager);
BinaryData styleBytes = AssetUtils.loadAsset("nutibright-v3.zip");
CompiledStyleSet styleSet = new CompiledStyleSet(new ZippedAssetPackage(styleBytes));
MBVectorTileDecoder decoder = new MBVectorTileDecoder(styleSet);
VectorTileLayer layer = new VectorTileLayer(source, decoder);
mapView.getLayers().add(layer);
MapPos bonnPos = mapView.getOptions().getBaseProjection().fromWgs84(new MapPos(7.0982, 50.7374));
mapView.setFocusPos(bonnPos, 0);
mapView.setZoom(14, 0);
And now Bonn is available to you offline!