Skip to content

Custom vector layer with markers

nutiteq edited this page Aug 14, 2012 · 2 revisions

This shows how to create new Vector layer with specific object types. The content is on-line OpenStreetmap POIs, via simple custom API.

Here we have two Classes: new Layer with Marker objects (extending MarkerLayer), and also we need new Task to load data. Layer and Task are optimized for tile-based data loading, which enables caching. Very basic caching in memory in client is implemented here, but it should be clear how to improve it (clean up cache for example, or make it persistent).

OsmPoiOnlineTileLayer.java

package com.nutiteq.app.layers;

import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;

import com.nutiteq.components.MapPos;
import com.nutiteq.geometry.Marker;
import com.nutiteq.log.Log;
import com.nutiteq.projections.Projection;
import com.nutiteq.style.MarkerStyle;
import com.nutiteq.style.StyleSet;
import com.nutiteq.vectorlayers.MarkerLayer;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;

/**
 * Loads OSM data from online custom source (basic CSV API)
 * This is sample for tile-based online vector layer implementation
 * Perhaps not the most optimal or nicest solution, but helps you get started
 * 
 * @author jaak
 *
 */
public class OsmPoiOnlineTileLayer extends MarkerLayer {

    private StyleSet<MarkerStyle> styleSet;
    private HashMap<MapPos,List<Marker>> markerTilesVisible = new HashMap<MapPos,List<Marker>>();
    private HashMap<MapPos, List<Marker>> markerTilesCache = new HashMap<MapPos,List<Marker>>();
    private HashMap<MapPos, List<Marker>> newMarkerTilesVisible =  new HashMap<MapPos,List<Marker>>();
    
    public OsmPoiOnlineTileLayer(Projection projection, StyleSet<MarkerStyle> style) {
        super(projection);
        this.styleSet = style;
    }

    
    /* 
     * This is called for every map frame draw. Must be very quick and non-blocking
     */
    @Override
    public List<Marker> getVisibleElements() {

        List<Marker> visibleElements = new LinkedList<Marker>();
        
        try {
            for (int i=0 ; i<markerTilesVisible.size() ; i++){
                List<Marker>markerTile = (List<Marker>) markerTilesVisible.values().toArray()[i];
                visibleElements.addAll(markerTile);
            }
        // Here is a bit ugly exception catching, but with proper locking and iterations lags can come
        } catch (NoSuchElementException e) {
        } catch (ConcurrentModificationException e) {
            Log.debug("NoSuchElementException");
        }

//       Log.debug("visible tiles: "+markerTilesVisible.size()+" elements: "+visibleElements.size());
       
        return visibleElements;
      
    }

    public void add(Geometry element) {
        throw new UnsupportedOperationException();
    }

    public void remove(Geometry element) {
        throw new UnsupportedOperationException();
    }

    /* 
     * Called after map view change: update data based on new view bounds (Envelope) and zoom level.
     * 
     * This runs on UI thread, so no blocking (IO) here. Instead start Tasks (which are running on separate thread).
     * In simpler case Task ends up with returning data by calling setVisibleElementsList() method from VectorLayer (which is a parent) 
     * Here we do not use VectorLayer's visible elements list, and use local markerTilesVisible field instead. This gives 
     * possibility to cache vector data by tiles. Here is caching only in memory, but you can extend it for persistent caching.
     * 
     */
    public void calculateVisibleElements(Envelope envelope, int zoom) {
        Log.debug("calculateVisibleElements start");
        
        // calculate currently visible tiles
        modifyLock.lock();
        newMarkerTilesVisible.clear();
                
        MapPos bottomLeft = projection.fromInternal((float) envelope.getMinX(),
                (float) envelope.getMinY());
        MapPos topRight = projection.fromInternal((float) envelope.getMaxX(),
                (float) envelope.getMaxY());

        MapPos minTile = TileUtils.MetersToTile(bottomLeft, zoom);
        MapPos maxTile = TileUtils.MetersToTile(topRight, zoom);

        Log.debug("visible tiles: X " + minTile.x + "-" + maxTile.x + " Y "
                + maxTile.y + "-" + minTile.y + " N="
                + ((maxTile.x - minTile.x+1) * (minTile.y - maxTile.y+1)));
      
        List<DownloadOsmPoiOnlineTileTask> taskList = new LinkedList<DownloadOsmPoiOnlineTileTask>();
        for (int x = (int) minTile.x; x <= maxTile.x; x++) {
            for (int y = (int) minTile.y; y >= maxTile.y; y--) {
                // is it new - add then a task
                MapPos tile = new MapPos(x, y, zoom);

                if(!markerTilesCache.containsKey(tile)) {
                    Log.debug("new tile, start task " + x + " " + y + " "
                            + zoom);
                    newMarkerTilesVisible.put(tile, new LinkedList<Marker>());
                    taskList.add(new DownloadOsmPoiOnlineTileTask(
                                    projection, this, modifyLock, x, y, zoom,
                                    styleSet));

                }else{
                    newMarkerTilesVisible.put(tile, markerTilesCache.get(tile));
                    Log.debug("tile loaded from cache "+ x + " " + y + " " + zoom);
                }
            }
        }
        modifyLock.unlock();
        
        components.vectorTaskPool.cancelAll();
        // now execute the tasks. Also add "last" flag to the last one
        int i=1;
        int last = taskList.size();
        for(DownloadOsmPoiOnlineTileTask task:taskList){
            if(i==last){
                task.setLast(true);
            }
            components.vectorTaskPool.execute(task);
            i++;
        }
 
        
        Log.debug("calculateVisibleElements end");
    }

    
    /**
     * Callback for Task to update loaded data
     * @param mapPos
     * @param newVisibleElementsList
     * @param last
     */
    public void setMarkerTileElementsList(MapPos mapPos,
            List<Marker> newVisibleElementsList, boolean last) {

        newMarkerTilesVisible.put(mapPos, newVisibleElementsList);

        // if last tile of this view, then update markers from
        // the "offscreen buffer"
        if(last){
            modifyLock.lock();
            markerTilesVisible.clear();
            markerTilesVisible.putAll(newMarkerTilesVisible);
            modifyLock.unlock();
        }
        
        // put loaded data the cache also
        markerTilesCache.putAll(markerTilesVisible);

       // TODO have cache cleanup (LRU cache) here
    }

}

DownloadOsmPoiOnlineTileTask.java

package com.nutiteq.app.layers;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

import com.nutiteq.components.MapPos;
import com.nutiteq.geometry.Marker;
import com.nutiteq.log.Log;
import com.nutiteq.projections.Projection;
import com.nutiteq.style.MarkerStyle;
import com.nutiteq.style.StyleSet;
import com.nutiteq.tasks.Task;
import com.nutiteq.ui.DefaultLabel;

/**
 * Task to download a tile of vector data, using simple custom CSV API
 * Note that all POIs have same symbology here, in real app this should 
 * probably be changed
 *
 */
public class DownloadOsmPoiOnlineTileTask implements Task {

    private final OsmPoiOnlineTileLayer markerLayer;

    private final ReentrantLock modifyLock;
    private final int x;
    private final int y;
    private final int zoom;

    private final StyleSet<MarkerStyle> style;

    private final Projection projection;

    private boolean last;

    public void setLast(boolean last) {
        this.last = last;
    }

    public DownloadOsmPoiOnlineTileTask(Projection projection,
            OsmPoiOnlineTileLayer markerLayer, ReentrantLock modifyLock, int x,
            int y, int zoom, StyleSet<MarkerStyle> style) {
        this.markerLayer = markerLayer;
        this.modifyLock = modifyLock;
        this.x = x;
        this.y = y;
        this.zoom = zoom;
        this.style = style;
        this.projection = projection;
    }

    
    @Override
    public void run() {
        
        List<Marker> newVisibleElementsList = downloadPoi(x, y, zoom);
        Log.debug("adding Markers: " + newVisibleElementsList.size());

        modifyLock.lock();
        markerLayer.setMarkerTileElementsList(new MapPos(x,y,zoom),newVisibleElementsList, last);
        modifyLock.unlock();
    }

    private List<Marker> downloadPoi(int x, int y, int zoom) {

        List<Marker> pois = new ArrayList<Marker>();

        HttpURLConnection con = null;
        String path = "http://kaart.maakaart.ee/poiexport/poi.php?layers=tourism:museum,amenity:fuel,amenity:atm&output=csv&zoom="
                + zoom + "&x=" + x + "&y=" + y + "&max=100";
        URL url;
        InputStream is = null;
        try {
            url = new URL(path);
            con = (HttpURLConnection) url.openConnection();
            con.setReadTimeout(10000); // ms
            con.setConnectTimeout(15000); // ms
            con.setRequestMethod("GET");
            con.setDoInput(true);
            // Start the query
            con.connect();
            Log.debug("connected " + path);
            is = con.getInputStream();
            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(is));
            // read line by line
            String line;
            while ((line = reader.readLine()) != null) {
                String[] rowData = line.split("\t");
                float lon = Float.parseFloat(rowData[0]);
                float lat = Float.parseFloat(rowData[1]);
                String name = "";
                if (rowData.length >= 3) {
                    name = rowData[2];
                }
                String address = "";
                if (rowData.length >= 4) {
                    address = rowData[3];
                }
                Marker poi = new Marker(projection.fromWgs84(lon, lat),
                        new DefaultLabel(name, address), this.style, new MapPos(x,y,zoom));
                poi.calculateInternalPos(projection);
                poi.setActiveStyle(zoom);
                pois.add(poi);
            }
        } catch (IOException ex) {
            Log.error("IOException" + ex.getMessage());
        } finally {
            try {
                if(is != null)
                  is.close();
            } catch (IOException e) {
                Log.error("IOException" + e.getMessage());
            }
        }
        return pois;
    }

    @Override
    public boolean isCancelable() {
        return true;
    }

}

TileUtils.java

package com.nutiteq.app.layers;

import com.nutiteq.components.MapPos;
import com.vividsolutions.jts.geom.Envelope;

public class TileUtils {
    
    private static final double TILESIZE = 256;
    private static final double initialResolution = 2.0f * Math.PI * 6378137.0f
            / TILESIZE;
    private static final double originShift = 2.0f * Math.PI * 6378137.0f / 2.0f;

    // following is from
    // http://code.google.com/p/gmap-tile-generator/source/browse/trunk/gmaps-tile-creator/src/gov/ca/maps/tile/geom/GlobalMercator.java?r=15
    /**
     * Returns tile for given Mercator coordinates
     * 
     * @return
     */
    public static MapPos MetersToTile(MapPos mp, int zoom) {
        int[] p = MetersToPixels(mp.x, mp.y, zoom);
        return PixelsToTile(p[0], p[1], zoom);
    }

    /**
     * Returns a tile covering region in given pixel coordinates
     * 
     * @param px
     * @param py
     * @param zoom 
     * @return
     */
    public static MapPos PixelsToTile(int px, int py, int zoom) {
        int tx = (int) Math.ceil(px / ((double) TILESIZE) - 1);
        int ty = (int) Math.ceil(py / ((double) TILESIZE) - 1);
        return new MapPos(tx, (1<<(zoom))-1-ty);
    }

    /**
     * Converts EPSG:900913 to pyramid pixel coordinates in given zoom level
     * 
     * @param mx
     * @param my
     * @param zoom
     * @return
     */
    public static int[] MetersToPixels(double mx, double my, int zoom) {
        double res = Resolution(zoom);
        int px = (int) Math.round((mx + originShift) / res);
        int py = (int) Math.round((my + originShift) / res);
        return new int[] { px, py };
    }

    /**
     * Resolution (meters/pixel) for given zoom level (measured at Equator)
     * 
     * @return
     */
    public static double Resolution(int zoom) {
        // return (2 * Math.PI * 6378137) / (this.tileSize * 2**zoom)
        return initialResolution / Math.pow(2, zoom);
    }
    
    /**
     * Returns bounds of the given tile in EPSG:900913 coordinates
     * 
     * @param tx
     * @param ty
     * @param zoom
     * @return
     */
    public static Envelope TileBounds(int tx, int ty, int zoom) {
            double[] min = PixelsToMeters(tx * TILESIZE, ty * TILESIZE, zoom);
            double minx = min[0], miny = min[1];
            double[] max = PixelsToMeters((tx + 1) * TILESIZE, (ty + 1) * TILESIZE,
                            zoom);
            double maxx = max[0], maxy = max[1];
            
            return new Envelope( minx, maxx, miny, maxy);
    }

    
    /**
     * Converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84
     * Datum
     * 
     * @return
     */
    public static double[] MetersToLatLon(double mx, double my) {

            double lon = (mx / originShift) * 180.0;
            double lat = (my / originShift) * 180.0;

            lat = 180
                            / Math.PI
                            * (2 * Math.atan(Math.exp(lat * Math.PI / 180.0)) - Math.PI / 2.0);
            return new double[] { lat, lon };
    }
    
    /**
     * Converts pixel coordinates in given zoom level of pyramid to EPSG:900913
     * 
     * @return
     */
    public static double[] PixelsToMeters(double px, double py, int zoom) {
            double res = Resolution(zoom);
            double mx = px * res - originShift;
            double my = py * res - originShift;
            return new double[] { mx, my };
    }

    
}
Clone this wiki locally