The whole world in your pocket or how to make a mobile card in a couple of days

The whole world in your pocket or how to make a mobile card in a couple of days




In the last article I talked about how you can quickly make a Web-dialer. But what if you put a more ambitious task - to build your own application with a map, without ads and with blackjack? And if in just a couple of days?


Let's do it! I ask under the cat.


First, let's look at what we have to do. At the exit, we want to get an application with reference data and a map. And to work offline. As a developer, I am primarily interested in the map, because we can already display reference data. And offline is a pretty strong limitation in this case, because there are not so many good libraries with offline support. Therefore, in the article we will concentrate on the map, and let's talk about the directory in passing.


Choose a map engine


The first thing to do is get the data for the application. The market has many sources, free and not very. For a start, we can use the OpenStreetMap as an open source of cartographic data. You can also take some amount of POI for our directory.


The next step is to choose a card. There are quite a few of them on the Internet, even fewer free ones, and with offline support there are only a few. I suggest using a pretty cool option - mapsforge/vtm . This is a vector OpenGL engine, very smart, supports offline, Android, iOS, various data sources, custom styling, overlays, markers, 3D and even 3D models of objects! Very, very cool.


There are a lot of examples in the repository for a quick start, there are ready-made maps, there is a plugin that allows you to collect your own map from data in OSM format. So, let's get started!


  MapView mapView = findViewById (R.id.map_view);
 this.map = mapView.map ();

 File baseMapFile = getMapFile ("cyprus.map");
 MapFileTileSource tileSource = new MapFileTileSource ();
 tileSource.setMapFile (baseMapFile.getAbsolutePath ());

 VectorTileLayer layer = this.map.setBaseMap (tileSource);

 MapInfo info = tileSource.getMapInfo ();
 if (info! = null) {
  MapPosition pos = new MapPosition ();
  pos.setByBoundingBox (info.boundingBox, Tile.SIZE * 4, Tile.SIZE * 4);
  this.map.setMapPosition (pos);
 }

 this.map.setTheme (VtmThemes.DEFAULT);

 this.map.layers (). add (new BuildingLayer (this.map, layer));
 this.map.layers (). add (new LabelLayer (this.map, layer));  

Create a MapFileTileSource data source, specify the location of the map file. Additionally, we are positioning in the center of the bounding box of interest to us, so as not to be somewhere outside the selected location at the start of the application. Set the default theme. Add a layer of houses and a layer of signatures. That's all. We start - miracles!



It seems faster and easier and can not be.


We do geocoding


The next important step is the implementation of geocoding. The map itself is not bad, but interactivity is needed. We want to tap into the map and see information on the object in which we fell. And there is some difficulty. By and large, there is no full geocoding in our library. This is perhaps the biggest disadvantage of it. If we don’t invent anything, we can use the existing functionality.


 //Determine click coordinates and find tiles in its zone
 float touchRadius = TOUCH_RADIUS * CanvasAdapter.getScale ();
 long mapSize = MercatorProjection.getMapSize ((byte) mMap.getMapPosition ().getZoomLevel ());
 double pixelX = MercatorProjection.longitudeToPixelX (p.getLongitude (), mapSize);
 double pixelY = MercatorProjection.latitudeToPixelY (p.getLatitude (), mapSize);
 int tileXMin = MercatorProjection.pixelXToTileX (pixelX - touchRadius, (byte) mMap.getMapPosition (). getZoomLevel ());
 int tileXMax = MercatorProjection.pixelXToTileX (pixelX + touchRadius, (byte) mMap.getMapPosition (). getZoomLevel ());
 int tileYMin = MercatorProjection.pixelYToTileY (pixelY - touchRadius, (byte) mMap.getMapPosition (). getZoomLevel ());
 int tileYMax = MercatorProjection.pixelYToTileY (pixelY + touchRadius, (byte) mMap.getMapPosition (). getZoomLevel ());
 Tile upperLeft = new Tile (tileXMin, tileYMin, (byte) mMap.getMapPosition (). GetZoomLevel ());
 Tile lowerRight = new Tile (tileXMax, tileYMax, (byte) mMap.getMapPosition (). GetZoomLevel ());
//Get data from the database by specifying the upper left and right lower tiles
 MapDatabase mapDatabase = ((MapDatabase) ((OverzoomTileDataSource) tileSource.getDataSource ()). GetDataSource ());
 MapReadResult mapReadResult = mapDatabase.readLabels (upperLeft, lowerRight);

 StringBuilder sb = new StringBuilder ();
//Filter received POIs based on the click area.
 sb.append ("*** POI ***");
 for (PointOfInterest pointOfInterest: mapReadResult.pointOfInterests) {
  Point layerXY = new Point ();
  mMap.viewport (). toScreenPoint (pointOfInterest.position, false, layerXY);
  Point tapXY = new Point (e.getX (), e.getY ());
  if (layerXY.distance (tapXY) & gt; touchRadius) {
  continue;
  }
  sb.append ("\ n");
  List & lt; Tag & gt;  tags = pointOfInterest.tags;
  for (Tag tag: tags) {
  sb.append ("\ n"). append (tag.key) .append ("="). append (tag.value);
  }
 }
//Filter the geometry that falls into the click area
 sb.append ("\ n \ n"). append ("*** WAYS ***");
 for (Way way: mapReadResult.ways) {
  if (way.geometryType! = GeometryBuffer.GeometryType.POLY
  ||  ! GeoPointUtils.contains (way.geoPoints [0], p)) {
  continue;
  }
  sb.append ("\ n");
  List & lt; Tag & gt;  tags = way.tags;
  for (Tag tag: tags) {
  sb.append ("\ n"). append (tag.key) .append ("="). append (tag.value);
  }
 }  

It turned out relatively verbose. You need to find a tile, get ways (in the terminology of OSM, the way is a linear object), and you can extract some attributes from them. In addition to the ways there is an opportunity to get more POI, but that's all. The rest of the logic will have to be wound up by yourself: choose the “right” one from the entire set of objects to which the click falls, filter by zoom levels. And one moment. In fact, we lose the information about the original geometry and in response to the search we get just a set of lines. If you want to do more and geo-editor, then this is clearly not enough.


But to demonstrate the approach, everything suits us.





"Advanced" geocoding


Generally speaking, there is a more advanced version. For this we need our own database. In particular, you can use SQLite. True, standard SQLite will not be enough for us, and we will have to build our own by connecting the RTree plugin for geo-searching to it. How to do this, I already told in the article , in the section “Making a good search.”
In this case, we get full control over the data, we can save everything that is required, and in the right format. We can also screw Full Text Search and search for our geo-objects and companies by name, address and other attributes.


Direction is:


  1. Making tables:
    • geo objects (id, type, geometry, attributes)
    • companies (id, attributes, geo_id) with reference to the geometry of the building in which it is located
    • geo-index on rtree like this:
        CREATE VIRTUAL TABLE geo_index USING rtree (
       id, - Integer primary key
       minX, maxX, - Minimum and maximum X coordinate
       minY, maxY - Minimum and maximum Y coordinate
       );  
  2. Fill everything with data.
  3. When we tap into the map, we get a GeoPoint and execute the query:
      SELECT id FROM geo_index
     WHERE minX & gt; = - 81.08 AND maxX & lt; = - 80.58
     AND minY & gt; = 35.00 AND maxY & lt; = 35.44  
  4. Last step: filter and select the appropriate object.

One of the implementation options can be found at repositories .


As a result, we can already show a map and handle clicks. Not bad.


Add important trivia


Let's add a couple of important functions.


Let's start with the current geolocation. In mapsforge/vtm for this, there is just a special. LocationLayer layer. The use is extremely simple.


  LocationLayer locationLayer = new LocationLayer (this.map);
 locationLayer.setEnabled (true);
//Position in the center of the map for simplicity, in general, it must be obtained from the GPS
 GeoPoint initialGeoPoint = this.map.getMapPosition (). GetGeoPoint ();
 locationLayer.setPosition (initialGeoPoint.getLatitude (), initialGeoPoint.getLongitude (), 1);
 this.map.layers (). add (locationLayer);  

There is only one drawback - the constant pulsation of the "blue point" on the edge of the screen when the current location is outside the map. Most likely, in the process of use you will rarely find yourself in such a situation, but this causes constant re-rendering, respectively, and a little load on the processor. Getting rid of it is a little more difficult, you need to climb into the shader and fix it. But this is quite for perfectionists. How to do - you can see here .


So, the position is. It's time to add a move button to the current position, as in all self-respecting map applications.


  View vLocation = findViewById (R.id.am_location);
 vLocation.setOnClickListener (v - & gt;
  this.map.animator (). animateTo (initialGeoPoint));  

We’ll also need zoom buttons.


  View vZoomIn = findViewById (R.id.am_zoom_in);
 vZoomIn.setOnClickListener (v - & gt;
  this.map.animator (). animateZoom (500, 2, 0, 0));

 View vZoomOut = findViewById (R.id.am_zoom_out);
 vZoomOut.setOnClickListener (v - & gt;
  this.map.animator (). animateZoom (500, 0.5, 0, 0));
  

And the cherry on the cake is a compass.


  View vCompass = findViewById (R.id.am_compass);
 vCompass.setVisibility (View.GONE);
 vCompass.setOnClickListener (v - & gt; {

  MapPosition mapPosition = this.map.getMapPosition ();
  mapPosition.setBearing (0);
  this.map.animator (). animateTo (500, mapPosition);

  vCompass.animate (). setListener (new Animator.AnimatorListener () {
  @Override
  public void onAnimationStart (Animator animation) {
  }

  @Override
  public void onAnimationEnd (Animator animation) {
  vCompass.setVisibility (View.GONE);
  }

  @Override
  public void onAnimationCancel (Animator animation) {
  }

  @Override
  public void onAnimationRepeat (Animator animation) {
  }
  }). setDuration (500) .rotation (0) .start ();
 });

 this.map.events.bind ((e, mapPosition) - & gt; {
  if (e == Map.ROTATE_EVENT) {
  vCompass.setRotation (mapPosition.getBearing ());
  vCompass.setVisibility (View.VISIBLE);
  }
 });  




Capture the world


Friends, we are at the finish line. It remains to add the final touch.After all, we are planning to seize the world, which means that we need to somehow cram it into our application.


And it’s so that with our engine it’s a lot easier than it seems.
We need to slightly modify the method for loading the map by adding a MultyMapTileSource to it. This is essentially a wrapper for any other tile sources, which allows you to immediately display everything on the map that has been added to it. Just a killer feature. As a result, it remains for us to prepare a world map with minimal detail, add it to the very first one in our wrapper, and on top draw everything else. Moreover, we can immediately add all the cards that we have in the catalog with maps of the application! Gorgeous, just gorgeous. And do not forget that it is offline:)


 //Create a multi-source
 MultiMapFileTileSource mm distributions = new MultiMapFileTileSource ();

 File baseMapFile = getMapFile ("cyprus.map");
 MapFileTileSource tileSource = new MapFileTileSource ();
 tileSource.setMapFile (baseMapFile.getAbsolutePath ());
 mmtilesource.add (tileSource);//Add all sources to MultiMapFileTileSource

 MapFileTileSource worldTileSource = new MapFileTileSource ();

 File worldMapFile = getMapFile ("world.map");
 worldTileSource.setMapFile (worldMapFile.getAbsolutePath ());
 mmtilesource.add (worldTileSource);
//As a base map we use multi-source
 VectorTileLayer layer = this.map.setBaseMap (mmtilesource);  


Perhaps we are ready for release. We build the build, put it in the market and get well-deserved stars :)


A couple of spoons of tar in a huge barrel of honey


The open source engine is developing actively, but his team, frankly, is quite modest. By and large, this is one person under the name devemux86 . And a couple more guys from time to time.


Sometimes there are artifacts in the drawing, some blinking and twitching. But I have never encountered any critical problems, let alone falls, which is good news.


There is another nuance that may not like. This is drawing rounds and circles. An example of how this looks in the screenshot:





If there are a lot of points in the original geometry (rounding is smooth), then on the map you can see a rather “angular” circle with a lot of small bumps and concavities. Obviously, this is done for the sake of performance and the size of the map file, but it doesn’t look very good.


Perhaps this is all the cons for today. You decide whether you can live with them or not. Meanwhile, we have been using this library for more than 1.5 years, the flight is excellent, at least on Android.


Results


In this article, I showed that even such a rather non-trivial task can be solved relatively quickly. You have received a ready skeleton with which you can prototype any project involving the use of an offline map in the shortest time.


If there is interest, in the next article I will show you how to make floors a la 2GIS. And it is actually much simpler than it seems:)

Source text: The whole world in your pocket or how to make a mobile card in a couple of days