com.google.appinventor.components.runtime.util.NativeOpenStreetMapController.java Source code

Java tutorial

Introduction

Here is the source code for com.google.appinventor.components.runtime.util.NativeOpenStreetMapController.java

Source

// -*- mode: java; c-basic-offset: 2; -*-
// Copyright  2016-2017 Massachusetts Institute of Technology, All rights reserved.
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.components.runtime.util;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Picture;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.PictureDrawable;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.view.ViewCompat;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.widget.RelativeLayout;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import com.google.appinventor.components.common.ComponentConstants;
import com.google.appinventor.components.runtime.Form;
import com.google.appinventor.components.runtime.LocationSensor;
import com.google.appinventor.components.runtime.util.MapFactory.HasFill;
import com.google.appinventor.components.runtime.util.MapFactory.HasStroke;
import com.google.appinventor.components.runtime.util.MapFactory.MapCircle;
import com.google.appinventor.components.runtime.util.MapFactory.MapController;
import com.google.appinventor.components.runtime.util.MapFactory.MapEventListener;
import com.google.appinventor.components.runtime.util.MapFactory.MapFeature;
import com.google.appinventor.components.runtime.util.MapFactory.MapLineString;
import com.google.appinventor.components.runtime.util.MapFactory.MapMarker;
import com.google.appinventor.components.runtime.util.MapFactory.MapPolygon;
import com.google.appinventor.components.runtime.util.MapFactory.MapRectangle;
import com.google.appinventor.components.runtime.util.MapFactory.MapType;
import com.google.appinventor.components.runtime.view.ZoomControlView;
import org.osmdroid.api.IGeoPoint;
import org.osmdroid.config.Configuration;
import org.osmdroid.events.MapListener;
import org.osmdroid.events.ScrollEvent;
import org.osmdroid.events.ZoomEvent;
import org.osmdroid.tileprovider.MapTile;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.util.BoundingBox;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.MapView;
import org.osmdroid.views.MapView.OnTapListener;
import org.osmdroid.views.overlay.Marker;
import org.osmdroid.views.overlay.Marker.OnMarkerClickListener;
import org.osmdroid.views.overlay.Marker.OnMarkerDragListener;
import org.osmdroid.views.overlay.Overlay;
import org.osmdroid.views.overlay.OverlayWithIW;
import org.osmdroid.views.overlay.OverlayWithIWVisitor;
import org.osmdroid.views.overlay.Polygon;
import org.osmdroid.views.overlay.Polyline;
import org.osmdroid.views.overlay.compass.CompassOverlay;
import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider;
import org.osmdroid.views.overlay.gestures.RotationGestureOverlay;
import org.osmdroid.views.overlay.infowindow.OverlayInfoWindow;
import org.osmdroid.views.overlay.mylocation.IMyLocationConsumer;
import org.osmdroid.views.overlay.mylocation.IMyLocationProvider;
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

class NativeOpenStreetMapController implements MapController, MapListener {
    /* copied from SVG */
    private static final long SPECIFIED_FILL = 1;
    private static final long SPECIFIED_FILL_OPACITY = 1 << 2;
    private static final long SPECIFIED_STROKE = 1 << 3;
    private static final long SPECIFIED_STROKE_OPACITY = 1 << 4;
    private static final long SPECIFIED_STROKE_WIDTH = 1 << 5;
    /* end copied from SVG */

    private static final String TAG = NativeOpenStreetMapController.class.getSimpleName();
    private boolean caches;
    private final Form form;
    private RelativeLayout containerView;
    private MapView view;
    private MapType tileType;
    private boolean zoomEnabled;
    private boolean zoomControlEnabled;
    private CompassOverlay compass = null;
    private final MyLocationNewOverlay userLocation;
    private RotationGestureOverlay rotation = null;
    private Set<MapEventListener> eventListeners = new HashSet<MapEventListener>();
    private Map<MapFeature, OverlayWithIW> featureOverlays = new HashMap<MapFeature, OverlayWithIW>();
    private SVG defaultMarkerSVG = null;
    private TouchOverlay touch = null;
    private OverlayInfoWindow defaultInfoWindow = null;
    private boolean ready = false;
    private ZoomControlView zoomControls = null;
    private float lastAzimuth = Float.NaN;

    private static class AppInventorLocationSensorAdapter
            implements IMyLocationProvider, LocationSensor.LocationSensorListener {
        private LocationSensor source;
        private Location lastLocation;
        private IMyLocationConsumer consumer;
        private boolean enabled = false;

        @Override
        public void setSource(LocationSensor source) {
            if (this.source == source) {
                return; // nothing to do here
            }
            if (this.source != null) {
                this.source.Enabled(false);
            }
            this.source = source;
            if (this.source != null) {
                this.source.Enabled(enabled);
            }
        }

        @Override
        public void onTimeIntervalChanged(int time) {
        }

        @Override
        public void onDistanceIntervalChanged(int distance) {
        }

        @Override
        public void onLocationChanged(Location location) {
            lastLocation = location;
            if (consumer != null) {
                consumer.onLocationChanged(location, this);
            }
        }

        @Override
        public void onStatusChanged(String s, int i, Bundle bundle) {
        }

        @Override
        public void onProviderEnabled(String s) {
        }

        @Override
        public void onProviderDisabled(String s) {
        }

        @Override
        public boolean startLocationProvider(IMyLocationConsumer consumer) {
            this.consumer = consumer;
            if (source != null) {
                source.Enabled(true);
                enabled = true;
            }
            return enabled;
        }

        @Override
        public void stopLocationProvider() {
            if (source != null) {
                source.Enabled(false);
            }
            enabled = false;
        }

        @Override
        public Location getLastKnownLocation() {
            return lastLocation;
        }

        @Override
        public void destroy() {
            this.consumer = null;
        }
    }

    private class TouchOverlay extends Overlay {
        private boolean scrollEnabled = true;

        @Override
        public void draw(Canvas arg0, MapView arg1, boolean arg2) {
        }

        @Override
        public boolean onFling(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY,
                MapView mapView) {
            return !scrollEnabled;
        }

        @Override
        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY,
                MapView mapView) {
            return !scrollEnabled;
        }

        @Override
        public boolean onLongPress(final MotionEvent pEvent, final MapView pMapView) {
            IGeoPoint p = pMapView.getProjection().fromPixels((int) pEvent.getX(), (int) pEvent.getY());
            final double lat = p.getLatitude();
            final double lng = p.getLongitude();
            for (MapEventListener l : eventListeners) {
                l.onLongPress(lat, lng);
            }
            return false; // We don't want to cancel propagation to other overlays
        }
    }

    private class MapReadyHandler extends Handler {

        @Override
        public void handleMessage(final Message msg) {
            switch (msg.what) {
            case MapTile.MAPTILE_SUCCESS_ID:
                if (!ready && form.canDispatchEvent(null, "MapReady")) {
                    ready = true;
                    form.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            for (MapEventListener l : eventListeners) {
                                l.onReady(NativeOpenStreetMapController.this);
                            }
                        }
                    });
                }
                view.invalidate();
                break;
            }
        }
    }

    private class CustomMapView extends MapView {
        public CustomMapView(Context context) {
            super(context, null, new MapReadyHandler());
        }

        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            scrollTo(getScrollX() + (oldw - w) / 2, getScrollY() + (oldh - h) / 2);
            super.onSizeChanged(w, h, oldw, oldh);
        }

        @Override
        public void onDetach() {
            // Suppress call to parent onDetach
        }
    }

    private final AppInventorLocationSensorAdapter locationProvider;

    NativeOpenStreetMapController(final Form form) {
        File osmdroid = new File(form.getCacheDir(), "osmdroid");
        if (osmdroid.exists() || osmdroid.mkdirs()) {
            Configuration.getInstance().setOsmdroidBasePath(osmdroid);
            File osmdroidTiles = new File(osmdroid, "tiles");
            if (osmdroidTiles.exists() || osmdroidTiles.mkdirs()) {
                Configuration.getInstance().setOsmdroidTileCache(osmdroidTiles);
                caches = true;
            }
        }
        this.form = form;
        this.touch = new TouchOverlay();
        view = new CustomMapView(form.getApplicationContext());
        locationProvider = new AppInventorLocationSensorAdapter();
        defaultInfoWindow = new OverlayInfoWindow(view);
        view.setTilesScaledToDpi(true);
        view.setMapListener(this);
        view.getOverlayManager().add(touch);
        view.addOnTapListener(new OnTapListener() {
            @Override
            public void onSingleTap(MapView view, double latitude, double longitude) {
                for (MapEventListener listener : eventListeners) {
                    listener.onSingleTap(latitude, longitude);
                }
            }

            @Override
            public void onDoubleTap(MapView view, double latitude, double longitude) {
                for (MapEventListener listener : eventListeners) {
                    listener.onDoubleTap(latitude, longitude);
                }
            }
        });
        zoomControls = new ZoomControlView(view);
        userLocation = new MyLocationNewOverlay(locationProvider, view);

        containerView = new RelativeLayout(form);
        containerView.setClipChildren(true);
        containerView.addView(view, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT));
        containerView.addView(zoomControls);
        zoomControls.setVisibility(View.GONE); // not shown by default
    }

    @Override
    public View getView() {
        return containerView;
    }

    @Override
    public double getLatitude() {
        return view.getMapCenter().getLatitude();
    }

    @Override
    public double getLongitude() {
        return view.getMapCenter().getLongitude();
    }

    @Override
    public void setCenter(double latitude, double longitude) {
        view.getController().setCenter(new GeoPoint(latitude, longitude));
    }

    @Override
    public void setZoom(int zoom) {
        view.getController().setZoom((double) zoom);
        zoomControls.updateButtons();
    }

    @Override
    public int getZoom() {
        return (int) view.getZoomLevel(false);
    }

    @Override
    public void setZoomEnabled(boolean enable) {
        this.zoomEnabled = enable;
        view.setMultiTouchControls(enable);
    }

    @Override
    public boolean isZoomEnabled() {
        return zoomEnabled;
    }

    @Override
    public void setMapType(MapType type) {
        switch (type) {
        case ROADS:
            tileType = type;
            view.setTileSource(TileSourceFactory.MAPNIK);
            break;
        case AERIAL:
            tileType = type;
            view.setTileSource(TileSourceFactory.USGS_SAT);
            break;
        case TERRAIN:
            tileType = type;
            view.setTileSource(TileSourceFactory.USGS_TOPO);
            break;
        case UNKNOWN:
        default:
            break;
        }
    }

    @Override
    public MapType getMapType() {
        return tileType;
    }

    @Override
    public void setCompassEnabled(boolean enabled) {
        if (enabled && compass == null) {
            compass = new CompassOverlay(view.getContext(), view);
            view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                @Override
                public boolean onPreDraw() {
                    float density = view.getContext().getResources().getDisplayMetrics().density;
                    compass.setCompassCenter(view.getMeasuredWidth() / density - 35, 35);
                    return true;
                }
            });
            view.getOverlayManager().add(compass);
        }
        if (compass != null) {
            if (enabled) {
                if (compass.getOrientationProvider() != null) {
                    compass.enableCompass();
                } else {
                    compass.enableCompass(new InternalCompassOrientationProvider(view.getContext()));
                }
                compass.onOrientationChanged(lastAzimuth, null);
            } else {
                lastAzimuth = compass.getOrientation();
                compass.disableCompass();
            }
        }
    }

    @Override
    public boolean isCompassEnabled() {
        return compass != null && compass.isCompassEnabled();
    }

    @Override
    public void setZoomControlEnabled(boolean enabled) {
        if (zoomControlEnabled != enabled) {
            zoomControls.setVisibility(enabled ? View.VISIBLE : View.GONE);
            zoomControlEnabled = enabled;
            containerView.invalidate();
        }
    }

    @Override
    public boolean isZoomControlEnabled() {
        return zoomControlEnabled;
    }

    @Override
    public void setShowUserEnabled(boolean enable) {
        userLocation.setEnabled(enable);
        if (enable) {
            userLocation.enableMyLocation();
            view.getOverlayManager().add(userLocation);
        } else {
            userLocation.disableMyLocation();
            view.getOverlayManager().remove(userLocation);
        }
    }

    @Override
    public boolean isShowUserEnabled() {
        return userLocation != null && userLocation.isEnabled();
    }

    @Override
    public void setRotationEnabled(boolean enabled) {
        if (enabled && rotation == null) {
            rotation = new RotationGestureOverlay(view);
        }
        if (rotation != null) {
            rotation.setEnabled(enabled);
            if (enabled) {
                view.getOverlayManager().add(rotation);
            } else {
                view.getOverlayManager().remove(rotation);
            }
        }
    }

    @Override
    public boolean isRotationEnabled() {
        return rotation != null && rotation.isEnabled();
    }

    @Override
    public void setPanEnabled(boolean enable) {
        touch.scrollEnabled = enable;
    }

    @Override
    public boolean isPanEnabled() {
        return touch.scrollEnabled;
    }

    @Override
    public void panTo(double latitude, double longitude, int zoom, double seconds) {
        view.getController().animateTo(new GeoPoint(latitude, longitude));
        if (view.getController().zoomTo((double) zoom)) {
            Animation animation = view.getAnimation();
            if (animation != null) {
                animation.setDuration((long) (1000 * seconds));
            }
        }
    }

    @Override
    public void addEventListener(MapEventListener listener) {
        eventListeners.add(listener);
        if ((ready || ViewCompat.isAttachedToWindow(view)) && form.canDispatchEvent(null, "MapReady")) {
            ready = true;
            listener.onReady(this);
        }
    }

    @Override
    public void addFeature(final MapMarker aiMarker) {
        createNativeMarker(aiMarker, new AsyncCallbackPair<Marker>() {
            @Override
            public void onFailure(String message) {
                Log.e(TAG, "Unable to create marker: " + message);
            }

            @Override
            public void onSuccess(Marker overlay) {
                overlay.setOnMarkerClickListener(new OnMarkerClickListener() {
                    @Override
                    public boolean onMarkerClick(Marker marker, MapView mapView) {
                        for (MapEventListener listener : eventListeners) {
                            listener.onFeatureClick(aiMarker);
                        }
                        if (aiMarker.EnableInfobox()) {
                            marker.showInfoWindow();
                        }
                        return false;
                    }

                    @Override
                    public boolean onMarkerLongPress(Marker marker, MapView mapView) {
                        for (MapEventListener listener : eventListeners) {
                            listener.onFeatureLongPress(aiMarker);
                        }
                        return false;
                    }
                });
                overlay.setOnMarkerDragListener(new OnMarkerDragListener() {
                    @Override
                    public void onMarkerDrag(Marker marker) {
                        for (MapEventListener listener : eventListeners) {
                            listener.onFeatureDrag(aiMarker);
                        }
                    }

                    @Override
                    public void onMarkerDragEnd(Marker marker) {
                        IGeoPoint point = marker.getPosition();
                        aiMarker.updateLocation(point.getLatitude(), point.getLongitude());
                        for (MapEventListener listener : eventListeners) {
                            listener.onFeatureStopDrag(aiMarker);
                        }
                    }

                    @Override
                    public void onMarkerDragStart(Marker marker) {
                        for (MapEventListener listener : eventListeners) {
                            listener.onFeatureStartDrag(aiMarker);
                        }
                    }
                });
                if (aiMarker.Visible()) {
                    showOverlay(overlay);
                } else {
                    hideOverlay(overlay);
                }
            }

        });
    }

    @Override
    public void addFeature(final MapLineString aiPolyline) {
        Polyline polyline = createNativePolyline(aiPolyline);
        featureOverlays.put(aiPolyline, polyline);
        polyline.setOnClickListener(new Polyline.OnClickListener() {
            @Override
            public boolean onClick(Polyline arg0, MapView arg1, GeoPoint arg2) {
                for (MapEventListener listener : eventListeners) {
                    listener.onFeatureClick(aiPolyline);
                }
                if (aiPolyline.EnableInfobox()) {
                    arg0.showInfoWindow(arg2);
                }
                return true;
            }

            @Override
            public boolean onLongClick(Polyline arg0, MapView arg1, GeoPoint arg2) {
                for (MapEventListener listener : eventListeners) {
                    listener.onFeatureLongPress(aiPolyline);
                }
                return true;
            }
        });
        polyline.setOnDragListener(new Polyline.OnDragListener() {
            @Override
            public void onDragStart(Polyline polyline) {
                for (MapEventListener listener : eventListeners) {
                    listener.onFeatureStartDrag(aiPolyline);
                }
            }

            @Override
            public void onDrag(Polyline polyline) {
                for (MapEventListener listener : eventListeners) {
                    listener.onFeatureDrag(aiPolyline);
                }
            }

            @Override
            public void onDragEnd(Polyline polyline) {
                aiPolyline.updatePoints(polyline.getPoints());
                for (MapEventListener listener : eventListeners) {
                    listener.onFeatureStopDrag(aiPolyline);
                }
            }
        });
        if (aiPolyline.Visible()) {
            showOverlay(polyline);
        } else {
            hideOverlay(polyline);
        }
    }

    private void configurePolygon(final MapFeature component, Polygon polygon) {
        featureOverlays.put(component, polygon);
        polygon.setOnClickListener(new Polygon.OnClickListener() {
            @Override
            public boolean onLongClick(Polygon arg0, MapView arg1, GeoPoint arg2) {
                for (MapEventListener listener : eventListeners) {
                    listener.onFeatureLongPress(component);
                }
                return true;
            }

            @Override
            public boolean onClick(Polygon arg0, MapView arg1, GeoPoint arg2) {
                for (MapEventListener listener : eventListeners) {
                    listener.onFeatureClick(component);
                }
                if (component.EnableInfobox()) {
                    arg0.showInfoWindow(arg2);
                }
                return true;
            }
        });
        polygon.setOnDragListener(new Polygon.OnDragListener() {
            @Override
            public void onDragStart(Polygon polygon) {
                for (MapEventListener listener : eventListeners) {
                    listener.onFeatureStartDrag(component);
                }
            }

            @Override
            public void onDrag(Polygon polygon) {
                for (MapEventListener listener : eventListeners) {
                    listener.onFeatureDrag(component);
                }
            }

            @Override
            public void onDragEnd(Polygon polygon) {
                if (component instanceof MapCircle) {
                    double latitude = 0, longitude = 0;
                    int count = polygon.getPoints().size();
                    // Note that this approximates the centroid of the Circle.
                    for (GeoPoint p : polygon.getPoints()) {
                        latitude += p.getLatitude();
                        longitude += p.getLongitude();
                    }
                    if (count > 0) {
                        ((MapCircle) component).updateCenter(latitude / count, longitude / count);
                    } else {
                        ((MapCircle) component).updateCenter(0, 0);
                    }
                } else if (component instanceof MapRectangle) {
                    double north = -90, east = -180, west = 180, south = 90;
                    for (GeoPoint p : polygon.getPoints()) {
                        double lat = p.getLatitude();
                        double lng = p.getLongitude();
                        north = Math.max(north, lat);
                        south = Math.min(south, lat);
                        east = Math.max(east, lng);
                        west = Math.min(west, lng);
                    }
                    ((MapRectangle) component).updateBounds(north, west, south, east);
                } else {
                    ((MapPolygon) component).updatePoints(Collections.singletonList(polygon.getPoints()));
                    List<List<GeoPoint>> holes = new ArrayList<List<GeoPoint>>();
                    holes.addAll(polygon.getHoles());
                    ((MapPolygon) component).updateHolePoints(Collections.singletonList(holes));
                }
                for (MapEventListener listener : eventListeners) {
                    listener.onFeatureStopDrag(component);
                }
            }
        });
        if (component.Visible()) {
            showOverlay(polygon);
        } else {
            hideOverlay(polygon);
        }
    }

    @Override
    public void addFeature(final MapPolygon aiPolygon) {
        configurePolygon(aiPolygon, createNativePolygon(aiPolygon));
    }

    @Override
    public void addFeature(MapCircle aiCircle) {
        configurePolygon(aiCircle, createNativeCircle(aiCircle));
    }

    @Override
    public void addFeature(MapRectangle aiRectangle) {
        configurePolygon(aiRectangle, createNativeRectangle(aiRectangle));
    }

    @Override
    public void removeFeature(MapFeature aiFeature) {
        view.getOverlayManager().remove(featureOverlays.get(aiFeature));
        featureOverlays.remove(aiFeature);
    }

    @Override
    public void updateFeaturePosition(MapMarker aiMarker) {
        Marker marker = (Marker) featureOverlays.get(aiMarker);
        if (marker != null) {
            marker.setPosition(new GeoPoint(aiMarker.Latitude(), aiMarker.Longitude()));
            view.invalidate();
        }
    }

    @Override
    public void updateFeaturePosition(MapLineString aiPolyline) {
        Polyline overlay = (Polyline) featureOverlays.get(aiPolyline);
        if (overlay != null) {
            overlay.setPoints(aiPolyline.getPoints());
            view.invalidate();
        }
    }

    @Override
    public void updateFeaturePosition(MapPolygon aiPolygon) {
        MultiPolygon polygon = (MultiPolygon) featureOverlays.get(aiPolygon);
        if (polygon != null) {
            polygon.setMultiPoints(aiPolygon.getPoints());
            polygon.setMultiHoles(aiPolygon.getHolePoints());
            view.invalidate();
        }
    }

    @Override
    public void updateFeaturePosition(MapCircle aiCircle) {
        GeoPoint center = new GeoPoint(aiCircle.Latitude(), aiCircle.Longitude());
        Polygon polygon = (Polygon) featureOverlays.get(aiCircle);
        if (polygon != null) {
            List<GeoPoint> geopoints = Polygon.pointsAsCircle(center, aiCircle.Radius());
            polygon.setPoints(geopoints);
            view.invalidate();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void updateFeaturePosition(MapRectangle aiRectangle) {
        Polygon polygon = (Polygon) featureOverlays.get(aiRectangle);
        if (polygon != null) {
            List<GeoPoint> geopoints = (List) Polygon.pointsAsRect(new BoundingBox(aiRectangle.NorthLatitude(),
                    aiRectangle.EastLongitude(), aiRectangle.SouthLatitude(), aiRectangle.WestLongitude()));
            polygon.setPoints(geopoints);
            view.invalidate();
        }
    }

    @Override
    public void updateFeatureFill(final HasFill aiFeature) {
        OverlayWithIW overlay = featureOverlays.get(aiFeature);
        if (overlay == null) {
            return; // not yet initialized
        }
        overlay.accept(new OverlayWithIWVisitor() {
            @Override
            public void visit(final Marker marker) {
                getMarkerDrawable((MapMarker) aiFeature, new AsyncCallbackPair<Drawable>() {
                    @Override
                    public void onFailure(String message) {
                        Log.e(TAG, "Unable to update fill color for marker: " + message);
                    }

                    @Override
                    public void onSuccess(Drawable result) {
                        marker.setIcon(result);
                        view.invalidate();
                    }
                });
            }

            @Override
            public void visit(Polyline polyline) {
                // polylines do not have fills
            }

            @Override
            public void visit(Polygon polygon) {
                polygon.setFillColor(aiFeature.FillColor());
                view.invalidate();
            }

        });
    }

    @Override
    public void updateFeatureStroke(final HasStroke aiFeature) {
        OverlayWithIW overlay = featureOverlays.get(aiFeature);
        if (overlay == null) {
            return; // not yet initialized
        }
        overlay.accept(new OverlayWithIWVisitor() {
            @Override
            public void visit(final Marker marker) {
                getMarkerDrawable((MapMarker) aiFeature, new AsyncCallbackPair<Drawable>() {
                    @Override
                    public void onFailure(String message) {
                        Log.e(TAG, "Unable to update stroke color for marker: " + message);
                    }

                    @Override
                    public void onSuccess(Drawable result) {
                        marker.setIcon(result);
                        view.invalidate();
                    }
                });
            }

            @Override
            public void visit(Polyline polyline) {
                DisplayMetrics metrics = new DisplayMetrics();
                form.getWindowManager().getDefaultDisplay().getMetrics(metrics);
                polyline.setColor(aiFeature.StrokeColor());
                polyline.setWidth(aiFeature.StrokeWidth() * metrics.density);
                view.invalidate();
            }

            @Override
            public void visit(Polygon polygon) {
                DisplayMetrics metrics = new DisplayMetrics();
                form.getWindowManager().getDefaultDisplay().getMetrics(metrics);
                polygon.setStrokeColor(aiFeature.StrokeColor());
                polygon.setStrokeWidth(aiFeature.StrokeWidth() * metrics.density);
                view.invalidate();
            }
        });
    }

    @Override
    public void updateFeatureText(MapFeature aiFeature) {
        OverlayWithIW overlay = featureOverlays.get(aiFeature);
        if (overlay != null) {
            overlay.setTitle(aiFeature.Title());
            overlay.setSnippet(aiFeature.Description());
        }
    }

    @Override
    public void updateFeatureDraggable(MapFeature aiFeature) {
        OverlayWithIW overlay = featureOverlays.get(aiFeature);
        if (overlay != null) {
            overlay.setDraggable(aiFeature.Draggable());
        }
    }

    @Override
    public void updateFeatureImage(MapMarker aiMarker) {
        final Marker marker = (Marker) featureOverlays.get(aiMarker);
        if (marker == null) {
            return; // not yet initialized
        }
        getMarkerDrawable(aiMarker, new AsyncCallbackPair<Drawable>() {
            @Override
            public void onFailure(String message) {
                Log.e(TAG, "Unable to update feature image: " + message);
            }

            @Override
            public void onSuccess(Drawable result) {
                marker.setIcon(result);
                view.invalidate();
            }
        });
    }

    @Override
    public void updateFeatureSize(MapMarker aiMarker) {
        final Marker marker = (Marker) featureOverlays.get(aiMarker);
        if (marker == null) {
            return;
        }
        getMarkerDrawable(aiMarker, new AsyncCallbackPair<Drawable>() {
            @Override
            public void onFailure(String message) {
                Log.wtf(TAG, "Cannot find default marker");
            }

            @Override
            public void onSuccess(Drawable result) {
                marker.setIcon(result);
                view.invalidate();
            }
        });
    }

    private void getMarkerDrawable(final MapMarker aiMarker, final AsyncCallbackPair<Drawable> callback) {
        final String assetPath = aiMarker.ImageAsset();
        if (assetPath == null || assetPath.length() == 0 || assetPath.endsWith(".svg")) {
            getMarkerDrawableVector(aiMarker, callback);
        } else {
            getMarkerDrawableRaster(aiMarker, callback);
        }
    }

    private void getMarkerDrawableVector(MapMarker aiMarker, AsyncCallbackPair<Drawable> callback) {
        SVG markerSvg = null;
        if (defaultMarkerSVG == null) {
            try {
                defaultMarkerSVG = SVG.getFromAsset(view.getContext().getAssets(), "marker.svg");
            } catch (SVGParseException e) {
                Log.e(TAG, "Invalid SVG in Marker asset", e);
            } catch (IOException e) {
                Log.e(TAG, "Unable to read Marker asset", e);
            }
            if (defaultMarkerSVG == null || defaultMarkerSVG.getRootElement() == null) {
                throw new IllegalStateException("Unable to load SVG from assets");
            }
        }
        final String markerAsset = aiMarker.ImageAsset();
        if (markerAsset != null && markerAsset.length() != 0) {
            try {
                markerSvg = SVG.getFromAsset(view.getContext().getAssets(), markerAsset);
            } catch (SVGParseException e) {
                Log.e(TAG, "Invalid SVG in Marker asset", e);
            } catch (IOException e) {
                Log.e(TAG, "Unable to read Marker asset", e);
            }
            if (markerSvg == null) {
                // Attempt to retrieve asset from ReplForm storage location
                InputStream is = null;
                try {
                    is = MediaUtil.openMedia(form, markerAsset);
                    markerSvg = SVG.getFromInputStream(is);
                } catch (SVGParseException e) {
                    Log.e(TAG, "Invalid SVG in Marker asset", e);
                } catch (IOException e) {
                    Log.e(TAG, "Unable to read Marker asset", e);
                } finally {
                    IOUtils.closeQuietly(TAG, is);
                }
            }
        }
        if (markerSvg == null) {
            markerSvg = defaultMarkerSVG;
        }
        try {
            callback.onSuccess(rasterizeSVG(aiMarker, markerSvg));
        } catch (Exception e) {
            callback.onFailure(e.getMessage());
        }
    }

    private void getMarkerDrawableRaster(final MapMarker aiMarker, final AsyncCallbackPair<Drawable> callback) {
        MediaUtil.getBitmapDrawableAsync(form, aiMarker.ImageAsset(), new AsyncCallbackPair<BitmapDrawable>() {
            @Override
            public void onFailure(String message) {
                callback.onSuccess(getDefaultMarkerDrawable(aiMarker));
            }

            @Override
            public void onSuccess(BitmapDrawable result) {
                callback.onSuccess(result);
            }
        });
    }

    private Drawable getDefaultMarkerDrawable(MapMarker aiMarker) {
        return rasterizeSVG(aiMarker, defaultMarkerSVG);
    }

    private static float getBestGuessWidth(SVG.Svg svg) {
        if (svg.width != null) {
            return svg.width.floatValue();
        } else if (svg.viewBox != null) {
            return svg.viewBox.width;
        } else {
            return ComponentConstants.MARKER_PREFERRED_WIDTH;
        }
    }

    private static float getBestGuessHeight(SVG.Svg svg) {
        if (svg.height != null) {
            return svg.height.floatValue();
        } else if (svg.viewBox != null) {
            return svg.viewBox.height;
        } else {
            return ComponentConstants.MARKER_PREFERRED_HEIGHT;
        }
    }

    private Drawable rasterizeSVG(MapMarker aiMarker, SVG markerSvg) {
        SVG.Svg svg = markerSvg.getRootElement();
        final float density = view.getContext().getResources().getDisplayMetrics().density;
        float height = aiMarker.Height() <= 0 ? getBestGuessHeight(svg) : aiMarker.Height();
        float width = aiMarker.Width() <= 0 ? getBestGuessWidth(svg) : aiMarker.Width();
        float scaleH = height / getBestGuessHeight(svg);
        float scaleW = width / getBestGuessWidth(svg);
        float scale = (float) Math.sqrt(scaleH * scaleH + scaleW * scaleW);

        // update fill color of SVG <path>
        Paint fillPaint = new Paint();
        Paint strokePaint = new Paint();
        PaintUtil.changePaint(fillPaint, aiMarker.FillColor());
        PaintUtil.changePaint(strokePaint, aiMarker.StrokeColor());
        SVG.Length strokeWidth = new SVG.Length(aiMarker.StrokeWidth() / scale);
        for (SVG.SvgObject element : svg.getChildren()) {
            if (element instanceof SVG.SvgConditionalElement) {
                SVG.SvgConditionalElement path = (SVG.SvgConditionalElement) element;
                path.baseStyle.fill = new SVG.Colour(fillPaint.getColor());
                path.baseStyle.fillOpacity = fillPaint.getAlpha() / 255.0f;
                path.baseStyle.stroke = new SVG.Colour(strokePaint.getColor());
                path.baseStyle.strokeOpacity = strokePaint.getAlpha() / 255.0f;
                path.baseStyle.strokeWidth = strokeWidth;
                if (path.style != null) {
                    if ((path.style.specifiedFlags & SPECIFIED_FILL) == 0) {
                        path.style.fill = new SVG.Colour(fillPaint.getColor());
                        path.style.specifiedFlags |= SPECIFIED_FILL;
                    }
                    if ((path.style.specifiedFlags & SPECIFIED_FILL_OPACITY) == 0) {
                        path.style.fillOpacity = fillPaint.getAlpha() / 255.0f;
                        path.style.specifiedFlags |= SPECIFIED_FILL_OPACITY;
                    }
                    if ((path.style.specifiedFlags & SPECIFIED_STROKE) == 0) {
                        path.style.stroke = new SVG.Colour(strokePaint.getColor());
                        path.style.specifiedFlags |= SPECIFIED_STROKE;
                    }
                    if ((path.style.specifiedFlags & SPECIFIED_STROKE_OPACITY) == 0) {
                        path.style.strokeOpacity = strokePaint.getAlpha() / 255.0f;
                        path.style.specifiedFlags |= SPECIFIED_STROKE_OPACITY;
                    }
                    if ((path.style.specifiedFlags & SPECIFIED_STROKE_WIDTH) == 0) {
                        path.style.strokeWidth = strokeWidth;
                        path.style.specifiedFlags |= SPECIFIED_STROKE_WIDTH;
                    }
                }
            }
        }

        // draw SVG to Picture and create a BitmapDrawable for rendering
        Picture picture = markerSvg.renderToPicture();
        Picture scaledPicture = new Picture();
        Canvas canvas = scaledPicture.beginRecording((int) ((width + 2.0f * aiMarker.StrokeWidth()) * density),
                (int) ((height + 2.0f * aiMarker.StrokeWidth()) * density));
        canvas.scale(density * scaleW, density * scaleH);
        canvas.translate(strokeWidth.floatValue(), strokeWidth.floatValue());
        picture.draw(canvas);
        scaledPicture.endRecording();
        return new PictureDrawable(scaledPicture);
    }

    private void createNativeMarker(final MapMarker aiMarker, AsyncCallbackPair<Marker> callback) {
        final Marker osmMarker = new Marker(view);
        featureOverlays.put(aiMarker, osmMarker);
        osmMarker.setDraggable(aiMarker.Draggable());
        osmMarker.setTitle(aiMarker.Title());
        osmMarker.setSnippet(aiMarker.Description());
        osmMarker.setPosition(new GeoPoint(aiMarker.Latitude(), aiMarker.Longitude()));
        osmMarker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM);
        getMarkerDrawable(aiMarker, new AsyncCallbackFacade<Drawable, Marker>(callback) {
            @Override
            public void onFailure(String message) {
                callback.onFailure(message);
            }

            @Override
            public void onSuccess(Drawable result) {
                osmMarker.setIcon(result);
                callback.onSuccess(osmMarker);
            }
        });
    }

    private Polyline createNativePolyline(final MapLineString aiLineString) {
        final Polyline osmLine = new Polyline();
        osmLine.setDraggable(aiLineString.Draggable());
        osmLine.setTitle(aiLineString.Title());
        osmLine.setSnippet(aiLineString.Description());
        osmLine.setPoints(aiLineString.getPoints());
        osmLine.setColor(aiLineString.StrokeColor());
        osmLine.setWidth(aiLineString.StrokeWidth());
        osmLine.setInfoWindow(defaultInfoWindow);
        return osmLine;
    }

    private void createPolygon(final Polygon osmPolygon, final MapFeature aiFeature) {
        osmPolygon.setDraggable(aiFeature.Draggable());
        osmPolygon.setTitle(aiFeature.Title());
        osmPolygon.setSnippet(aiFeature.Description());
        osmPolygon.setStrokeColor(((HasStroke) aiFeature).StrokeColor());
        osmPolygon.setStrokeWidth(((HasStroke) aiFeature).StrokeWidth());
        osmPolygon.setFillColor(((HasFill) aiFeature).FillColor());
        osmPolygon.setInfoWindow(defaultInfoWindow);
    }

    private MultiPolygon createNativePolygon(final MapPolygon aiPolygon) {
        final MultiPolygon osmPolygon = new MultiPolygon();
        createPolygon(osmPolygon, aiPolygon);
        osmPolygon.setMultiPoints(aiPolygon.getPoints());
        osmPolygon.setMultiHoles(aiPolygon.getHolePoints());
        return osmPolygon;
    }

    private Polygon createNativeCircle(final MapCircle aiCircle) {
        final Polygon osmPolygon = new Polygon();
        createPolygon(osmPolygon, aiCircle);
        osmPolygon.setPoints(
                Polygon.pointsAsCircle(new GeoPoint(aiCircle.Latitude(), aiCircle.Longitude()), aiCircle.Radius()));
        return osmPolygon;
    }

    private Polygon createNativeRectangle(final MapRectangle aiRectangle) {
        BoundingBox bbox = new BoundingBox(aiRectangle.NorthLatitude(), aiRectangle.EastLongitude(),
                aiRectangle.SouthLatitude(), aiRectangle.WestLongitude());
        final Polygon osmPolygon = new Polygon();
        createPolygon(osmPolygon, aiRectangle);
        osmPolygon.setPoints(new ArrayList<GeoPoint>((List) Polygon.pointsAsRect(bbox)));
        return osmPolygon;
    }

    @Override
    public void showFeature(MapFeature feature) {
        showOverlay(featureOverlays.get(feature));
    }

    protected void showOverlay(OverlayWithIW overlay) {
        view.getOverlayManager().add(overlay);
    }

    @Override
    public void hideFeature(MapFeature feature) {
        hideOverlay(featureOverlays.get(feature));
    }

    protected void hideOverlay(OverlayWithIW overlay) {
        view.getOverlayManager().remove(overlay);
    }

    @Override
    public boolean isFeatureVisible(MapFeature feature) {
        OverlayWithIW overlay = featureOverlays.get(feature);
        return overlay != null && view.getOverlayManager().contains(overlay);
    }

    @Override
    public void showInfobox(MapFeature feature) {
        OverlayWithIW overlay = featureOverlays.get(feature);
        overlay.showInfoWindow();
    }

    @Override
    public void hideInfobox(MapFeature feature) {
        OverlayWithIW overlay = featureOverlays.get(feature);
        overlay.closeInfoWindow();
    }

    @Override
    public boolean isInfoboxVisible(MapFeature feature) {
        OverlayWithIW overlay = featureOverlays.get(feature);
        return overlay != null && overlay.isInfoWindowOpen();
    }

    @Override
    public BoundingBox getBoundingBox() {
        return view.getBoundingBox();
    }

    @Override
    public void setBoundingBox(BoundingBox bbox) {
        view.getController().setCenter(bbox.getCenter());
        view.getController().zoomToSpan(bbox.getLatitudeSpan(), bbox.getLongitudeSpan());
    }

    @Override
    public boolean onScroll(ScrollEvent event) {
        for (MapEventListener listener : eventListeners) {
            listener.onBoundsChanged();
        }
        return true;
    }

    @Override
    public boolean onZoom(ZoomEvent event) {
        zoomControls.updateButtons();
        for (MapEventListener listener : eventListeners) {
            listener.onZoom();
        }
        return true;
    }

    @Override
    public LocationSensor.LocationSensorListener getLocationListener() {
        return locationProvider;
    }

    @Override
    public int getOverlayCount() {
        System.err.println(view.getOverlays());
        return view.getOverlays().size();
    }

    @Override
    public void setRotation(float Rotation) {
        view.setMapOrientation(Rotation);
    }

    @Override
    public float getRotation() {
        return view.getMapOrientation();
    }

    static class MultiPolygon extends Polygon {

        private List<Polygon> children = new ArrayList<Polygon>();
        private boolean draggable;
        private OnClickListener clickListener;
        private OnDragListener dragListener;

        @Override
        public void showInfoWindow() {
            if (children.size() > 0) {
                children.get(0).showInfoWindow();
            }
        }

        @Override
        public void draw(Canvas canvas, MapView mapView, boolean b) {
            for (Polygon child : children) {
                child.draw(canvas, mapView, b);
            }
        }

        public void setMultiPoints(List<List<GeoPoint>> points) {
            Iterator<Polygon> polygonIterator = children.iterator();
            Iterator<List<GeoPoint>> pointIterator = points.iterator();
            while (polygonIterator.hasNext() && pointIterator.hasNext()) {
                polygonIterator.next().setPoints(pointIterator.next());
            }
            while (polygonIterator.hasNext()) {
                polygonIterator.next();
                polygonIterator.remove();
            }
            while (pointIterator.hasNext()) {
                Polygon p = new Polygon();
                p.setPoints(pointIterator.next());
                p.setStrokeColor(getStrokeColor());
                p.setFillColor(getFillColor());
                p.setStrokeWidth(getStrokeWidth());
                p.setInfoWindow(getInfoWindow());
                p.setDraggable(draggable);
                p.setOnClickListener(clickListener);
                p.setOnDragListener(dragListener);
                children.add(p);
            }
        }

        public void setMultiHoles(List<List<List<GeoPoint>>> holes) {
            if (holes == null || holes.isEmpty()) {
                for (Polygon child : children) {
                    child.setHoles(Collections.<List<GeoPoint>>emptyList());
                }
            } else if (holes.size() != children.size()) {
                throw new IllegalArgumentException("Holes and points are not of the same arity.");
            } else {
                Iterator<Polygon> polygonIterator = children.iterator();
                Iterator<List<List<GeoPoint>>> holeIterator = holes.iterator();
                while (polygonIterator.hasNext() && holeIterator.hasNext()) {
                    polygonIterator.next().setHoles(holeIterator.next());
                }
            }
        }

        @Override
        public void setDraggable(boolean draggable) {
            super.setDraggable(draggable);
            this.draggable = draggable;
            for (Polygon child : children) {
                child.setDraggable(draggable);
            }
        }

        @Override
        public void setOnClickListener(OnClickListener listener) {
            super.setOnClickListener(listener);
            clickListener = listener;
            for (Polygon child : children) {
                child.setOnClickListener(listener);
            }
        }

        @Override
        public void setOnDragListener(OnDragListener listener) {
            super.setOnDragListener(listener);
            dragListener = listener;
            for (Polygon child : children) {
                child.setOnDragListener(listener);
            }
        }

        @Override
        public void setStrokeWidth(float strokeWidth) {
            super.setStrokeWidth(strokeWidth);
            for (Polygon child : children) {
                child.setStrokeWidth(strokeWidth);
            }
        }

        @Override
        public void setStrokeColor(int strokeColor) {
            super.setStrokeColor(strokeColor);
            for (Polygon child : children) {
                child.setStrokeColor(strokeColor);
            }
        }

        @Override
        public void setFillColor(int fillColor) {
            super.setFillColor(fillColor);
            for (Polygon child : children) {
                child.setFillColor(fillColor);
            }
        }

        @Override
        public void setTitle(String title) {
            super.setTitle(title);
            for (Polygon child : children) {
                child.setTitle(title);
            }
        }

        @Override
        public void setSnippet(String snippet) {
            super.setSnippet(snippet);
            for (Polygon child : children) {
                child.setSnippet(snippet);
            }
        }

        @Override
        public boolean onSingleTapConfirmed(MotionEvent event, MapView mapView) {
            for (Polygon child : children) {
                if (child.onSingleTapConfirmed(event, mapView)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public boolean contains(MotionEvent event) {
            for (Polygon child : children) {
                if (child.contains(event)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public boolean onLongPress(MotionEvent event, MapView mapView) {
            boolean touched = contains(event);
            if (touched) {
                if (mDraggable) {
                    mIsDragged = true;
                    closeInfoWindow();
                    mDragStartPoint = event;
                    if (mOnDragListener != null) {
                        mOnDragListener.onDragStart(this);
                    }
                    moveToEventPosition(event, mDragStartPoint, mapView);
                } else if (mOnClickListener != null) {
                    mOnClickListener.onLongClick(this, mapView,
                            (GeoPoint) mapView.getProjection().fromPixels((int) event.getX(), (int) event.getY()));
                }
            }
            return touched;
        }

        @Override
        public void moveToEventPosition(final MotionEvent event, final MotionEvent start, final MapView view) {
            for (Polygon child : children) {
                child.moveToEventPosition(event, start, view);
            }
        }

        @Override
        public void finishMove(final MotionEvent start, final MotionEvent end, final MapView view) {
            for (Polygon child : children) {
                child.finishMove(start, end, view);
            }
        }

        @Override
        public boolean onTouchEvent(MotionEvent event, MapView mapView) {
            if (mDraggable && mIsDragged) {
                if (event.getAction() == MotionEvent.ACTION_UP) {
                    mIsDragged = false;
                    finishMove(mDragStartPoint, event, mapView);
                    if (mOnDragListener != null) {
                        mOnDragListener.onDragEnd(this);
                    }
                    return true;
                } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
                    moveToEventPosition(event, mDragStartPoint, mapView);
                    if (mOnDragListener != null) {
                        mOnDragListener.onDrag(this);
                    }
                    return true;
                }
            }
            return false;
        }
    }
}