Le monde entier dans votre poche ou comment faire une carte mobile en quelques jours



Dans un article précédent, j'ai expliqué comment créer rapidement un numéroteur Web. Mais que se passe-t-il si vous définissez une tùche plus ambitieuse - assembler votre propre application avec une carte, sans publicité et avec blackjack? Et si en seulement quelques jours?


Faisons-le! Je demande un chat.


Voyons d'abord ce que nous devons faire. En sortie, nous voulons obtenir une application avec des données de référence et une carte. Et pour travailler hors ligne. En tant que développeur, je m'intéresse principalement à la carte, car nous savons déjà comment afficher les données de référence. Et hors ligne est une limitation assez forte dans ce cas, car il n'y a pas beaucoup de bonnes bibliothÚques avec un support hors ligne. Par conséquent, dans l'article, nous nous concentrerons sur la carte, mais parlons en passant du guide.


Choisissez un moteur de carte


La premiÚre chose à faire est d'obtenir les données de l'application. Il existe de nombreuses sources sur le marché, gratuites et peu nombreuses. Pour commencer, OpenStreetMap nous convient tout à fait en tant que source ouverte de données cartographiques. Vous pouvez y prendre un certain nombre de POI pour notre annuaire.


L'Ă©tape suivante consiste Ă  choisir un moteur de cartes. Sur Internet, il y en a un certain nombre, encore moins gratuits, et avec un support hors ligne, il n'y en a gĂ©nĂ©ralement que quelques-uns. Je suggĂšre d'utiliser une option assez cool - mapsforge / vtm . Il s'agit d'un moteur OpenGL vectoriel, trĂšs rapide, supporte offline, Android, iOS, diverses sources de donnĂ©es, style personnalisĂ©, superpositions, marqueurs, modĂšles 3D et mĂȘme 3D d'objets! TrĂšs, trĂšs cool.


Le rĂ©fĂ©rentiel contient de nombreux exemples de dĂ©marrage rapide, il existe des cartes prĂȘtes Ă  l'emploi, il existe un plug-in qui vous permet d'assembler votre propre carte Ă  partir de donnĂ©es au format OSM. Alors, commençons!


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

CrĂ©ez une source de donnĂ©es MapFileTileSource, spĂ©cifiez l'emplacement du fichier de carte. De plus, nous nous positionnons au centre du cadre englobant qui nous intĂ©resse, afin de ne pas ĂȘtre quelque part en dehors de l'emplacement sĂ©lectionnĂ© au dĂ©marrage de l'application. Installez le thĂšme par dĂ©faut. Ajoutez une couche de maisons et une couche de signatures. C’est tout. Nous lançons - des miracles!



Cela semble plus rapide et plus facile et ne pourrait pas l'ĂȘtre.


Nous faisons du géocodage


La prochaine Ă©tape importante consiste Ă  implĂ©menter le gĂ©ocodage. La carte elle-mĂȘme est dĂ©jĂ  bonne, mais l'interactivitĂ© est nĂ©cessaire. Nous voulons puiser dans la carte et voir des informations sur l'objet que nous avons touchĂ©. Et il y a une difficultĂ©. Dans l'ensemble, il n'y a pas de gĂ©ocodage Ă  part entiĂšre dans notre bibliothĂšque. C'est peut-ĂȘtre son plus gros inconvĂ©nient. Si rien n'est inventĂ©, alors nous pouvons profiter des fonctionnalitĂ©s existantes.


 //          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()); //   ,        MapDatabase mapDatabase = ((MapDatabase) ((OverzoomTileDataSource) tileSource.getDataSource()).getDataSource()); MapReadResult mapReadResult = mapDatabase.readLabels(upperLeft, lowerRight); StringBuilder sb = new StringBuilder(); //   POI     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) > touchRadius) { continue; } sb.append("\n"); List<Tag> tags = pointOfInterest.tags; for (Tag tag : tags) { sb.append("\n").append(tag.key).append("=").append(tag.value); } } //  ,     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<Tag> tags = way.tags; for (Tag tag : tags) { sb.append("\n").append(tag.key).append("=").append(tag.value); } } 

Il s'est avĂ©rĂ© relativement verbeux. Vous devez trouver une tuile, obtenir des moyens (dans la terminologie OSM, il s'agit d'un objet linĂ©aire), et vous pouvez en extraire certains attributs. En plus des moyens, il est possible d'obtenir un POI, mais c'est tout. Vous devrez remonter le reste de la logique vous-mĂȘme: choisissez le «correct» dans l'ensemble des objets que le clic a touchĂ©, filtrez par niveaux de zoom. Et encore une chose. En fait, nous perdons des informations sur la gĂ©omĂ©trie d'origine et n'obtenons en rĂ©ponse Ă  une recherche qu'un ensemble de lignes. Si vous souhaitez Ă©galement crĂ©er un gĂ©o-Ă©diteur, cela ne suffira Ă©videmment pas.


Mais pour démontrer l'approche, tout nous convient.





Géocodage avancé


D'une maniÚre générale, il existe une option plus avancée. Pour cela, nous avons besoin de notre propre base. En particulier, vous pouvez utiliser SQLite. Certes, SQLite standard ne nous suffira pas, et nous devrons créer le notre en y connectant le plugin RTree pour la recherche géographique. Comment faire cela, je l'ai déjà dit dans l' article , la section "Faites une bonne recherche."
Dans ce cas, nous obtenons un contrÎle total sur les données, nous pouvons enregistrer tout ce qui est nécessaire et dans le bon format. Nous pouvons également accélérer la recherche en texte intégral et rechercher nos objets géographiques et sociétés par nom, adresse et autres attributs.


La direction est:


  1. Nous fabriquons des tableaux:
    • objets gĂ©ographiques (id, type, gĂ©omĂ©trie, attributs)
    • entreprises (id, attributs, geo_id) en rĂ©fĂ©rence Ă  la gĂ©omĂ©trie du bĂątiment dans lequel il se trouve
    • rtree geoindex comme ceci:
       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. Nous remplissons tout de données.
  3. Lorsque vous accédez à la carte, nous obtenons GeoPoint et exécutons la demande:
     SELECT id FROM geo_index WHERE minX>=-81.08 AND maxX<=-80.58 AND minY>=35.00 AND maxY<=35.44 
  4. DerniÚre étape: filtrer et sélectionner l'objet approprié.

L'une des options d'implĂ©mentation peut ĂȘtre consultĂ©e dans le rĂ©fĂ©rentiel .


Par conséquent, nous savons déjà comment afficher la carte et gérer les clics. Pas mal.


Ajoutez des petites choses importantes.


Ajoutons quelques fonctionnalités importantes.


Commençons par l'emplacement actuel. Dans mapsforge / vtm, il y a juste un spĂ©cial pour cela. Couche LocationLayer. L'utilisation est extrĂȘmement simple.


 LocationLayer locationLayer = new LocationLayer(this.map); locationLayer.setEnabled(true); //       , ,     GPS GeoPoint initialGeoPoint = this.map.getMapPosition().getGeoPoint(); locationLayer.setPosition(initialGeoPoint.getLatitude(), initialGeoPoint.getLongitude(), 1); this.map.layers().add(locationLayer); 

Il n'y a qu'un seul inconvénient - c'est l'ondulation constante du «point bleu» sur la bordure de l'écran lorsque l'emplacement actuel est en dehors de la carte. TrÚs probablement, dans le processus d'utilisation, vous vous retrouverez rarement dans une telle situation, mais cela provoque un re-rendu constant, en conséquence, il charge un peu le processeur. Pour se débarrasser de cela, c'est un peu plus difficile, vous devez entrer dans le shader et le réparer. Mais c'est déjà pour les perfectionnistes. Comment faire - vous pouvez voir ici .


Donc, la position est. Il est temps d'ajouter le bouton de navigation Ă  la position actuelle, comme dans toutes les applications de cartographie qui se respectent.


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

Nous avons également besoin des boutons de zoom.


 View vZoomIn = findViewById(R.id.am_zoom_in); vZoomIn.setOnClickListener(v -> this.map.animator().animateZoom(500, 2, 0, 0)); View vZoomOut = findViewById(R.id.am_zoom_out); vZoomOut.setOnClickListener(v -> this.map.animator().animateZoom(500, 0.5, 0, 0)); 

Et la cerise sur le gĂąteau est une boussole.


 View vCompass = findViewById(R.id.am_compass); vCompass.setVisibility(View.GONE); vCompass.setOnClickListener(v -> { 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) -> { if (e == Map.ROTATE_EVENT) { vCompass.setRotation(mapPosition.getBearing()); vCompass.setVisibility(View.VISIBLE); } }); 




Capturer le monde


Amis, nous sommes à la ligne d'arrivée. Reste à ajouter la touche finale. Nous prévoyons de capturer le monde, ce qui signifie que nous devons en quelque sorte l'intégrer dans notre application.


Et les choses sont telles qu'avec notre moteur, c'est beaucoup plus facile qu'il n'y paraĂźt.
Nous devons légÚrement modifier la méthode de chargement de la carte en y ajoutant un MultyMapTileSource. Il s'agit essentiellement d'un wrapper pour toutes les autres sources de tuiles, qui vous permet d'afficher tout ce qui y est ajouté sur la carte à la fois. Juste une fonctionnalité qui tue. En conséquence, il nous reste à préparer une carte du monde avec un minimum de détails, à l'ajouter tout d'abord à notre emballage et à dessiner le reste par-dessus. De plus, nous pouvons immédiatement ajouter toutes les cartes que nous avons dans le catalogue avec des cartes de l'application! Magnifique, tout simplement magnifique. Et n'oubliez pas qu'il est hors ligne :)


 //  - MultiMapFileTileSource mmtilesource = new MultiMapFileTileSource(); File baseMapFile = getMapFile("cyprus.map"); MapFileTileSource tileSource = new MapFileTileSource(); tileSource.setMapFile(baseMapFile.getAbsolutePath()); mmtilesource.add(tileSource); //     MultiMapFileTileSource MapFileTileSource worldTileSource = new MapFileTileSource(); File worldMapFile = getMapFile("world.map"); worldTileSource.setMapFile(worldMapFile.getAbsolutePath()); mmtilesource.add(worldTileSource); //      - VectorTileLayer layer = this.map.setBaseMap(mmtilesource); 


Peut-ĂȘtre que nous sommes prĂȘts pour la sortie. Nous collectons la construction, la mettons sur le marchĂ© et obtenons les Ă©toiles bien mĂ©ritĂ©es :)


Un couple de cuillÚres de goudron dans un énorme baril de miel


Le moteur open source se développe activement, mais son équipe, franchement, est plutÎt modeste. Dans l'ensemble, il s'agit d'une personne sous le nom de devemux86 . Et quelques gars de temps en temps.


Parfois, il y a des artefacts dans le rendu, certains clignotent et se contractent. Mais je n'ai jamais rencontré de problÚmes critiques et encore plus de crashs, qui ne peuvent que se réjouir.


Il y a encore une nuance qui peut ne pas ĂȘtre agrĂ©able. Ceci est un dessin de filets et de cercles. Un exemple de la façon dont cela ressemble Ă  la capture d'Ă©cran:





Si dans la gĂ©omĂ©trie initiale il y a beaucoup de points (l'arrondi est lisse), alors sur la carte vous pouvez voir un cercle plutĂŽt "angulaire" avec de nombreux petits renflements et concavitĂ©s. Évidemment, cela est fait pour des raisons de performances et de taille du fichier de carte, mais cela n'a pas l'air trĂšs bien.


C'est peut-ĂȘtre tout le contre pour aujourd'hui. Vous dĂ©cidez si vous pouvez vivre avec eux ou non. Pendant ce temps, nous utilisons cette bibliothĂšque depuis plus d'un an et demi, le vol est excellent, du moins sur Android.


Résumé


Dans cet article, j'ai montrĂ© que mĂȘme un problĂšme assez banal peut ĂȘtre rĂ©solu assez rapidement. Vous avez un squelette prĂȘt Ă  l'emploi, avec lequel vous pouvez crĂ©er un prototype de tout projet impliquant l'utilisation d'une carte hors ligne en un minimum de temps.


S'il y a un intĂ©rĂȘt, dans le prochain article je montrerai comment faire des planchers Ă  la 2GIS. Et c'est en fait beaucoup plus simple qu'il n'y paraĂźt :)

Source: https://habr.com/ru/post/fr453182/


All Articles