Die ganze Welt in der Tasche oder wie man in ein paar Tagen eine Handykarte macht



In einem früheren Artikel habe ich darüber gesprochen, wie man schnell einen Web-Dialer erstellt. Aber was ist, wenn Sie sich eine ehrgeizigere Aufgabe stellen - Ihre eigene Anwendung mit einer Karte, ohne Werbung und mit Blackjack zusammenzustellen? Und wenn in nur wenigen Tagen?


Lass es uns tun! Ich frage nach Katze.


Lassen Sie uns zunächst herausfinden, was wir tun müssen. Am Ausgang möchten wir eine Anwendung mit Referenzdaten und einer Karte erhalten. Und offline arbeiten. Als Entwickler interessiert mich vor allem nur die Karte, da wir bereits wissen, wie Referenzdaten angezeigt werden. Und offline ist in diesem Fall eine ziemlich starke Einschränkung, da es nicht viele gute Bibliotheken mit Offline-Unterstützung gibt. Daher konzentrieren wir uns im Artikel auf die Karte, aber lassen Sie uns nebenbei über den Leitfaden sprechen.


Wählen Sie eine Karten-Engine


Als erstes müssen Sie die Daten für die Anwendung abrufen. Es gibt viele Quellen auf dem Markt, kostenlos und nicht sehr. OpenStreetMap eignet sich für uns zunächst als Open Source für Kartendaten. Dort können Sie eine bestimmte Anzahl von POIs für unser Verzeichnis nehmen.


Der nächste Schritt ist die Auswahl einer Karten-Engine. Im Internet gibt es einige davon, noch weniger kostenlose, und mit Offline-Unterstützung im Allgemeinen gibt es nur wenige. Ich schlage vor, eine ziemlich coole Option zu verwenden - mapsforge / vtm . Dies ist eine Vektor-OpenGL-Engine, die sehr schnell ist und Offline-, Android-, iOS-, verschiedene Datenquellen, benutzerdefiniertes Styling, Overlays, Marker, 3D- und sogar 3D-Modelle von Objekten unterstützt! Sehr sehr cool.


Das Repository enthält viele Beispiele für den Schnellstart. Es gibt vorgefertigte Karten und ein Plug-In, mit dem Sie Ihre eigene Karte aus Daten im OSM-Format zusammenstellen können. Also, fangen wir an!


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

Erstellen Sie eine MapFileTileSource-Datenquelle und geben Sie den Speicherort der Kartendatei an. Darüber hinaus positionieren wir uns in der Mitte des Begrenzungsrahmens, der uns interessiert, um beim Start der Anwendung nicht außerhalb des ausgewählten Speicherorts zu sein. Installieren Sie das Standarddesign. Fügen Sie eine Schicht Häuser und eine Schicht Signaturen hinzu. Das ist alles Wir starten - Wunder!



Es scheint schneller und einfacher und könnte nicht sein.


Wir machen Geokodierung


Der nächste wichtige Schritt ist die Implementierung der Geokodierung. Die Karte selbst ist bereits gut, aber Interaktivität ist erforderlich. Wir möchten auf die Karte tippen und Informationen zu dem Objekt sehen, das wir getroffen haben. Und es gibt einige Schwierigkeiten. Im Großen und Ganzen gibt es in unserer Bibliothek keine vollständige Geokodierung. Dies ist vielleicht das größte Minus. Wenn nichts erfunden wird, können wir die vorhandene Funktionalität nutzen.


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

Es stellte sich als relativ ausführlich heraus. Sie müssen eine Kachel finden, Wege finden (in der OSM-Terminologie ist dies ein lineares Objekt) und einige Attribute daraus extrahieren. Zusätzlich zu den Möglichkeiten ist es möglich, einen POI zu erhalten, aber das ist alles. Sie müssen den Rest der Logik selbst erledigen: Wählen Sie aus dem gesamten Objektsatz, den der Klick getroffen hat, das „Richtige“ aus und filtern Sie nach Zoomstufen. Und noch etwas. Tatsächlich verlieren wir Informationen über die ursprüngliche Geometrie und erhalten als Antwort auf eine Suche nur eine Reihe von Linien. Wenn Sie auch einen Geo-Editor erstellen möchten, reicht dies natürlich nicht aus.


Aber um den Ansatz zu demonstrieren, passt alles zu uns.





Erweiterte Geokodierung


Im Allgemeinen gibt es eine erweiterte Option. Dafür brauchen wir unsere eigene Basis. Insbesondere können Sie SQLite verwenden. Es stimmt, Standard-SQLite wird uns nicht ausreichen, und wir müssen unser eigenes erstellen, indem wir das RTree-Plugin für die Geosuche damit verbinden. Wie das geht, habe ich bereits im Artikel im Abschnitt "Gute Suche" erklärt.
In diesem Fall haben wir die volle Kontrolle über die Daten, können alles Notwendige und im richtigen Format speichern. Wir können auch die Volltextsuche beschleunigen und nach unseren Geoobjekten und Unternehmen nach Name, Adresse und anderen Attributen suchen.


Die Richtung ist:


  1. Wir machen Tabellen:
    • Geoobjekte (ID, Typ, Geometrie, Attribute)
    • Firmen (ID, Attribute, geo_id) in Bezug auf die Geometrie des Gebäudes, in dem es sich befindet
    • rtree geoindex wie folgt :
       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. Wir füllen alles mit Daten.
  3. Wenn Sie auf die Karte tippen, erhalten wir GeoPoint und führen die Anforderung aus:
     SELECT id FROM geo_index WHERE minX>=-81.08 AND maxX<=-80.58 AND minY>=35.00 AND maxY<=35.44 
  4. Letzter Schritt: Filtern und wählen Sie das entsprechende Objekt aus.

Eine der Implementierungsoptionen kann im Repository angezeigt werden.


Daher wissen wir bereits, wie die Karte angezeigt und Klicks verarbeitet werden. Nicht schlecht.


Fügen Sie wichtige Kleinigkeiten hinzu.


Fügen wir einige wichtige Funktionen hinzu.


Beginnen wir mit dem aktuellen Standort. In mapsforge / vtm gibt es dafür nur ein besonderes. LocationLayer-Ebene. Die Bedienung ist sehr einfach.


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

Es gibt nur einen Nachteil: Es ist die konstante Welligkeit des „blauen Punkts“ am Bildschirmrand, wenn sich der aktuelle Standort außerhalb der Karte befindet. Wahrscheinlich werden Sie sich im Verlauf des Gebrauchs selten in einer solchen Situation befinden, aber dies führt zu einem ständigen erneuten Rendern, dementsprechend wird der Prozessor ein wenig belastet. Um dies zu beseitigen, ist es etwas schwieriger, in den Shader zu gelangen und ihn zu reparieren. Aber das ist schon für Perfektionisten. Wie es geht - sehen Sie hier .


Die Position ist also. Wie bei allen Mapping-Anwendungen mit Selbstachtung ist es an der Zeit, die Navigationsschaltfläche an der aktuellen Position hinzuzufügen.


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

Wir brauchen auch die Zoomtasten.


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

Und die Kirsche auf dem Kuchen ist ein Kompass.


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




Die Welt einfangen


Freunde, wir sind am Ziel. Es bleibt der letzte Schliff hinzuzufügen. Wir planen, die Welt einzufangen, was bedeutet, dass wir sie irgendwie in unsere Anwendung packen müssen.


Und die Dinge sind so, dass es mit unserem Motor viel einfacher ist, als es sich anhört.
Wir müssen die Methode zum Laden der Karte leicht ändern, indem wir eine MultyMapTileSource hinzufügen. Dies ist im Wesentlichen ein Wrapper für alle anderen Kachelquellen, mit dem Sie alles, was hinzugefügt wurde, auf der Karte gleichzeitig anzeigen können. Nur ein Killer-Feature. Daher bleibt es uns überlassen, eine Weltkarte mit minimalen Details zu erstellen, sie zuerst in unseren Umschlag zu legen und den Rest darauf zu zeichnen. Außerdem können wir sofort alle Karten, die wir im Katalog haben, mit Karten der Anwendung hinzufügen! Wunderschön, einfach wunderschön. Und vergiss nicht, dass es offline ist :)


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


Vielleicht sind wir bereit für die Veröffentlichung. Wir sammeln den Build, bringen ihn auf den Markt und bekommen die verdienten Sterne :)


Ein paar Löffel Teer in einem riesigen Fass Honig


Die Open-Source-Engine entwickelt sich aktiv weiter, aber sein Team ist ehrlich gesagt eher bescheiden. Im Großen und Ganzen ist dies eine Person unter dem Namen devemux86 . Und von Zeit zu Zeit noch ein paar Leute.


Manchmal gibt es Artefakte im Rendering, einige blinken und zucken. Aber ich bin nie auf kritische Probleme und umso mehr Abstürze gestoßen, die sich nur freuen können.


Es gibt noch eine Nuance, die möglicherweise nicht angenehm ist. Dies ist eine Zeichnung von Filets und Kreisen. Ein Beispiel dafür, wie dies im Screenshot aussieht:





Wenn es in der anfänglichen Geometrie viele Punkte gibt (die Rundung ist glatt), sehen Sie auf der Karte einen eher „eckigen“ Kreis mit vielen kleinen Ausbuchtungen und Konkavitäten. Dies geschieht natürlich aus Gründen der Leistung und der Größe der Kartendatei, sieht aber nicht sehr gut aus.


Vielleicht ist dies der Nachteil für heute. Sie entscheiden, ob Sie mit ihnen leben können oder nicht. Mittlerweile nutzen wir diese Bibliothek seit mehr als 1,5 Jahren, der Flug ist zumindest unter Android hervorragend.


Zusammenfassung


In diesem Artikel habe ich gezeigt, dass selbst ein solches eher nicht triviales Problem relativ schnell gelöst werden kann. Sie haben ein fertiges Skelett erhalten, mit dem Sie jedes Projekt, bei dem eine Offline-Karte verwendet wird, in kürzester Zeit prototypisieren können.


Bei Interesse werde ich im nächsten Artikel zeigen, wie man Fußböden a la 2GIS macht. Und das ist tatsächlich viel einfacher als es scheint :)

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


All Articles