447 lines
15 KiB
JavaScript
447 lines
15 KiB
JavaScript
/**
|
|
* Horde mapping service. This file provides a general API for interacting with
|
|
* inline "slippy" maps. You must also include the file for the specific
|
|
* provider support you want included.
|
|
*
|
|
* Copyright 2009-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file COPYING for license information (GPL). If you
|
|
* did not receive this file, see http://www.horde.org/licenses/gpl.
|
|
*
|
|
* @author Michael J. Rubinsky <mrubinsk@horde.org>
|
|
*/
|
|
|
|
/**
|
|
* Class for dealing with OpenLayers based inline mapping.
|
|
*
|
|
* Requires that the openlayer.js file has been included, as well as any js files
|
|
* specific to any commercial mapping providers, such as google/bing/osm etc...
|
|
*
|
|
* var options = {};
|
|
* // opts.defaultBase - Id of the baselayer to enable by default.
|
|
* // opts.delayed - Don't bind the map to the dom until display() is
|
|
* // called.
|
|
* // opts.elt - DOM Node to place map in
|
|
* // opts.onhover - Event handler to run on feature hover (hightlight)
|
|
* // opts.layers - An Array of OpenLayers.Layer objects
|
|
* // opts.mapClick - Callback to handle a click on the map
|
|
* // opts.markerBackground - Custom marker background image to use by default.
|
|
* // opts.markerDragEnd - Callback to handle when a marker is dragged.
|
|
* // opts.markerImage - Custom marker image to use by default.
|
|
* // opts.onClick - Callback for handling click events on features.
|
|
* // opts.onHover - Callback for handling hover events on features.
|
|
* // opts.panzoom - Use the larger PanZoomBar control. If false, will
|
|
* // use the smaller ZoomPanel control.
|
|
* // - Callback
|
|
* // opts.useMarkerLayer - Add a vector layer to be used to place markers.
|
|
* // opts.hide - Don't show markerlayer in LayerSwitcher
|
|
* // opts.onBaseLayerChange - Callback fired when baselayer is changed.
|
|
* // opts.zoomworldicon - Show the worldzoomicon on the PanZoomBar control
|
|
* // that resets/centers map.
|
|
* var map = new HordeMap.OpenLayers(options);
|
|
*
|
|
*/
|
|
HordeMap.Map.Horde = Class.create({
|
|
|
|
map: null,
|
|
markerLayer: null,
|
|
_proj: null,
|
|
_layerSwitcher: null,
|
|
|
|
initialize: function(opts)
|
|
{
|
|
// @TODO: BC Break
|
|
if (HordeMap.conf.markerImage) {
|
|
opts.markerImage = HordeMap.conf.markerImage;
|
|
opts.markerBackground = HordeMap.conf.markerBackground;
|
|
}
|
|
|
|
// defaults
|
|
var o = {
|
|
useMarkerLayer: false,
|
|
draggableFeatures: true,
|
|
showLayerSwitcher: true,
|
|
markerLayerTitle: 'Markers',
|
|
delayed: false,
|
|
panzoom: true,
|
|
zoomworldicon: false,
|
|
layers: [],
|
|
onHover: false,
|
|
onClick: false,
|
|
hide: true,
|
|
onBaseLayerChange: false,
|
|
defaultBaseLayer: false,
|
|
// default stylemap
|
|
styleMap: new OpenLayers.StyleMap({
|
|
'default': {
|
|
externalGraphic: opts.markerImage,
|
|
backgroundGraphic: opts.markerBackground,
|
|
backgroundXOffset: 0,
|
|
backgroundYOffset: -7,
|
|
backgroundGraphicZIndex: 10,
|
|
pointRadius: (opts.pointRadius) ? opts.pointRadius : 10
|
|
}
|
|
})
|
|
};
|
|
this.opts = Object.extend(o, opts || {});
|
|
|
|
// Generate the base map object. Always use EPSG:4326 (WGS84) for display
|
|
// and EPSG:900913 (spherical mercator) for projection for compatibility
|
|
// with commercial mapping services such as Google etc...
|
|
var options = {
|
|
projection: new OpenLayers.Projection("EPSG:900913"),
|
|
displayProjection: new OpenLayers.Projection("EPSG:4326"),
|
|
units: "m",
|
|
numZoomLevels: 18,
|
|
maxResolution: 156543.0339,
|
|
maxExtent: new OpenLayers.Bounds(-20037508, -20037508, 20037508, 20037508.34),
|
|
controls: [
|
|
new OpenLayers.Control.Navigation(),
|
|
new OpenLayers.Control.Attribution()
|
|
],
|
|
styleMap: this.opts.styleMap
|
|
};
|
|
if (this.opts.panzoom) {
|
|
options.controls.push(new OpenLayers.Control.PanZoomBar({ 'zoomWorldIcon': this.opts.zoomworldicon }));
|
|
} else {
|
|
options.controls.push(new OpenLayers.Control.ZoomPanel());
|
|
}
|
|
// Set the language
|
|
OpenLayers.Lang.setCode(HordeMap.conf.language);
|
|
this.map = new OpenLayers.Map((this.opts.delayed ? null : this.opts.elt), options);
|
|
|
|
// Create the vector layer for markers if requested.
|
|
// @TODO H5 BC break - useMarkerLayer should be permap, not per page
|
|
if (this.opts.useMarkerLayer || HordeMap.conf.useMarkerLayer) {
|
|
this.markerLayer = this.createVectorLayer(this.opts);
|
|
this.opts.layers.push(this.markerLayer);
|
|
}
|
|
|
|
this.map.addLayers(this.opts.layers);
|
|
if (this.opts.showLayerSwitcher) {
|
|
this._layerSwitcher = new OpenLayers.Control.LayerSwitcher();
|
|
this.map.addControl(this._layerSwitcher);
|
|
}
|
|
|
|
// Create a click control to handle click events on the map
|
|
if (this.opts.mapClick) {
|
|
var click = new OpenLayers.Control.Click({
|
|
onClick: this._onMapClick.bind(this)
|
|
});
|
|
this.map.addControl(click);
|
|
click.activate();
|
|
}
|
|
|
|
// Used for converting between internal and display projections.
|
|
this._proj = new OpenLayers.Projection("EPSG:4326");
|
|
if (this.opts.defaultBaseLayer) {
|
|
this.map.setBaseLayer(this.map.getLayersByName(this.opts.defaultBaseLayer).pop());
|
|
}
|
|
this.map.zoomToMaxExtent();
|
|
if (this.opts.onBaseLayerChange) {
|
|
this.map.events.register('changebaselayer', null, this.opts.onBaseLayerChange);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create a vector layer and attach to map. Can pass hover and click
|
|
* handlers if this is the *only* layer to use them. Otherwise, use
|
|
* addHighlightControl/addClickControl methods after all layers are
|
|
* created.
|
|
*
|
|
* opts
|
|
* markerLayerTitle - The title to show in the LayerSwitcher
|
|
* hide - Do not show layer in LayerSwitcher
|
|
* onHover - Hover handler
|
|
* onClick - Click handler
|
|
*/
|
|
createVectorLayer: function(opts)
|
|
{
|
|
var styleMap = opts.styleMap || this.styleMap;
|
|
var layer = new OpenLayers.Layer.Vector(
|
|
opts.markerLayerTitle,
|
|
{
|
|
'styleMap': styleMap,
|
|
'rendererOptions': { zIndexing: true }
|
|
}
|
|
);
|
|
if (opts.hide) {
|
|
layer.displayInLayerSwitcher = false;
|
|
}
|
|
if (opts.draggableFeatures) {
|
|
var dragControl = new OpenLayers.Control.DragFeature(
|
|
layer,
|
|
{ onComplete: opts.markerDragEnd });
|
|
|
|
this.map.addControl(dragControl);
|
|
dragControl.activate();
|
|
}
|
|
|
|
if (opts.onHover) {
|
|
this.addHighlightControl({
|
|
'onHover': opts.onHover,
|
|
'layers': layer
|
|
});
|
|
}
|
|
|
|
if (opts.onClick) {
|
|
this.addClickControl({
|
|
'layers': layer,
|
|
'onClick': opts.onClick
|
|
});
|
|
}
|
|
|
|
return layer;
|
|
},
|
|
|
|
addHighlightControl: function(opts)
|
|
{
|
|
var selectControl = new OpenLayers.Control.SelectFeature(
|
|
opts.layers, {
|
|
hover: true,
|
|
highlightOnly: true,
|
|
renderIntent: 'temporary',
|
|
eventListeners: {
|
|
beforefeaturehighlighted: opts.onHover,
|
|
featurehighlighted: opts.onHover,
|
|
featureunhighlighted: opts.onHover
|
|
}
|
|
}
|
|
);
|
|
this.map.addControl(selectControl);
|
|
selectControl.activate();
|
|
|
|
return selectControl;
|
|
},
|
|
|
|
/**
|
|
* Add a click control to the map. HordeMap only supports one selectFeature
|
|
* control for click handlers per map, though it may contain several layers.
|
|
*
|
|
* @param object opts
|
|
* 'layers': [] All layers that should be included in the control layer.
|
|
* Note that any layers on top of layers that should handle
|
|
* clicks *must* be included in the array.
|
|
* This is an OL requirement.
|
|
* 'active': [] Layers that should actually respond to the click request.
|
|
*/
|
|
addClickControl: function(opts)
|
|
{
|
|
var clickControl = new OpenLayers.Control.SelectFeature(
|
|
opts.layers, {
|
|
'hover': false,
|
|
'clickout': false,
|
|
'toggle': true,
|
|
'hover': false,
|
|
'multiple': false,
|
|
'renderIntent': 'temporary'
|
|
}
|
|
);
|
|
opts.active.each(function(l) {
|
|
l.events.on({
|
|
'featureselected': opts.onClick
|
|
});
|
|
});
|
|
this.map.addControl(clickControl);
|
|
clickControl.activate();
|
|
|
|
return clickControl;
|
|
},
|
|
|
|
/**
|
|
* @param string name The feed display name.
|
|
* @param string feed_url The URL for the feed.
|
|
* @param string proxy A local proxy to get around same origin policy.
|
|
*
|
|
* @return OpenLayers.Layer With narkers added to displayed map for each
|
|
* georss entry.
|
|
*/
|
|
addGeoRssLayer: function(name, feed_url, proxy)
|
|
{
|
|
var style = new OpenLayers.Style({ 'pointRadius': 20, 'externalGraphic': '${thumbnail}' });
|
|
var layer = new OpenLayers.Layer.GML(name, feed_url, {
|
|
projection: new OpenLayers.Projection('EPSG:4326'),
|
|
format: OpenLayers.Format.GeoRSS,
|
|
formatOptions: {
|
|
createFeatureFromItem: function(item) {
|
|
var feature = OpenLayers.Format.GeoRSS.prototype
|
|
.createFeatureFromItem.apply(this, arguments);
|
|
feature.attributes.thumbnail =
|
|
this.getElementsByTagNameNS(
|
|
item, '*', 'thumbnail')[0].getAttribute('url');
|
|
return feature;
|
|
}
|
|
},
|
|
styleMap: new OpenLayers.StyleMap({
|
|
'default': style
|
|
})
|
|
});
|
|
this.map.addLayer(layer);
|
|
return layer;
|
|
},
|
|
|
|
removeGeoRssLayer: function(layer)
|
|
{
|
|
this.map.removeLayer(layer);
|
|
},
|
|
|
|
getZoom: function()
|
|
{
|
|
return this.map.getZoom();
|
|
},
|
|
|
|
display: function(n)
|
|
{
|
|
if (Object.isUndefined(this.map)) {
|
|
return;
|
|
}
|
|
if (!n) {
|
|
n = this.opts.elt;
|
|
}
|
|
this.map.render(n);
|
|
},
|
|
|
|
destroy: function()
|
|
{
|
|
this.map.destroy();
|
|
},
|
|
|
|
setCenter: function(p, z)
|
|
{
|
|
var ll = new OpenLayers.LonLat(p.lon, p.lat);
|
|
ll.transform(this._proj, this.map.getProjectionObject());
|
|
this.map.setCenter(ll, z);
|
|
},
|
|
|
|
zoomTo: function(z)
|
|
{
|
|
this.map.zoomTo(z);
|
|
},
|
|
|
|
/**
|
|
* Adds a simple marker to the map. Will use the markerImage property
|
|
* optionally passed into the map options. To add a feature with varying
|
|
* markerImage, pass a stylecallback method that returns a suitable style
|
|
* object.
|
|
*
|
|
* @param lonlat p { 'lon': x, 'lat': y }
|
|
* @para object opts Options
|
|
* 'styleCallback': callback to provide a custom styleobject for marker
|
|
* 'layer': use this layer instead of this.markerLayer to place marker
|
|
*/
|
|
addMarker: function(p, opts)
|
|
{
|
|
opts = Object.extend({ 'styleCallback': Prototype.K }, opts);
|
|
var ll = new OpenLayers.Geometry.Point(p.lon, p.lat);
|
|
ll.transform(this._proj, this.map.getProjectionObject());
|
|
s = opts.styleCallback(this.markerLayer.style);
|
|
var m = new OpenLayers.Feature.Vector(ll);
|
|
if (opts.layer) {
|
|
opts.layer.addFeatures([m]);
|
|
} else {
|
|
this.markerLayer.addFeatures([m]);
|
|
}
|
|
return m;
|
|
},
|
|
|
|
removeMarker: function(m, opts)
|
|
{
|
|
opts = opts || {};
|
|
if (opts.layer) {
|
|
opts.layer.destroyFeatures([m]);
|
|
} else {
|
|
this.markerLayer.destroyFeatures([m]);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Move a marker to new location.
|
|
*
|
|
* @param object m An ol vector feature object representing the marker.
|
|
* @param object ll {lat: lon:}
|
|
*
|
|
* @return void
|
|
*/
|
|
moveMarker: function(m, ll)
|
|
{
|
|
var point = new OpenLayers.LonLat(ll.lon, ll.lat);
|
|
point.transform(this._proj, this.map.getProjectionObject());
|
|
m.move(point);
|
|
},
|
|
|
|
/**
|
|
* Zoom map to the best fit while containing all markers
|
|
*
|
|
*/
|
|
zoomToFit: function(layer)
|
|
{
|
|
if (!layer) {
|
|
layer = this.markerLayer;
|
|
}
|
|
if (layer.getDataExtent()) {
|
|
this.map.zoomToExtent(layer.getDataExtent());
|
|
}
|
|
},
|
|
|
|
getMap: function()
|
|
{
|
|
return this.map;
|
|
},
|
|
|
|
updateMapSize: function()
|
|
{
|
|
this.map.updateSize();
|
|
this.map.calculateBounds();
|
|
},
|
|
|
|
getMapNodeId: function()
|
|
{
|
|
return this.opts.elt;
|
|
},
|
|
|
|
_onFeatureDragEnd: function(feature)
|
|
{
|
|
if (this.opts.markerDragEnd) {
|
|
return this.opts.markerDragEnd(feature);
|
|
}
|
|
},
|
|
|
|
_onMapClick: function(e)
|
|
{
|
|
// get*Px functions always return units in the layer's projection
|
|
var lonlat = this.map.getLonLatFromViewPortPx(e.xy);
|
|
lonlat.transform(this.map.getProjectionObject(), this._proj);
|
|
if (this.opts.mapClick) { this.opts.mapClick({ lonlat: lonlat }); }
|
|
}
|
|
|
|
});
|
|
|
|
// Extension to OpenLayers to allow better abstraction:
|
|
OpenLayers.Feature.Vector.prototype.getLonLat = function() {
|
|
var ll = new OpenLayers.LonLat(this.geometry.x, this.geometry.y);
|
|
ll.transform(new OpenLayers.Projection('EPSG:900913'), new OpenLayers.Projection('EPSG:4326'));
|
|
return ll;
|
|
};
|
|
|
|
// Custom OL click handler - doesn't propagate a click event when performing
|
|
// a double click
|
|
OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, {
|
|
defaultHandlerOptions: {
|
|
'single': true,
|
|
'double': false,
|
|
'pixelTolerance': 0,
|
|
'stopSingle': false,
|
|
'stopDouble': false
|
|
},
|
|
|
|
initialize: function(options) {
|
|
this.handlerOptions = OpenLayers.Util.extend({}, this.defaultHandlerOptions);
|
|
OpenLayers.Control.prototype.initialize.apply(this, arguments);
|
|
this.handler = new OpenLayers.Handler.Click(
|
|
this, { 'click': options.onClick }, this.handlerOptions);
|
|
}
|
|
});
|
|
|
|
|
|
HordeMap.Geocoder.Horde = Class.create({});
|