Skip to content

Geo Widget

Sebastian edited this page Feb 23, 2023 · 6 revisions

Geowidget is and android widget that allows visualisation , inspection and manipulation of geospatial data View geospatial features Edit geospatial features View details on geospatial features Offline support

This widget implements kujaku mapview (which implements Mapbox). Kujaku is a japanese word meaning peacock

Geowidget can offer more features like drawing polygons and estimate size of a map section.

It

  1. Allows customisation
  2. Use tiles from different tile servers eg. Digital Globe for Reveal
  3. Supports GeoJSON
  4. Extendable

How to set it up

1. Add the dependency

Add the artifact repository url to your project root build.gradle file

allprojects {
  repositories {
    \\ Other repositories here
    ...
    mavenCentral()
  }
}

Add the library as a dependency

 implementation('io.ona.kujaku:library:0.9.0') {
        exclude group: 'com.android.volley'
        exclude group: 'stax', module: 'stax-api'
    }

2. Get an access token

This may be required in case you are accessing styles and other functionality from mapbox.com

Enable your token like this in the host application:

public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    // Mapbox Access token
    Mapbox.getInstance(getApplicationContext(), getString(R.string.mapbox_access_token));
  }
}

3. Add a map

Open the Java file of the activity where you'd like to include the map in and add the code below to the file.

private KujakuMapView kujakuMapView;

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   kujakuMapView = (MapView) findViewById(R.id.kujakuMapView);
   kujakuMapView.onCreate(savedInstanceState);
   kujakuMapView.getMapAsync(new OnMapReadyCallback() {
      @Override
      public void onMapReady(MapboxMap mapboxMap) {
         // Customize map with markers, polylines, etc.
      }
   });
}

Open the activity's XML layout file and add the mapView within your layout.

<io.ona.kujaku.views.KujakuMapView
  android:id="@+id/kujakuMapView"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  mapbox:mapbox_styleUrl="@string/mapbox_style_mapbox_streets" />

Sample Json configs

Below is a sample JSON Config to use geowidget (Some tags and blocks are subject to change based on need basis)

{
  "title": "Register Structure",
  "display_back_button": true,
  "no_padding": true,
  "fields": [
    {
      "key": "structure",
      "type": "geowidget",
      "v_zoom_max": {
        "value": "16.5",
        "err": "Please zoom in to add a point"
      }
    },
    {
      "key": "selectedOpAreaLabel",
      "type": "label",
      "text": "Selected Operational Area",
      "read_only": false,
      "hint_on_text": false,
      "text_color": "#000000",
      "openmrs_entity_parent": "",
      "openmrs_entity": "",
      "openmrs_entity_id": ""
    },
    {
      "key": "selectedOpAreaName",
      "type": "label",
      "text": "",
      "read_only": false,
      "hint_on_text": true,
      "text_color": "#000000",
      "openmrs_entity_parent": "",
      "openmrs_entity": "",
      "openmrs_entity_id": ""
    },
    {
      "key": "structureType",
      "openmrs_entity_parent": "",
      "openmrs_entity": "",
      "openmrs_entity_id": "",
      "type": "native_radio",
      "label": "Type of Structure",
      "options": [
        {
          "key": "Residential Structure",
          "text": "Residential Structure"
        },
        {
          "key": "Mosquito Collection Point",
          "text": "Mosquito Collection Point"
        },
        {
          "key": "Larval Breeding Site",
          "text": "Larval Breeding Site"
        },
        {
          "key": "Potential Area of Transmission",
          "text": "Potential Area of Transmission"
        }
      ],
      "value": "Residential Structure",
      "v_required": {
        "value": true,
        "err": "Please specify Type of structure"
      }
    },
    {
      "key": "physicalType",
      "openmrs_entity_parent": "",
      "openmrs_entity": "",
      "openmrs_entity_id": "",
      "type": "native_radio",
      "label": "Location Physical Type",
      "options": [
        {
          "key": "Home",
          "text": "Home"
        },
        {
          "key": "Hut",
          "text": "Hut"
        }
      ],
      "value": "Home",
      "relevance": {
        "step1:structureType": {
          "type": "string",
          "ex": "equalTo(., \"Residential Structure\")"
        }
      }
    },
    {
      "key": "structureName",
      "openmrs_entity_parent": "",
      "openmrs_entity": "",
      "openmrs_entity_id": "",
      "type": "edit_text",
      "hint": "Name of structure",
      "edit_type": "name"
    },
    {
      "key": "zoom_level",
      "type": "hidden",
      "openmrs_entity_parent": "",
      "openmrs_entity": "",
      "openmrs_entity_id": ""
    },
    {
      "key": "valid_operational_area",
      "type": "hidden",
      "openmrs_entity_parent": "",
      "openmrs_entity": "",
      "openmrs_entity_id": ""
    }
  ]
}

5. Java 8

If you're using an Android Studio version that is 3.1.0 or above, you can ignore this section because the new dex compiler D8 will be enabled by default. The Mapbox Maps SDK for Android introduces the use of Java 8. To fix any Java versioning issues, ensure that you are using Gradle version of 3.0 or greater. Once you’ve done that, add the following compileOptions to the android section of your app-level build.gradle file like so:

android {
  ...
  compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

Technology behind Kujaku(Dependencies):

Kujaku is built on-top-of Mapbox: We are currently using com.mapbox.mapboxsdk:mapbox-android-sdk:9.7.1

Other dependencies include:

androidx.legacy:legacy-support-v4:1.0.0
io.ona.kujaku:utils:0.9.0
com.android.volley:volley:1.2.0
com.snatik:storage:2.1.0
joda-time:joda-time:2.9.9
com.android.support:cardview-v7:26.+
com.android.support:recyclerview-v7:26.+
com.google.android.gms:play-services-maps:8.4.0
com.google.android.gms:play-services-location:8.4.0
io.realm:realm-android-library:4.1.1
io.realm:realm-annotations:4.1.1
com.android.support:appcompat-v7:26.1.0
com.cocoahero.android:geojson:1.0.1

Permissions:

The library uses the following permissions:

  • android.permission.ACCESS_FINE_LOCATION To get the user’s location when the “My location button” is clicked

  • android.permission.ACCESS_NETWORK_STATE To automatically switch between Google Location services and device GPS depending on internet connection availability when retrieving the user’s location

  • android.permission.READ_EXTERNAL_STORAGE To read styles stored in local storage when using the MapActivity

  • android.permission.WRITE_EXTERNAL_STORAGE To cache styles in local storage when using the MapActivity

  • android.permission.INTERNET To fetch tiles and styles from Mapbox To use enable use of Google Location services when retrieving the user’s location

How to feed it with data

To provide data to KujakuMapView you initially set a Mapbox style, the KujakuMapView comes with its default style referenced by it’s style url. This style can contain all the styling and data required. For more go to https://www.mapbox.com/studio/.

The style contains the basemap that you want to use, it can also contain parameters such as zoom level, map center and constraints. These and more can be found on the Mapbox Style Specification documentation at https://www.mapbox.com/mapbox-gl-js/style-spec/

1. Split your data and styling into different layers and sources (Most straightforward)

image showing map layers

Each layer in this approach has a specific styling. You then attach data-sources to it. The data points in your source will use the layer’s styling eg circles, polygons, icons etc.

We end up with something like this:

Map of Africa

2. Use single layer type for all your data with a single data source

In this case, you style data across ranges or style data with conditions using a single layer and a single data source (a single FeatureCollection made up of GeoJSON feature array). The only limit being you can only use this a single layer type’s visual characteristics - You cannot mix a fill layer, line layer or an icon layer

For a single feature

com.mapbox.geojson.Feature feature =
       com.mapbox.geojson.Feature.fromGeometry(
               com.mapbox.geojson.Point.fromLngLat(
                       // Lat, Long
                       1.39239, 7.923993
               )
       );


GeoJSONSource pointsSource = new GeoJsonSource(pointsSourceId);
pointsSource.setGeoJson(feature);

For multiple features

GeoJson FeatureCollection string
JSONArray featuresArray = new JSONArray();
//featuresArray.put(jsonFeature)

JSONObject featureCollection = new JSONObject();
featureCollection.put("type", "FeatureCollection");
featureCollection.put("features", featuresArray);
featureCollection.toString();

GeoJSONSource pointsSource = new GeoJsonSource(“reveal-tasks-source”);
pointsSource.setGeoJson(featureCollection);

There are many other ways to create the GeoJSONSource...

Finally, add the source to the MapboxMap

mapboxMap.addSource(pointsSource);

What a mapbox style with layer styling looks like?

        {
            "id": "red and blue kids - ",
            "type": "symbol",
            "source": "composite",
            "source-layer": "zeir-livingstone-data-2-kigamba",
            "filter": ["==", "$type", "Point"],
            "layout": {
                "icon-image": [
                    "step",
                    ["get", "is_red"],
                    "red-baby",
                    1,
                    "blue-baby"
                ],
                "icon-allow-overlap": true,
                "visibility": "none"
            },
            "paint": {}
        },
        {
            "id": "red and blue kids - data conditions",
            "type": "symbol",
            "source": "composite",
            "source-layer": "zeir-livingstone-data-2-kigamba",
            "filter": ["==", "$type", "Point"],
            "layout": {
                "icon-allow-overlap": true,
                "icon-image": [
                    "match",
                    ["get", "is_red"],
                    0,
                    "blue-baby",
                    [1],
                    "red-baby", 
                    "" 
                ]
            },
            "paint": {}
        }

This uses the following expressions https://www.mapbox.com/mapbox-gl-js/style-spec/#expressions-match and https://www.mapbox.com/mapbox-gl-js/style-spec/#expressions-step

How to register listeners

The most important listeners:

onMapClick
onCameraChange
onScroll

How to get the feature that was clicked on the map depends on the onMapClickListener

mapboxMap.addOnMapClickListener(new MapboxMap.OnMapClickListener() {

   @Override
   public void onMapClick(@NonNull LatLng point) {
      // Convert LatLng coordinates to screen pixel and only query the rendered features.
      final PointF pixel = mapboxMap.getProjection().toScreenLocation(point);
      List<Feature> features = mapboxMap.queryRenderedFeatures(pixel);

   }
}

How to add a point on KujakuMapView

  1. Use the low-level API which gives you control on what to show and define the user experience/flow to achieve the add point operation

The low-level API means that it enables you to control the small operations that end up forming a complete add point API. The KujakuMapView

In the add point API, you have access to the following functions

/**
* Enables/disables the marker layout so that the user can scroll to their preferred location
*
* @param canAddPoint
*/
void enableAddPoint(boolean canAddPoint);

/**
* Call this to enable/disable adding a new point using GPS. In this layout the user cannot
* scroll since the marker position is guided by the GPS updates. Disabling it disables the marker
* layout and GPS querying.
*
* @param canAddPoint
* @param onLocationChanged
*/
void enableAddPoint(boolean canAddPoint, @Nullable OnLocationChanged onLocationChanged);

/**
* This should be called after calling {@link #enableAddPoint(boolean)} with {@code true} thus
* disabling the marker layout, adding a point at the current marker position & returns a geoJSON
* feature of the current marker position.
*
* @return
*/
@Nullable JSONObject dropPoint();

/**
* This should be called after calling {@link #enableAddPoint(boolean, OnLocationChanged)} with {@code true} thus
* disabling the marker layout, disabling GPS location updates, adding a point at @param latLng & returns a geoJSON
* feature at @param latLng
*
* @param latLng
* @return
*/
@Nullable JSONObject dropPoint(@Nullable LatLng latLng);

The following activity in the app showcases this io.ona.kujaku.sample.activities.LowLevelManualAddPointMapView.java

In low-level, you therefore have to define your own UI for enabling the user to show the marker layout and enable the add point mode. You also add the UI for disabling the mode

kujakuMapView = findViewById(R.id.kmv_lowLevelManualAddPointMapView_mapView);
kujakuMapView.showCurrentLocationBtn(true);
kujakuMapView.enableAddPoint(true);

Button button = findViewById(R.id.btn_lowLevelManualAddPointMapView_doneBtn);
button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       if (kujakuMapView.isCanAddPoint()) {
           JSONObject featurePoint = kujakuMapView.dropPoint();
           Log.e("FEATURE POINT", featurePoint.toString());
       }
   }
});

button.setOnLongClickListener(new View.OnLongClickListener() {
   @Override
   public boolean onLongClick(View v) {
       kujakuMapView.enableAddPoint(!kujakuMapView.isCanAddPoint());

       return true;
   }
});
  1. Use the high-level API which gives you less control off what is shown and the flow on the UI. You basically call one method and provide a callback. Once the add operation is either completed or cancelled, the callback is made with the point GeoJSON or the cancel function

The following activity in the app showcases this io.ona.kujaku.sample.activities.HighLevelMapView.java

In high-level API, all UI is provided and you cannot change it

kujakuMapView = findViewById(R.id.kmv_highLevelMapView_mapView);
kujakuMapView.addPoint(false, new AddPointCallback() {
   @Override
   public void onPointAdd(JSONObject jsonObject) {
       Log.d(TAG, jsonObject.toString());
   }

   @Override
   public void onCancel() {
       Log.d(TAG, "User cancelled adding points");
   }
});
Clone this wiki locally