Статьи

Реализация рендера карт общего профиля для JavaFX (часть 1: модель)


Если вы посмотрите одну из демонстраций JavaFX на
javafx.com , вы найдете что-то с рендером карт. Несмотря на то, что он работает нормально, к сожалению, демонстрационная версия оборачивает компонент JXMapViewer из
SwingX , который может работать только в профиле рабочего стола; другими словами, это не будет работать на мобильном телефоне.

В рамках проекта blueBill Mobile я разработал чистую реализацию средства визуализации карт, основанного на общем профиле, который может работать везде, в том числе на мобильном телефоне. Эта библиотека является гибкой и настраиваемой, и, конечно, включает возможность наложения слоев с различными видами информации.

Работа в основном заключается в порте похожего API для JME, который является частью windRose , с некоторым специфическим рефакторингом для JavaFX (а также некоторым общим рефакторингом, который должна догнать библиотека JME).

Из-за этой ошибки я пока не смог раскрутить подпроект из BlueBill Mobile . Вы можете проверить источники, из которых я описываю:

svn co -r 167 https://kenai.com/svn/bluebill-mobile~svn/trunk/src/FX/blueBill-mobileFX/src/it/tidalwave/geo/mapfx

Партнер JME может быть проверен из:

svn co -r 490 https://windrose.dev.java.net/svn/windrose/trunk/src/MobileMaps

В этой первой статье я собираюсь описать более простую часть библиотеки, то есть ее модель.

 

Точка, координаты, сектор

Первые классы моделей довольно очевидны:

  • Точка : представляет собой точку в декартовой системе с целочисленными координатами. Он может представлять точку визуализированной карты и точку растрового изображения карты.
  • Координаты : представляет точку в географических координатах (широта, долгота, высота).
  • Сектор : представляет географическую область, ограниченную минимальной / максимальной широтой и минимальной / максимальной долготой.

Там не так много, чтобы сказать, но короткая записка для координат. JSR-179 (Location API) предоставляет правильную реализацию этой концепции, которую я изначально использовал для своего кода JavaFX. Причина, по которой я предпочел новую реализацию, заключается в том, что таким образом я могу в полной мере наслаждаться связыванием JavaFX.

package it.tidalwave.geo.mapfx.model;

public class Point
{
public var x: Integer;
public var y: Integer;

public function withTranslation (deltaX : Integer, deltaY : Integer)
{
return Point
{
x: x + deltaX
y: y + deltaY
};
}

public function withAntiTranslation (deltaX : Integer, deltaY : Integer)
{
return Point
{
x: x - deltaX
y: y - deltaY
};
}

public function withTranslation (point : Point)
{
return withTranslation(point.x, point.y);
}

public function withAntiTranslation (point : Point)
{
return withAntiTranslation(point.x, point.y);
}

override function toString()
{
return "Point[{x};{y}]";
}
}

package it.tidalwave.geo.mapfx.model;

public class Coordinates
{
public var latitude : Number = 0;
public var longitude : Number = 0;
public var altitude : Number = 0;

override function toString()
{
return "Coordinates[{latitude},{longitude},{altitude}]";
}
}


package it.tidalwave.geo.mapfx.model;

public class Sector
{
public var minLatitude : Number = 0;
public var maxLatitude : Number = 0;
public var minLongitude : Number = 0;
public var maxLongitude : Number = 0;

override function toString()
{
return "Sector[{minLatitude},{minLongitude} -> {maxLatitude},{maxLongitude}]";
}
}

 

Проекция, MercatorProjection

Проекция (карта) — это метод представления поверхности сферы или другой фигуры на плоскости. С точки зрения программного обеспечения, это класс, который должен предоставлять методы для преобразования координат в точку и наоборот. Проекция — это абстрактный класс, который может быть реализован различными способами в соответствии с необходимой нам системой проекции карты. MercatorProjection — это распространенный метод, используемый как OpenStreetMap, так и Microsoft Virtual Earth .

Есть только несколько вещей, которые можно сказать о MercatorProjection:

  • JavaFXScript не предоставляет побитовые операторы (ни сдвиг, ни побитовые операции). Вместо них есть класс javafx.util.Bits с некоторыми статическими методами.
  • В то время как вы можете использовать java.lang.Math для доступа к некоторым математическим функциям, в мобильном профиле этого класса не хватает многих вещей (потому что это реализация JME), включая экспоненциальные и тригонометрические функции, которые необходимы системе проекции карты. Начиная с JavaFX 1.2 у вас есть класс javafx.util.Math со всем необходимым, и это небольшое, но важное улучшение.
package it.tidalwave.geo.mapfx.model;

public abstract class Projection
{
public abstract function longitudeToX (longitude : Number, zoomLevel : Integer) : Integer;

public abstract function latitudeToY (latitude : Number, zoomLevel : Integer) : Integer;

public abstract function xToLongitude (x : Integer, zoomLevel : Integer) : Number;

public abstract function yToLatitude (y : Integer, zoomLevel : Integer) : Number;

public function coordinatesToPoint (coordinates : Coordinates, zoomLevel : Integer) : Point
{
return Point
{
y: latitudeToY(coordinates.latitude, zoomLevel)
x: longitudeToX(coordinates.longitude, zoomLevel)
};
}

public function pointToCoordinates (point : Point, zoomLevel : Integer) : Coordinates
{
return Coordinates
{
latitude: yToLatitude(point.y, zoomLevel)
longitude: xToLongitude(point.x, zoomLevel)
}
}

public abstract function metersPerPixel (coordinates : Coordinates, zoomLevel : Integer) : Number;
}


package it.tidalwave.geo.mapfx.model;

import javafx.util.Bits;
import javafx.util.Math;

public class MercatorProjection extends Projection
{
public-init var maxZoomLevel : Integer;

public-init var tileSize : Integer;

def EARTH_RADIUS = 6378137;
def EARTH_CIRCUMFERENCE = EARTH_RADIUS * 2.0 * Math.PI;
def EARTH_HALF_CIRCUMFERENCE = EARTH_CIRCUMFERENCE / 2;

override function metersPerPixel (coordinates : Coordinates, zoomLevel : Integer) : Number
{
return earthArc(zoomLevel) * Math.cos(Math.toRadians(coordinates.latitude));
}

override function longitudeToX (longitude : Number, zoomLevel : Integer) : Integer
{
def metersX = EARTH_RADIUS * Math.toRadians(longitude);
return Math.round((EARTH_HALF_CIRCUMFERENCE + metersX) / earthArc(zoomLevel));
}

override function latitudeToY (latitude : Number, zoomLevel : Integer) : Integer
{
def sinLat = Math.sin(Math.toRadians(latitude));
def metersY = EARTH_RADIUS / 2 * Math.log((1 + sinLat) / (1 - sinLat));
return Math.round((EARTH_HALF_CIRCUMFERENCE - metersY) / earthArc(zoomLevel));
}

override function xToLongitude (x : Integer, zoomLevel : Integer) : Number
{
def metersX = x * earthArc(zoomLevel) - EARTH_HALF_CIRCUMFERENCE;
return Math.toDegrees(metersX / EARTH_RADIUS);
}

override function yToLatitude (y : Integer, zoomLevel : Integer) : Number
{
def metersY = EARTH_HALF_CIRCUMFERENCE - y * earthArc(zoomLevel);
def exp = Math.exp(metersY / (EARTH_RADIUS / 2));
return Math.toDegrees(Math.asin((exp - 1) / (exp + 1)));
}

function earthArc (zoomLevel : Integer): Number
{
return EARTH_CIRCUMFERENCE / ((Bits.shiftLeft(1, maxZoomLevel - zoomLevel)) * tileSize);
}
}

 

TileSource, TileSourceSupport

TileSource — это класс, который предоставляет листы карты для заданных координат; более конкретно, он предоставляет те же возможности преобразования Projection, с помощью дополнительной функции findTileURL (), которая возвращает URL-адрес фрагмента карты, который должен быть загружен из удаленной службы предоставления карт, такой как OpenStreetMap или Microsoft Visual Earth.

TileSourceSupport — это частичная реализация, которая делегирует функции преобразования координат экземпляру Projection; конкретные реализации должны наследоваться от этого класса, обеспечивая реализацию findTileURL ().

package it.tidalwave.geo.mapfx.model;

public abstract class TileSource
{
public-read protected var displayName : String;

public-read protected var maxZoomLevel : Integer;

public-read protected var minZoomLevel : Integer;

public-read protected var defaultZoomLevel : Integer;

public-read protected var cachePrefix : String;

public-read protected var tileSize : Integer;

public abstract function findTileURL (x : Integer, y : Integer, zoomLevel : Integer) : String;

public abstract function coordinatesToPoint (coordinates : Coordinates, zoomLevel : Integer) : Point;

public abstract function pointToCoordinates (point : Point, zoomLevel : Integer): Coordinates;

public abstract function longitudeToX (longitude : Number, zoomLevel : Integer) : Integer;

public abstract function latitudeToY (latitude : Number, zoomLevel : Integer) : Integer;

public abstract function metersPerPixel (coordinates : Coordinates, zoomLevel : Integer) : Number;

override function toString()
{
return "TileSource[{displayName}, zoom: {minZoomLevel}-{maxZoomLevel};{defaultZoomLevel} "
"cachePrefix: {cachePrefix} tileSize:{tileSize}";
}
}


package it.tidalwave.geo.mapfx.model;

public abstract class TileSourceSupport extends TileSource
{
public-read protected var projection : Projection;

override function coordinatesToPoint (coordinates : Coordinates, zoomLevel : Integer) : Point
{
return projection.coordinatesToPoint(coordinates, zoomLevel);
}

override function pointToCoordinates (point : Point, zoomLevel : Integer) : Coordinates
{
return projection.pointToCoordinates(point, zoomLevel);
}

override function latitudeToY (latitude : Number, zoomLevel : Integer) : Integer
{
return projection.latitudeToY(latitude, zoomLevel);
}

override function longitudeToX (longitude : Number, zoomLevel : Integer) : Integer
{
return projection.longitudeToX(longitude, zoomLevel);
}

override function metersPerPixel (coordinates : Coordinates, zoomLevel : Integer) : Number
{
return projection.metersPerPixel(coordinates, zoomLevel);
}
}

 

Реализации TileSource для OpenStreetMaps и Microsoft Visual Earth

Ниже вы можете найти источники конкретных реализаций TileSource для двух наиболее распространенных карт, предоставляющих веб-сервисы, OpenStreetMap (OSM) и Microsoft Virtual Earth (MVE). Реализация MVE принимает параметр, который позволяет выбрать чистый картографический сервис, спутниковый сервис и картографический спутниковый сервис.

Я не буду вдаваться в подробности алгоритма, используемого для вычисления URL, так как вы можете найти их в документации двух веб-сервисов.

Единственное, на что стоит обратить внимание, это то, что реализация MVE снова демонстрирует использование javafx.lang.Bits как для сдвигов битов, так и для битов и.

В качестве примечания, хотя OSM является полностью открытым исходным кодом и может свободно использоваться, напомним, что MVE не открыт, и вам следует связаться с представителем Microsoft для получения разрешения на подключение к нему.

Код может легко работать с Картами Google, но использование их карт вне компонента веб-браузера, напрямую использующего API Карт Google в JavaScript, строго запрещено.

package it.tidalwave.geo.mapfx.model.osm;

import it.tidalwave.geo.mapfx.model.TileSourceSupport;
import it.tidalwave.geo.mapfx.model.MercatorProjection;

public class OSMTileSource extends TileSourceSupport
{
init
{
displayName = "OpenStreetMap";
cachePrefix = "OSM/";
minZoomLevel = 1;
maxZoomLevel = 17;
defaultZoomLevel = 9;
tileSize = 256;

projection = MercatorProjection
{
maxZoomLevel: maxZoomLevel
tileSize: tileSize
};
}

override function findTileURL (x : Integer, y : Integer, zoom : Integer) : String
{
return "http://tile.openstreetmap.org/{(maxZoomLevel - zoom)}/{x}/{y}.png";
}
}





package it.tidalwave.geo.mapfx.model.mve;

import javafx.util.Bits;
import it.tidalwave.geo.mapfx.model.TileSourceSupport;
import it.tidalwave.geo.mapfx.model.MercatorProjection;

public class Mode
{
var type : String;
var ext : String;
var displayName : String;
}

public def MAP = Mode
{
displayName : "map"
type : "r"
ext : ".png"
};

public def SATELLITE = Mode
{
displayName : "satellite"
type : "a"
ext : ".jpeg"
};

public def MAP_AND_SATELLITE = Mode
{
displayName : "map + satellite"
type : "h"
ext : ".jpeg"
};

public class MVETileSource extends TileSourceSupport
{
public-init var mode = MAP on replace
{
displayName = "Microsoft Virtual Earth ({mode.displayName})";
};

init
{
displayName = "Microsoft Virtual Earth";
cachePrefix = "MVE/";
minZoomLevel = 1;
maxZoomLevel = 19;
defaultZoomLevel = 7;
tileSize = 256;

projection = MercatorProjection
{
maxZoomLevel: maxZoomLevel
tileSize: tileSize
};
}

override function findTileURL (x : Integer, y : Integer, zoomLevel : Integer) : String
{
def quad = tileToQuadKey(x, y, maxZoomLevel - zoomLevel);
return "http://{mode.type}{quad.charAt(quad.length() - 1)}.ortho.tiles.virtualearth.net/"
"tiles/{mode.type}{quad}{mode.ext}?g=1";
}

function tileToQuadKey (x : Integer, y : Integer, zoomLevel : Integer): String
{
var quad = "";
var i = zoomLevel;

while (i > 0)
{
def mask = Bits.shiftLeft(1, (i - 1));
var cell = 0;

if (Bits.bitAnd(x, mask) != 0)
{
cell++;
}

if (Bits.bitAnd(y, mask) != 0)
{
cell += 2;
}

quad += "{cell}";
i--;
}

return quad;
}
}

 

 

MapModel (и MapView)

The last class we’re going to see is MapModel. It’s basically a façade for the whole system, as it’s the only class of the model that you have to instantiate in order to render a map. It can be configured with a TileSource and basically delegates its functions to it.

The current implementation is not complete. In windRose, the equivalent class (still named TileProvider) also includes support for downloading tiles from the internet with background threads (something that we don’t need in JavaFX since background downloading is performed by the runtime. But it also provides the capability of caching map tiles on the local storage, a thing that I’ve not implemented yet since JavaFX 1.2 has introduced a portable way to access the local storage (being a filesystem or anything else) that I’ve to try yet.

package it.tidalwave.geo.mapfx.model;

public class MapModel
{
public var tileSource : TileSource;

public-read var maxZoomLevel = bind tileSource.maxZoomLevel;

public-read var minZoomLevel = bind tileSource.minZoomLevel;

public-read var defaultZoomLevel = bind tileSource.defaultZoomLevel;

public-read var cachePrefix = bind tileSource.cachePrefix;

public var internetDownloadAllowed = false;

public function metersPerPixel (coordinates : Coordinates, zoomLevel : Integer) : Number
{
return tileSource.metersPerPixel(coordinates, zoomLevel);
}

public function coordinatesToPoint (coordinates : Coordinates, zoomLevel : Integer) : Point
{
return tileSource.coordinatesToPoint(coordinates, zoomLevel);
}

public function pointToCoordinates (point : Point, zoomLevel : Integer) : Coordinates
{
return tileSource.pointToCoordinates(point, zoomLevel);
}

public function findTileURL (x : Integer, y : Integer, zoomLevel : Integer) : String
{
return tileSource.findTileURL(x, y, zoomLevel);
}

public function latitudeToY (latitude : Number, zoomLevel : Integer) : Integer
{
return tileSource.latitudeToY(latitude, zoomLevel);
}

public function longitudeToX (longitude : Number, zoomLevel : Integer) : Integer
{
return tileSource.longitudeToX(longitude, zoomLevel);
}
}

While the full description of the rendering component (MapView and companions) will be published in a future article, I’m just giving you an example on how the MapModel can be used together MapView in an application:

import it.tidalwave.geo.mapfx.model.Coordinates;
import it.tidalwave.geo.mapfx.model.MapModel;
import it.tidalwave.geo.mapfx.model.mve.MVETileSource;

def mapModel = MapModel
{
tileSource: MVETileSource{};
};

var centerCoordinates = Coordinates {latitude: 44.410208; longitude: 8.926315 };

def mapView = MapView
{
mapModel: bind mapModel
width: 480
height: 640
centerCoordinates: bind centerCoordinates
enabled: true
};