MapViewport class
A MapViewport provides a view on a rectangular area of a map plane in a stack of map planes on different zoom levels.
It also provides the functionality to change between map planes (zoom in and zoom out) and to move the viewport around on the current map plane (pan left, rigth, up, or down).
A MapViewport manages a stack of map Layer
s.
A MapViewport is a PropertyObservable
. It emits propery change
events for
zoom
- emitted if the zoom level is changed
center
- emitted if the center of the map viewport is changed
class MapViewport extends Object with PropertyObservable{ DivElement _root; /// the root DOM element of the map viewport Element get root => _root; //TODO: make configurable. final ProjectedCRS _crs = new EPSG3857(); ProjectedCRS get crs => _crs; /** * Creates a map viewport. * * [container] is either an [Element] or a string consisting of * a CSS selector. */ MapViewport(container) { _require(container != null,"container must not be null"); if (container is String) { container = query(container); _require(container != null, "didn't find container with id '$container'"); } else if (container is Element) { // OK } else { _require(false,"expected Element or String, got $container"); } _root = new DivElement() ..classes.add("dartkart-map-viewport"); container.children ..clear() ..add(_root); attachEventListeners(); controlsPane = new ControlsPane(); } void attachEventListeners() { //TODO: remind subscription; solve detach _root.onMouseWheel.listen(_onMouseWheel); new _DragController(this); new _DoubleClickController(this); } /// Transforms projected coordinates [p] to coordinates in the current map /// zoom plane Point2D mapToZoomPlane(Point2D p) { var zp = zoomPlaneSize; var w = _crs.projectedBounds.width; var h = _crs.projectedBounds.height; return p.flipY() .translate(dx: w/2, dy: h/2) .scale(sx: zp.width/w, sy: zp.height/h) .toInt(); } /// Transforms coordinates [p] in the current map zoom plane to /// projected coordinates Point2D zoomPlaneToMap(Point2D p) { var zp = zoomPlaneSize; var w = _crs.projectedBounds.width; var h = _crs.projectedBounds.height; return p.scale(sx: w/zp.width, sy: h/zp.height) .translate(dx:-w/2, dy:-h/2) .flipY(); } /// Transforms coordinates in the current map zoom plane to viewport /// coordinates Point2D zoomPlaneToViewport(Point2D p) { var centerOnZoomPlane = mapToZoomPlane(earthToMap(center)); return (p - centerOnZoomPlane).toInt() + (viewportSize / 2); } /// viewport coordinates to coordinates in the current map zoom plane Point2D viewportToZoomPlane(Point2D p) { var centerOnZoomPlane = mapToZoomPlane(earthToMap(center)); var delta = p - (viewportSize / 2); return (centerOnZoomPlane + delta).toInt(); } /// Transforms geographic coordinates [ll] to projected coordinates Point2D earthToMap(LatLon ll) => _crs.project(ll); /// Transforms projected coordinates [p] to geographic coordinates LatLon mapToEarth(Point2D p) => _crs.unproject(p); /// the viewport size Dimension get viewportSize => new Dimension(_root.client.width, _root.client.height); /// the size of the current map zoom plane Dimension get zoomPlaneSize { var dim = (1 << zoom) * 256; return new Dimension(dim, dim); } /// the top-left point in "page coordinates" Point2D get topLeftInPage { offset(Element e) { var p = new Point2D(e.offset.left, e.offset.top); return e.parent == null ? p : p + offset(e.parent); } return offset(_root); } /** * The bounding box of the viewport in which we are currently rending * part of the map. * * The screen bounding box depends on the current zoom level ant the * current map center. In most cases, in partiuclar on zoom levels > 2, * it is equal to the extend of the map viewport. In lower zoom levels, * where the zoom plane is smaller than the map viewport, or if the * center is moved very far east, west, north, or south, it only covers * part of the viewport. * */ Bounds get screenBoundingBox { var vpSize = viewportSize; var vpCenter = (viewportSize / 2).toInt(); var zpSize = zoomPlaneSize; var zpCenter = mapToZoomPlane(earthToMap(center)); var zp = zoomPlaneSize; var x = math.max(0, vpCenter.x - zpCenter.x); var y = math.max(0, vpCenter.y - zpCenter.y); var width = math.min(vpSize.x, vpCenter.x + (zpSize.x - zpCenter.x)); var height = math.min(vpSize.y, vpCenter.y + (zpSize.y - zpCenter.y)); return new Bounds([x,y], [x+width, y+height]); } /** * Transforms page coordinates to viewport coordinates. * * [v] is either * * a [Point2D] * * a [MouseEvent] - uses the coordinates (pageX, pageY) * * The result are viewport coordinates for the map viewport where * * (0,0) is the upper left corner of the map viewport * * x runs to the right * * y runs down */ Point2D pageToViewport(v) { if (v is MouseEvent) { v = new Point2D(v.page.x, v.page.y); } else if (v is Point2D) {} // do nothing else throw new ArgumentError("expected MouseEvent or Point2D, got $v"); return v - topLeftInPage; } /** * Renders the map. */ void render() { _layers.forEach((l)=> l.render()); _controlsPane.layout(); } /* ----------------------- layer handling -------------------------- */ final List<Layer> _layers = []; /// update the z-indexes of the layer. Reflects the ordering in /// the layer stack. The layer with the highest index is renderer /// on top, the layer with index 0 is rendered at the bottom. _updateLayerZIndex() { var reversed = _layers.reversed.toList(); for (int i=0; i<layers.length; i++) { reversed[i].container.style.zIndex = (i * -100).toString(); } } /** * Adds a [layer] to the map. * * [layer] is appended to the list of layers of this map. It therefore * has the highest layer index and becomes rendered on top of the layer * stack of this map. * * Throws [ArgumentError] if [layer] is null. [layer] is ignored if it * is already attached to this map. * * ##Example * var source= "http://a.tile.openstreetmap.org/{z}/{x}/{y}.png"; * map.addLayer(new OsmLayer(tileSource: source)); */ void addLayer(Layer layer) { _require(layer != null, "layer must not be null"); if (hasLayer(layer)) return; _layers.add(layer); _root.children.add(layer.container); layer.attach(this); _updateLayerZIndex(); render(); _notifyLayerEvent(layer,LayerEvent.ADDED); } /** * Removes [layer] from the stack of layers of this map. * * Ignores [layer] if it is null or if it isn't attached to this map. */ void removeLayer(Layer layer) { if (layer == null) return; if (!hasLayer(layer)) return; layer.detach(); _root.children.remove(layer.container); _layers.remove(layer); _updateLayerZIndex(); render(); _notifyLayerEvent(layer,LayerEvent.REMOVED); } /// true, if [layer] is part of the layer stack of this map bool hasLayer(Layer layer) => _layers.contains(layer); /// an unmodifiable list of layers of this map. Empty, if /// no layes are defined. List<Layer> get layers => new UnmodifiableListView(_layers); /** * Moves the [layer] to the top. */ void moveToTop(Layer layer) { if (!hasLayer(layer)) return; _layers.remove(layer); _layers.add(layer); _updateLayerZIndex(); _notifyLayerEvent(layer,LayerEvent.MOVED); } /** * Moves the [layer] to the bottom. */ void moveToBottom(Layer layer) { if (!hasLayer(layer)) return; _layers ..remove(layer) ..insert(0, layer); _updateLayerZIndex(); _notifyLayerEvent(layer,LayerEvent.MOVED); } /** * Moves the [layer] to the position [index] in the * layer stack. */ void moveTo(Layer layer, int index) { if (!hasLayer(layer)) return; index = math.max(index, 0); index = math.min(index, _layers.length); _layers ..remove(layer) ..insert(index,layer); _updateLayerZIndex(); _notifyLayerEvent(layer,LayerEvent.MOVED); } final StreamController<LayerEvent> _layerEventsController = new StreamController<LayerEvent>(); Stream<LayerEvent> _layerEventsStream; _notifyLayerEvent(layer, type) { if (!_layerEventsController.hasListener) return; if (_layerEventsController.isPaused) return; var event = new LayerEvent(this, layer, type); _layerEventsController.sink.add(event); } /** * Stream of layer change events. * * ## Example * * map.onLayersChanged * .where((LayerEvent e) => e.type == LayerEvent.ADDED)) * .listen((LayerEvent e) { * print("layer added - num layers: ${map.layers.length}"); * }); */ Stream<LayerEvent> get onLayersChanged { if (_layerEventsStream == null) { _layerEventsStream = _layerEventsController.stream.asBroadcastStream(); } return _layerEventsStream; } /* ----------------------- zooming --------------------------- */ int _zoom = 0; /// the current zoom level int get zoom => _zoom; /** * Set the zoom level [zoom]. * * [zoom] >= 0 expected, otherwise throws an [ArgumentError]. */ void set zoom(int value) { _require(value >= 0, "zoom >= 0 expected, got $value"); if (value == _zoom) return; _zoom = value; render(); notify("zoom", _zoom, value); } /** * Zoom in by [delta] zoom levels. * * Throws [ArgumentError] if [delta] < 0. Fires a zoom change event. */ void zoomIn([int delta=1]) { _require(delta >= 0, "delta >= 0 expected, got $delta"); if (delta == 0) return; //TODO: check for max zoom level var oldZoom = _zoom; _zoom+= delta; render(); notify("zoom", oldZoom, _zoom); } /** * Zoom out by [delta] zoom levels. * * Throws [ArgumentError] if [delta] < 0. Fires a zoom change event. */ void zoomOut([int delta=1]) { if (delta < 0) throw new ArgumentError("delta >= 0 expected, got $delta"); if (delta == 0) return; var oldZoom = _zoom; _zoom = math.max(0, _zoom - delta); render(); notify("zoom", oldZoom, _zoom); } /* ----------------------- event handlers --------------------------- */ _onMouseWheel(WheelEvent evt) { var doZoom = evt.deltaY < 0 ? zoomIn : zoomOut; doZoom(); } /* --------------------- map center ---------------------------------- */ LatLon _center = new LatLon.origin(); /// the current map center in geographic coordinates LatLon get center => _center; /** * Sets the current map [center]. * * [center] must not be null. Broadcasts a [PropertyChangeEvent] if * the center is changed, see [onCenterChanged]. */ void set center(LatLon value) { _require(value != null, "center must not be null"); if (_center == value) return; var old = _center; _center = value; render(); notify("center", old, _center); } /* --------------------- panning ---------------------------------- */ void pan(delta, {bool animate: false}) { if (animate) { new PanBehaviour(this).animate(new Point2D.from(delta)); } else { delta = new Point2D.from(delta); var p = mapToZoomPlane(earthToMap(center)); p = p + delta; if (p.x <= 0 || p.y <= 0) return; var size = zoomPlaneSize; if (p.x >= size.width || p.y >= size.height) return; var c = zoomPlaneToMap(p); if (!crs.projectedBounds.contains(c)) return; center = mapToEarth(c); } } /// Pans the viewport num [pixels] to the north. /// Animates panning if [animate] is true. void panNorth({int pixels:100, bool animate:false}) => pan([0,-pixels], animate: animate); /// Pans the viewport num [pixels] to the south. /// Animates panning if [animate] is true. void panSouth({int pixels:100, bool animate:false}) => pan([0,pixels], animate: animate); /// Pans the viewport num [pixels] to the west /// Animates panning if [animate] is true. void panWest({int pixels:100, bool animate:false}) => pan([-pixels, 0], animate: animate); /// Pans the viewport num [pixels] to the east /// Animates panning if [animate] is true. void panEast({int pixels:100, bool animate:false}) => pan([pixels, 0],animate: animate); /* ----------------------- controls pane ------------------------ */ ControlsPane _controlsPane; /// the pane with the interactive map controls ControlsPane get controlsPane => _controlsPane; /// sets the [pane] for the interactive map controls void set controlsPane(ControlsPane pane) { if (pane == _controlsPane) return; // don't add twice if (_controlsPane != null) { _controlsPane.detach(); _root.children.remove(_controlsPane.root); } _controlsPane = pane; if (_controlsPane != null) { _controlsPane ..attach(this) // render the controls pane on top of the map layers. // The z-index for the top most layer is 0. ..root.style.zIndex = "100"; _root.children.add(_controlsPane.root); } } }
Extends
Object_PropertyObservable > MapViewport
Constructors
new MapViewport(container) #
Creates a map viewport.
container is either an Element
or a string consisting of
a CSS selector.
MapViewport(container) { _require(container != null,"container must not be null"); if (container is String) { container = query(container); _require(container != null, "didn't find container with id '$container'"); } else if (container is Element) { // OK } else { _require(false,"expected Element or String, got $container"); } _root = new DivElement() ..classes.add("dartkart-map-viewport"); container.children ..clear() ..add(_root); attachEventListeners(); controlsPane = new ControlsPane(); }
Properties
void set center(LatLon value) #
Sets the current map center.
center must not be null. Broadcasts a PropertyChangeEvent
if
the center is changed, see onCenterChanged
.
void set center(LatLon value) { _require(value != null, "center must not be null"); if (_center == value) return; var old = _center; _center = value; render(); notify("center", old, _center); }
ControlsPane get controlsPane #
the pane with the interactive map controls
ControlsPane get controlsPane => _controlsPane;
void set controlsPane(ControlsPane pane) #
sets the pane for the interactive map controls
void set controlsPane(ControlsPane pane) { if (pane == _controlsPane) return; // don't add twice if (_controlsPane != null) { _controlsPane.detach(); _root.children.remove(_controlsPane.root); } _controlsPane = pane; if (_controlsPane != null) { _controlsPane ..attach(this) // render the controls pane on top of the map layers. // The z-index for the top most layer is 0. ..root.style.zIndex = "100"; _root.children.add(_controlsPane.root); } }
final ProjectedCRS crs #
ProjectedCRS get crs => _crs;
final List<Layer> layers #
an unmodifiable list of layers of this map. Empty, if no layes are defined.
List<Layer> get layers => new UnmodifiableListView(_layers);
final Stream<LayerEvent> onLayersChanged #
Stream of layer change events.
Example
map.onLayersChanged
.where((LayerEvent e) => e.type == LayerEvent.ADDED))
.listen((LayerEvent e) {
print("layer added - num layers: ${map.layers.length}");
});
Stream<LayerEvent> get onLayersChanged { if (_layerEventsStream == null) { _layerEventsStream = _layerEventsController.stream.asBroadcastStream(); } return _layerEventsStream; }
final Stream<PropertyChangeEvent> onPropertyChanged #
the stream of property change events
Example
// an observable with a mixed in PropertyObservable
var observable = ...;
// listen for property change events for the property
// 'my_property'
observable.onPropertyChanged
.where((evt) => evt.name == "my_property")
.listen((evt) => print("new value: ${evt.newValue}"));
Stream<PropertyChangeEvent> get onPropertyChanged { //TODO: fix this - if at least one listener is present, // change events are emitted, regardless of whether the // individual listeners are paused or not. Consequence: // lots of change events are possibly queued up in // paused listener streams. => need a custom implementation // of a multiplexing stream which disards events if // they are streamed to a disabled listener // if (_stream == null) { _stream = _controller.stream.asBroadcastStream(); } return _stream; }
final Bounds screenBoundingBox #
The bounding box of the viewport in which we are currently rending part of the map.
The screen bounding box depends on the current zoom level ant the current map center. In most cases, in partiuclar on zoom levels > 2, it is equal to the extend of the map viewport. In lower zoom levels, where the zoom plane is smaller than the map viewport, or if the center is moved very far east, west, north, or south, it only covers part of the viewport.
Bounds get screenBoundingBox { var vpSize = viewportSize; var vpCenter = (viewportSize / 2).toInt(); var zpSize = zoomPlaneSize; var zpCenter = mapToZoomPlane(earthToMap(center)); var zp = zoomPlaneSize; var x = math.max(0, vpCenter.x - zpCenter.x); var y = math.max(0, vpCenter.y - zpCenter.y); var width = math.min(vpSize.x, vpCenter.x + (zpSize.x - zpCenter.x)); var height = math.min(vpSize.y, vpCenter.y + (zpSize.y - zpCenter.y)); return new Bounds([x,y], [x+width, y+height]); }
final Point2D topLeftInPage #
the top-left point in "page coordinates"
Point2D get topLeftInPage { offset(Element e) { var p = new Point2D(e.offset.left, e.offset.top); return e.parent == null ? p : p + offset(e.parent); } return offset(_root); }
final Dimension viewportSize #
the viewport size
Dimension get viewportSize => new Dimension(_root.client.width, _root.client.height);
Methods
void addLayer(Layer layer) #
Adds a layer to the map.
layer is appended to the list of layers of this map. It therefore has the highest layer index and becomes rendered on top of the layer stack of this map.
Throws ArgumentError
if
layer is null.
layer is ignored if it
is already attached to this map.
Example
var source= "http://a.tile.openstreetmap.org/{z}/{x}/{y}.png"; map.addLayer(new OsmLayer(tileSource: source));
void addLayer(Layer layer) { _require(layer != null, "layer must not be null"); if (hasLayer(layer)) return; _layers.add(layer); _root.children.add(layer.container); layer.attach(this); _updateLayerZIndex(); render(); _notifyLayerEvent(layer,LayerEvent.ADDED); }
void attachEventListeners() #
void attachEventListeners() { //TODO: remind subscription; solve detach _root.onMouseWheel.listen(_onMouseWheel); new _DragController(this); new _DoubleClickController(this); }
Point2D earthToMap(LatLon ll) #
Transforms geographic coordinates ll to projected coordinates
Point2D earthToMap(LatLon ll) => _crs.project(ll);
bool hasLayer(Layer layer) #
true, if layer is part of the layer stack of this map
bool hasLayer(Layer layer) => _layers.contains(layer);
LatLon mapToEarth(Point2D p) #
Transforms projected coordinates p to geographic coordinates
LatLon mapToEarth(Point2D p) => _crs.unproject(p);
Point2D mapToZoomPlane(Point2D p) #
Transforms projected coordinates p to coordinates in the current map zoom plane
Point2D mapToZoomPlane(Point2D p) { var zp = zoomPlaneSize; var w = _crs.projectedBounds.width; var h = _crs.projectedBounds.height; return p.flipY() .translate(dx: w/2, dy: h/2) .scale(sx: zp.width/w, sy: zp.height/h) .toInt(); }
void moveTo(Layer layer, int index) #
Moves the layer to the position index in the layer stack.
void moveTo(Layer layer, int index) { if (!hasLayer(layer)) return; index = math.max(index, 0); index = math.min(index, _layers.length); _layers ..remove(layer) ..insert(index,layer); _updateLayerZIndex(); _notifyLayerEvent(layer,LayerEvent.MOVED); }
void moveToBottom(Layer layer) #
Moves the layer to the bottom.
void moveToBottom(Layer layer) { if (!hasLayer(layer)) return; _layers ..remove(layer) ..insert(0, layer); _updateLayerZIndex(); _notifyLayerEvent(layer,LayerEvent.MOVED); }
void moveToTop(Layer layer) #
Moves the layer to the top.
void moveToTop(Layer layer) { if (!hasLayer(layer)) return; _layers.remove(layer); _layers.add(layer); _updateLayerZIndex(); _notifyLayerEvent(layer,LayerEvent.MOVED); }
void notify(String property, oldValue, newValue) #
Notifies observers about an update of the property with name property in this object. oldValue was replaced by newValue.
Observers are only notified, provided newValue is different from oldValue and if there is at least one listener.
void notify(String property, oldValue, newValue) { if (oldValue == newValue) return; //TODO: fix me - see notes in onPropertyChanged if (!_controller.hasListener || _controller.isPaused) return; _controller.sink.add( new PropertyChangeEvent(this, property,oldValue,newValue) ); }
Point2D pageToViewport(v) #
Transforms page coordinates to viewport coordinates.
v is either
* a Point2D
* a MouseEvent - uses the coordinates (pageX, pageY)
The result are viewport coordinates for the map viewport where * (0,0) is the upper left corner of the map viewport * x runs to the right * y runs down
Point2D pageToViewport(v) { if (v is MouseEvent) { v = new Point2D(v.page.x, v.page.y); } else if (v is Point2D) {} // do nothing else throw new ArgumentError("expected MouseEvent or Point2D, got $v"); return v - topLeftInPage; }
void pan(delta, {bool animate: false}) #
void pan(delta, {bool animate: false}) { if (animate) { new PanBehaviour(this).animate(new Point2D.from(delta)); } else { delta = new Point2D.from(delta); var p = mapToZoomPlane(earthToMap(center)); p = p + delta; if (p.x <= 0 || p.y <= 0) return; var size = zoomPlaneSize; if (p.x >= size.width || p.y >= size.height) return; var c = zoomPlaneToMap(p); if (!crs.projectedBounds.contains(c)) return; center = mapToEarth(c); } }
void panEast({int pixels: 100, bool animate: false}) #
Pans the viewport num pixels to the east Animates panning if animate is true.
void panEast({int pixels:100, bool animate:false}) => pan([pixels, 0],animate: animate);
void panNorth({int pixels: 100, bool animate: false}) #
Pans the viewport num pixels to the north. Animates panning if animate is true.
void panNorth({int pixels:100, bool animate:false}) => pan([0,-pixels], animate: animate);
void panSouth({int pixels: 100, bool animate: false}) #
Pans the viewport num pixels to the south. Animates panning if animate is true.
void panSouth({int pixels:100, bool animate:false}) => pan([0,pixels], animate: animate);
void panWest({int pixels: 100, bool animate: false}) #
Pans the viewport num pixels to the west Animates panning if animate is true.
void panWest({int pixels:100, bool animate:false}) => pan([-pixels, 0], animate: animate);
void removeLayer(Layer layer) #
Removes layer from the stack of layers of this map.
Ignores layer if it is null or if it isn't attached to this map.
void removeLayer(Layer layer) { if (layer == null) return; if (!hasLayer(layer)) return; layer.detach(); _root.children.remove(layer.container); _layers.remove(layer); _updateLayerZIndex(); render(); _notifyLayerEvent(layer,LayerEvent.REMOVED); }
void render() #
Renders the map.
void render() { _layers.forEach((l)=> l.render()); _controlsPane.layout(); }
Point2D viewportToZoomPlane(Point2D p) #
viewport coordinates to coordinates in the current map zoom plane
Point2D viewportToZoomPlane(Point2D p) { var centerOnZoomPlane = mapToZoomPlane(earthToMap(center)); var delta = p - (viewportSize / 2); return (centerOnZoomPlane + delta).toInt(); }
void zoomIn([int delta = 1]) #
Zoom in by delta zoom levels.
Throws ArgumentError
if
delta < 0. Fires a zoom change event.
void zoomIn([int delta=1]) { _require(delta >= 0, "delta >= 0 expected, got $delta"); if (delta == 0) return; //TODO: check for max zoom level var oldZoom = _zoom; _zoom+= delta; render(); notify("zoom", oldZoom, _zoom); }
void zoomOut([int delta = 1]) #
Zoom out by delta zoom levels.
Throws ArgumentError
if
delta < 0. Fires a zoom change event.
void zoomOut([int delta=1]) { if (delta < 0) throw new ArgumentError("delta >= 0 expected, got $delta"); if (delta == 0) return; var oldZoom = _zoom; _zoom = math.max(0, _zoom - delta); render(); notify("zoom", oldZoom, _zoom); }
Point2D zoomPlaneToMap(Point2D p) #
Transforms coordinates p in the current map zoom plane to projected coordinates
Point2D zoomPlaneToMap(Point2D p) { var zp = zoomPlaneSize; var w = _crs.projectedBounds.width; var h = _crs.projectedBounds.height; return p.scale(sx: w/zp.width, sy: h/zp.height) .translate(dx:-w/2, dy:-h/2) .flipY(); }