Skip to content

Instantly share code, notes, and snippets.

@jaakla
Forked from Nikituh/mobile_gis_workshop.md
Last active August 10, 2017 09:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jaakla/26dea2f4f981a1c58a65159528fa8465 to your computer and use it in GitHub Desktop.
Save jaakla/26dea2f4f981a1c58a65159528fa8465 to your computer and use it in GitHub Desktop.
Mobile GIS workshop

Your first native mobile GIS app with CARTO SDK

alt text

Jaak Laineste, @jaakl

Preparation

A computer with:

  • Linux, Mac or Windows
  • Additional permissions to install software (Java JDK and USB Drivers) may be needed depending on your OS
  • Android Studio, includes everything needed for Android app development

A mobile device:

  • Decent Android device with Android version 4.0.2 or newer
  • USB cable to connect device to your computer

Required skills:

  • Development and Java basics

More help

1. Install Android Studio

Download and install software

Download Android studio:

2. Start Android Studio, download Components

  • With first Studio start it downloads and installs required additional Android SDK components and dependencies
  • Standard Install type is just fine, it should show "create new project" wizard when preparations are done.
  • Be patient - many Android Studio steps (updates, builds etc) can take many seconds, even few minutes with very minimal progress indication

3. Prepare and connect to your Android device

  • Enable development mode for your Android device. Exact procedure depends a bit on your device and OS, you can just google "enable development ". Go to Development settings and enable USB Debugging. For example on most devices following works: enable development
  • On some OS-s (like Windows) you may need to install additional USB drivers, see more info here: https://developer.android.com/studio/run/win-usb.html. This is not needed on Mac (and probably Linux)
  • Connect your Android device with USB to the computer and it should ask debug permissions on device. Allow it.
  • Note: if you really want you can try using Android Emulator (a.k.a. virtual device), but this is much slower, and does not have (good) multi-touch for map pinch-zoom/tilt features.
  • To verify connection create any basic new project for mobile device (not Watch) with new project wizard and next-next and run it on device with Run > Run 'app' from menu. You should see your device in list of targets as the first one, and selecting it installs and starts app on it.

Android app principles

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:

basic activity

A Layout (view) in Android is usually 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):

alt text

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:

alt text

Get CARTO SDK

However, in order to be able to make use of CARTO’s map, we first need to import the SDK library.

Creating a new Project

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).

alt text

We don’t really want much example code in our example, so choose an Empty Activity:

alt text

Importing CARTO Mobile SDK

We use Maven dependency mechanism to include our SDK. This downloads the library automatically, just add the following lines to your Module:app build.gradle file and press Sync now

dependencies {
    compile 'com.carto:carto-mobile-sdk:4.0.2@aar'
}

To use latest dev version with latest cool features see https://github.com/CartoDB/mobile-sdk/wiki/Using-dev-build

Implementing CARTO Mobile SDK

Now you should be ready to start using CARTO’s SDK. Replace TextView in your activity’s main Layout xml (under app/res in the Project browser) with com.carto.ui.MapView, as in 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="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</RelativeLayout>

And in your activity’s onCreate method reference that Layout:

@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. Auto-fix it (Command-Enter in Mac) and import... lines will be added to your code and error 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.

Getting online maps to work you need internet access for your app, and android requires that you add a permission beforehand. Be sure add the following to your AndroidManifest.xml (found from app/manifests), right before the tag:

<uses-permission android:name="android.permission.INTERNET"/>

Getting Mobile SDK License

Normally you get a license key by logging in to https://carto.com/ (register Free account if needed) and registering a new mobile application under your Account Settings and API keys. As faster backup we have created a temporary license key:

XTUMwQ0ZGSy9wMWUxUmNzRnNUdEhRRVd3Nzk3emZHOGNBaFVBaUcySW5IMVFDTFNOcnVTOXJWTXppc0h2c0RJPQoKcHJvZHVjdHM9KgpwYWNrYWdlTmFtZT0qCndhdGVybWFyaz1jYXJ0bwp2YWxpZFVudGlsPTIwMTctMTItMDEKYXBwVG9rZW49NGExOTkwYTktNmIyYy00YTcwLThlNzEtOThmZDA1Mjc4MTM3Cg==

Setting CARTO Mobile SDK License

Using your own license key from carto.com "API key" user settings, or key above, set your license like this:

MapView.registerLicense("<YOUR-LICENSE-HERE>", getApplicationContext());

Setting up a basic Map

Once the license is registered, you can initialize your map. CARTO SDK does not predefine any built-in map layers, so usually you want to add at least one base layer to it (mapView was declared as a class variable):

setContentView(R.layout.activity_hello_map);

MapView 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:

default map

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:

// note that we use map projection as base for coordinates
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:

alt text

Adding Markers (and other objects)

Now it’s time to add a marker to our custom map. A GIS person just does not add points to map. There must be Layers in view level to organize data, and we also have concept of DataSource in data model level. So you first add Layer to MapView, then suitable DataSource to a Layer, and finally can add objects to a DataSource. Specific objects are:

  • Projection (from map.getOptions().getBaseProjection()) to convert WGS84 coordinates to mapview projection
  • VectorLayer which handles vector data from different datasources: online, on-memory, from files etc.
  • LocalVectorDataSource which allows to keep objects (Markers, Points, Polygons, Lines and even 3D objects) in memory.
  • MarkerStyleBuilder has to be used to build style for Markers.
  • Summary: create Markers to given locations and style. Style is created via StyleBuilder. Then add Marker to LocalVectorDataSource, create Layer with same Datasource and add it to MapView. All done

Now try to create code with these elements. If you really must, peek code snippet below screenshot image.

tallinn marker

        // Create a new layer
        Projection projection = mapView.getOptions().getBaseProjection();
        LocalVectorDataSource datasource = new LocalVectorDataSource(projection);
        VectorLayer overlayLayer = new VectorLayer(datasource);

        // Add layer to map
        mapView.getLayers().add(overlayLayer);

        MarkerStyleBuilder styleBuilder = new MarkerStyleBuilder();
        styleBuilder.setSize(30);

        styleBuilder.setColor(new Color(android.graphics.Color.GREEN));

        // Set marker position and style
        MapPos position = projection.fromLatLong(59.426939, 24.646469);
        MarkerStyle style = styleBuilder.buildStyle();

        // Create marker and add it to the source
        Marker marker = new Marker(position, style);
        datasource.add(marker);

Now, how about we make the map interactive?

Listening to map clicks

CARTO Mobile SDK can also listen to map touches and movements. For this following classes are needed:

  • VectorElementEventListener - abstract class you need to implement as your class. It has method onVectorElementClicked() which is called if a map object is clicked.
  • Add the listener to your MapView
  • BalloonPopup is special general overlay element which looks like Balloon. You can add texts (2 lines: name and description), and other graphical elements into it. If you want you can create completely your custom balloons, but this has the typical elements for many use cases.
  • BalloonPopup is used same way like Markers - define style with StyleBuilder, and add it to DataSource which goes to a Layer which goes to MapView. Special about it is that you can use another VectorElement (e.g. Marker) as base location of this - so it will go to the top of this element.
  • MapEventListener - similar abstract class you need to implement as your class. It has methods like onMapMoved() and onMapClicked() and these are called for specific events.
    private class MyMapElementListener extends VectorElementEventListener {
        private final MapView mapView;
        private LocalVectorDataSource vectorDataSource;
        private BalloonPopupStyle balloonStyle;

        public MyMapElementListener(MapView mapView) {

            // prepare special datasource and layer, where to add balloon Popups
            vectorDataSource = new LocalVectorDataSource(mapView.getOptions().getBaseProjection());
            this.mapView = mapView;
            this.mapView.getLayers().add(new VectorLayer(vectorDataSource));

            // prepare style for Popups
            BalloonPopupStyleBuilder styleBuilder = new BalloonPopupStyleBuilder();

            // Make sure this label is shown on top all other labels
            styleBuilder.setPlacementPriority(10);

            balloonStyle = styleBuilder.buildStyle();

        }


        @Override
        public boolean onVectorElementClicked(VectorElementClickInfo clickInfo) {


            VectorElement clickedObject = clickInfo.getVectorElement();

            MapPos wgs84Position = mapView.getOptions().getBaseProjection().toWgs84(clickInfo.getClickPos());

            String title = "Clicked object";
            String description = String.format(Locale.US, "%.4f, %.4f", wgs84Position.getY(), wgs84Position.getX());

            BalloonPopup clickPopup = new BalloonPopup((Billboard)clickedObject, balloonStyle, title, description);

            vectorDataSource.add(clickPopup);

            // returning true means that click is "consumed", no more calls are done for other nearby elements
            return true;
        }
    }

And then activate the listener after adding your first marker:

overlayLayer.setVectorElementEventListener(new MyMapElementListener(mapView));

Now, if you click on your map, it should look something like this:

click event

CARTO Offline Map

So far we've covered what we can do online, but our base map also works offline. There are several ways to have offline base maps, two of them are:

  • Download map package for big region, like state or country
  • Download map for given bounding box area, for example like city or national park

Here we will go for second case, download a city area, as this is slightly less code.

  1. First we define area bounds to be downloaded. You can use e.g. http://bboxfinder.com/ to find coordinates for your area of interest. Make sure it is not too big, more than about 50x50 km is too slow and big, and may be even rejected by server.
// City of Boston area
String downloadArea = "bbox(-71.181107,42.288231,-70.999146,42.398362)";
  1. During app startup you need to define a storage folder for map packages in the device (and create if needed):
File folder = new File(getApplicationContext().getExternalFilesDir(null), "map_packages");

if (!folder.isDirectory()) {
   folder.mkdir();
}
  1. Then we initialize our package manager and start package download, if it’s not already downloaded. Note that there could happen IOException if storage is full or not available.
try {
   // Define manager
   CartoPackageManager manager = new CartoPackageManager("nutiteq.osm", folder.getAbsolutePath());
   
   // Start it
   manager.start();
   
   // if package is not already downloaded, then start this
   if (manager.getLocalPackage(downloadArea) == null) {
       manager.startPackageDownload(downloadArea);
   }
   
}
catch (IOException e) {
   System.out.println("Exception: " + e.getMessage());
}


Finally we need to create and add the actual layer and zoom in to Bonn. Copy the following snippet to your activity’s onCreate, see that we use one trick to "borrow" styling from the online layer to avoid defining own:

 // add new Layer with offline data source
 PackageManagerTileDataSource source = new PackageManagerTileDataSource(manager);

 // here we reuse style from online layer. We do not add that layer to map anymore, just need style decoder
 VectorTileLayer offlineBaseMapLayer = new VectorTileLayer(source, layer.getTileDecoder());
 mapView.getLayers().add(offlineBaseMapLayer);

 // zoom map to the region
 MapPos mapCenter = mapView.getOptions().getBaseProjection().fromWgs84(new MapPos(-71.181107,42.288231));
 mapView.setFocusPos(mapCenter, 0);
 mapView.setZoom(9, 0);

Load data from CARTO server

Finally, some GIS stuff: let's assume you have some GIS data and you want to show it on mobile. For this we provide following workflow:

  1. Upload your data to your carto.com account.
  2. Create visualizations and analysis there
  3. Create mobile app using CARTO mobile SDK, consuming data from server.

There are several ways to consume maps from CARTO account:

  • Raster tiles, using XYZ URL
  • Vector tiles, using similar URL + custom styling
  • Using Vector tiles and CartoCSS defined in CARTO Builder (web UI)
  • Vector tiles via Named Map, created programmatically with CARTO API
  • Using viz.json map configuration (temporarily disabled)
  • Using offline map package creator - convert CARTO data to vector tile mbtiles package
  • SQL API, loading raw dataset from server and set styling in code

Some of the methods require Enterprise CARTO Accoutn and api key, and other require a lot of code, so we use the last, SQL API option. It only requires that your table is defined as "public" there, so it is usable for free CARTO accounts.

Following is code with some comments. You see that we use following elements:

  • CartoSQLService - does SQL queries, returns FeatureCollection. This is generic data holder, similar to JSON Array
  • FeatureCollection is converted to VectorElementVector, and styles are applied in this step
  • The Vector is added to DataSource, we use same as for Markers above

Note that click on the city dot now crashes app. Can you fix this?

        // add some GeoJSON data from CARTO SQL API
        // It is a network/IO operation, we don't want to block our main thread

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {

                try {

                    final CartoSQLService service = new CartoSQLService();
                    service.setUsername("nutiteq");

                    FeatureCollection features = null;
                    String query = "SELECT * FROM cities15000";
                    PointStyleBuilder pointStyleBuilder = new PointStyleBuilder();

                    VectorElementVector elements = new VectorElementVector();

                    features = service.queryFeatures(query, projection);


                    pointStyleBuilder.setColor(new Color(android.graphics.Color.RED));
                    pointStyleBuilder.setSize(3);

                    PointStyle pStyle = pointStyleBuilder.buildStyle();

                    for (int i = 0; i < features.getFeatureCount(); i++) {

                        // I know there are only Point geometries
                        // your data may have also LineGeometry or PolygonGeometry

                        PointGeometry geometry = (PointGeometry) features.getFeature(i).getGeometry();
                        elements.add(new Point(geometry, pStyle));
                    }

                    datasource.addAll(elements);

                }catch (IOException e){
                    Log.e("app","Exception: " + e.getMessage());
                }
            }
        });

        thread.start();

city lights

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment