604 lines
20 KiB
JavaScript
604 lines
20 KiB
JavaScript
//>>built
|
|
define("dojox/geo/charting/Map", ["dojo/_base/lang","dojo/_base/array","dojo/_base/declare","dojo/_base/html","dojo/dom",
|
|
"dojo/dom-geometry","dojo/dom-class", "dojo/_base/xhr","dojo/_base/connect","dojo/_base/window", "dojox/gfx",
|
|
"dojox/geo/charting/_base","dojox/geo/charting/Feature","dojox/geo/charting/_Marker","dojo/number","dojo/_base/sniff"],
|
|
function(lang, arr, declare, html, dom, domGeom, domClass, xhr, connect, win, gfx, base,
|
|
Feature, Marker, number, has) {
|
|
|
|
return declare("dojox.geo.charting.Map", null, {
|
|
// summary:
|
|
// Map widget interacted with charting.
|
|
// description:
|
|
// Support rendering Americas, AsiaPacific, ContinentalEurope, EuropeMiddleEastAfrica,
|
|
// USStates, WorldCountries, and WorldCountriesMercator by default.
|
|
// example:
|
|
// | var usaMap = new dojox.geo.charting.Map(srcNode, "dojotoolkit/dojox/geo/charting/resources/data/USStates.json");
|
|
// | <div id="map" style="width:600px;height:400px;"></div>
|
|
|
|
// defaultColor: String
|
|
// Default map feature color, e.g: "#B7B7B7"
|
|
defaultColor:"#B7B7B7",
|
|
// highlightColor: String
|
|
// Map feature color when mouse over it, e.g: "#"
|
|
highlightColor:"#D5D5D5",
|
|
// series: Array
|
|
// stack to data range, e.g: [{name:'label 1', min:20, max:70, color:'#DDDDDD'},{...},...]
|
|
series:[],
|
|
dataBindingAttribute:null,
|
|
dataBindingValueFunction:null,
|
|
dataStore:null,
|
|
showTooltips: true,
|
|
enableFeatureZoom: true,
|
|
colorAnimationDuration:0,
|
|
_idAttributes:null,
|
|
_onSetListener:null,
|
|
_onNewListener:null,
|
|
_onDeleteListener:null,
|
|
constructor: function(/*HTML Node*/container, /*String or Json object*/shapeData){
|
|
// container:
|
|
// map container html node/id
|
|
// shapeData:
|
|
// map shape data json object, or url to json file
|
|
|
|
html.style(container, "display", "block");
|
|
|
|
this.container = container;
|
|
var containerBounds = this._getContainerBounds();
|
|
// get map container coords
|
|
this.surface = gfx.createSurface(container, containerBounds.w, containerBounds.h);
|
|
|
|
this._createZoomingCursor();
|
|
|
|
// add transparent background for event capture
|
|
this.mapBackground = this.surface.createRect({x: 0, y: 0, width: containerBounds.w, height: containerBounds.w}).setFill("rgba(0,0,0,0)");
|
|
|
|
this.mapObj = this.surface.createGroup();
|
|
this.mapObj.features = {};
|
|
|
|
if (typeof shapeData == "object") {
|
|
this._init(shapeData);
|
|
} else {
|
|
// load map shape file
|
|
if (typeof shapeData == "string" && shapeData.length > 0) {
|
|
xhr.get({
|
|
url: shapeData,
|
|
handleAs: "json",
|
|
sync: true,
|
|
load: lang.hitch(this, "_init")
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
_getContainerBounds: function() {
|
|
// summary:
|
|
// returns the bounds {x:, y:, w: ,h:} of the DOM node container in absolute coordinates
|
|
// tags:
|
|
// private
|
|
|
|
var position = domGeom.position(this.container,true);
|
|
var marginBox = domGeom.getMarginBox(this.container);
|
|
// use contentBox for correct width and height - surface spans outside border otherwise
|
|
var contentBox = domGeom.getContentBox(this.container);
|
|
this._storedContainerBounds = {
|
|
x: position.x,
|
|
y: position.y,
|
|
w: contentBox.w || 100,
|
|
h: contentBox.h || 100
|
|
};
|
|
return this._storedContainerBounds;
|
|
},
|
|
|
|
resize: function(/**boolean**/ adjustMapCenter/**boolean**/,adjustMapScale,/**boolean**/ animate) {
|
|
// summary:
|
|
// resize the underlying GFX surface to accommodate to parent DOM Node size change
|
|
// adjustMapCenter: boolean
|
|
// keeps the center of the map when resizing the surface
|
|
// adjustMapScale: boolean
|
|
// adjusts the map scale to keep the visible portion of the map as much as possible
|
|
|
|
var oldBounds = this._storedContainerBounds;
|
|
var newBounds = this._getContainerBounds();
|
|
|
|
if ((oldBounds.w == newBounds.w) && (oldBounds.h == newBounds.h)) {
|
|
return;
|
|
}
|
|
|
|
// set surface dimensions, and background
|
|
this.mapBackground.setShape({width:newBounds.w, height:newBounds.h});
|
|
this.surface.setDimensions(newBounds.w,newBounds.h);
|
|
|
|
this.mapObj.marker.hide();
|
|
this.mapObj.marker._needTooltipRefresh = true;
|
|
|
|
if (adjustMapCenter) {
|
|
|
|
var mapScale = this.getMapScale();
|
|
var newScale = mapScale;
|
|
|
|
if (adjustMapScale) {
|
|
var bbox = this.mapObj.boundBox;
|
|
var widthFactor = newBounds.w / oldBounds.w;
|
|
var heightFactor = newBounds.h / oldBounds.h;
|
|
newScale = mapScale * Math.sqrt(widthFactor * heightFactor);
|
|
}
|
|
|
|
// current map center
|
|
var invariantMapPoint = this.screenCoordsToMapCoords(oldBounds.w/2,oldBounds.h/2);
|
|
|
|
// apply new parameters
|
|
this.setMapCenterAndScale(invariantMapPoint.x,invariantMapPoint.y,newScale,animate);
|
|
}
|
|
},
|
|
|
|
_isMobileDevice: function() {
|
|
// summary:
|
|
// tests whether the application is running on a mobile device (android or iOS)
|
|
// tags:
|
|
// private
|
|
return (has("safari")
|
|
&& (navigator.userAgent.indexOf("iPhone") > -1 ||
|
|
navigator.userAgent.indexOf("iPod") > -1 ||
|
|
navigator.userAgent.indexOf("iPad") > -1
|
|
)) || (navigator.userAgent.toLowerCase().indexOf("android") > -1);
|
|
},
|
|
|
|
|
|
setMarkerData: function(/*String*/ markerFile){
|
|
// summary:
|
|
// import markers from outside file, associate with map feature by feature id
|
|
// which identified in map shape file, e.g: "NY":"New York"
|
|
// markerFile:
|
|
// outside marker data url, handled as json style.
|
|
// data format: {"NY":"New York",.....}
|
|
xhr.get({
|
|
url: markerFile,
|
|
handleAs: "json",
|
|
handle: lang.hitch(this, "_appendMarker")
|
|
});
|
|
},
|
|
|
|
setDataBindingAttribute: function(/*String*/prop) {
|
|
// summary:
|
|
// sets the property name of the dataStore items to use as value (see Feature.setValue function)
|
|
// prop:
|
|
// the property
|
|
this.dataBindingAttribute = prop;
|
|
|
|
// refresh data
|
|
if (this.dataStore) {
|
|
this._queryDataStore();
|
|
}
|
|
},
|
|
|
|
setDataBindingValueFunction: function(/* function */valueFunction) {
|
|
// summary:
|
|
// sets the function that extracts values from dataStore items,to use as Feature values (see Feature.setValue function)
|
|
// prop:
|
|
// the function
|
|
this.dataBindingValueFunction = valueFunction;
|
|
|
|
// refresh data
|
|
if (this.dataStore) {
|
|
this._queryDataStore();
|
|
}
|
|
},
|
|
|
|
|
|
|
|
_queryDataStore: function() {
|
|
if (!this.dataBindingAttribute || (this.dataBindingAttribute.length == 0))
|
|
return;
|
|
|
|
var mapInstance = this;
|
|
this.dataStore.fetch({
|
|
scope: this,
|
|
onComplete: function(items){
|
|
this._idAttributes = mapInstance.dataStore.getIdentityAttributes({});
|
|
arr.forEach(items, function(item) {
|
|
var id = mapInstance.dataStore.getValue(item, this._idAttributes[0]);
|
|
if(mapInstance.mapObj.features[id]){
|
|
var val = null;
|
|
var itemVal = mapInstance.dataStore.getValue(item, mapInstance.dataBindingAttribute);
|
|
if (itemVal) {
|
|
if (this.dataBindingValueFunction) {
|
|
val = this.dataBindingValueFunction(itemVal);
|
|
} else {
|
|
if (isNaN(val)) {
|
|
// regular parse
|
|
val=number.parse(itemVal);
|
|
} else {
|
|
val = itemVal;
|
|
}
|
|
}
|
|
}
|
|
if (val)
|
|
mapInstance.mapObj.features[id].setValue(val);
|
|
}
|
|
},this);
|
|
}
|
|
});
|
|
},
|
|
|
|
_onSet:function(item,attribute,oldValue,newValue){
|
|
// look for matching feature
|
|
var id = this.dataStore.getValue(item, this._idAttributes[0]);
|
|
var feature = this.mapObj.features[id];
|
|
if (feature && (attribute == this.dataBindingAttribute)) {
|
|
if (newValue)
|
|
feature.setValue(newValue);
|
|
else
|
|
feature.unsetValue();
|
|
}
|
|
},
|
|
|
|
_onNew:function(newItem, parentItem){
|
|
var id = this.dataStore.getValue(item, this._idAttributes[0]);
|
|
var feature = this.mapObj.features[id];
|
|
if (feature && (attribute == this.dataBindingAttribute)) {
|
|
feature.setValue(newValue);
|
|
}
|
|
},
|
|
|
|
_onDelete:function(item){
|
|
var id = item[this._idAttributes[0]];
|
|
var feature = this.mapObj.features[id];
|
|
if (feature) {
|
|
feature.unsetValue();
|
|
}
|
|
},
|
|
|
|
setDataStore: function(/*ItemFileReadStore*/ dataStore, /*String*/ dataBindingProp){
|
|
// summary:
|
|
// populate data for each map feature from fetched data store
|
|
// dataStore:
|
|
// the dataStore to fetch the information from
|
|
// dataBindingProp:
|
|
// sets the property name of the dataStore items to use as value
|
|
if (this.dataStore != dataStore) {
|
|
// disconnect previous listener if any
|
|
if (this._onSetListener) {
|
|
connect.disconnect(this._onSetListener);
|
|
connect.disconnect(this._onNewListener);
|
|
connect.disconnect(this._onDeleteListener);
|
|
}
|
|
|
|
// set new dataStore
|
|
this.dataStore = dataStore;
|
|
|
|
// install listener on new dataStore
|
|
if (dataStore) {
|
|
_onSetListener = connect.connect(this.dataStore,"onSet",this,this._onSet);
|
|
_onNewListener = connect.connect(this.dataStore,"onNew",this,this._onNew);
|
|
_onDeleteListener = connect.connect(this.dataStore,"onDelete",this,this._onDelete);
|
|
}
|
|
}
|
|
if (dataBindingProp)
|
|
this.setDataBindingAttribute(dataBindingProp);
|
|
|
|
},
|
|
|
|
|
|
|
|
addSeries: function(/*url or Json Object*/ series){
|
|
// summary:
|
|
// sets ranges of data values (associated with label, color) to style map data values
|
|
// series:
|
|
// array of range objects such as : [{name:'label 1', min:20, max:70, color:'#DDDDDD'},{...},...]
|
|
|
|
if (typeof series == "object") {
|
|
this._addSeriesImpl(series);
|
|
} else {
|
|
// load series file
|
|
if (typeof series == "string" && series.length > 0) {
|
|
xhr.get({
|
|
url: series,
|
|
handleAs: "json",
|
|
sync: true,
|
|
load: lang.hitch(this, function(content){
|
|
this._addSeriesImpl(content.series);
|
|
})
|
|
});
|
|
}
|
|
}
|
|
|
|
},
|
|
|
|
_addSeriesImpl: function(/*Json object*/series) {
|
|
|
|
this.series = series;
|
|
|
|
// refresh color scheme
|
|
for (var item in this.mapObj.features) {
|
|
var feature = this.mapObj.features[item];
|
|
feature.setValue(feature.value);
|
|
}
|
|
},
|
|
|
|
|
|
fitToMapArea: function(/*bbox: {x,y,w,h}*/mapArea,pixelMargin,animate,/* callback function */onAnimationEnd){
|
|
// summary:
|
|
// set this component's transformation so that the specified area fits in the component (centered)
|
|
// mapArea:
|
|
// the map area that needs to fill the component
|
|
// pixelMargin: int
|
|
// a margin (in pixels) from the borders of the Map component.
|
|
// animate: boolean
|
|
// true if the transform change should be animated
|
|
// onAnimationEnd: function
|
|
// a callback function to be executed when the animation completes (if animate set to true).
|
|
|
|
if(!pixelMargin){
|
|
pixelMargin = 0;
|
|
}
|
|
var width = mapArea.w,
|
|
height = mapArea.h,
|
|
containerBounds = this._getContainerBounds(),
|
|
scale = Math.min((containerBounds.w - 2 * pixelMargin) / width,
|
|
(containerBounds.h - 2 * pixelMargin) / height);
|
|
|
|
this.setMapCenterAndScale(mapArea.x + mapArea.w / 2,mapArea.y + mapArea.h / 2,scale,animate,onAnimationEnd);
|
|
},
|
|
|
|
fitToMapContents: function(pixelMargin,animate,/* callback function */onAnimationEnd){
|
|
// summary:
|
|
// set this component's transformation so that the whole map data fits in the component (centered)
|
|
// pixelMargin: int
|
|
// a margin (in pixels) from the borders of the Map component.
|
|
// animate: boolean
|
|
// true if the transform change should be animated
|
|
// onAnimationEnd: function
|
|
// a callback function to be executed when the animation completes (if animate set to true).
|
|
|
|
//transform map to fit container
|
|
var bbox = this.mapObj.boundBox;
|
|
this.fitToMapArea(bbox,pixelMargin,animate,onAnimationEnd);
|
|
},
|
|
|
|
setMapCenter: function(centerX,centerY,animate,/* callback function */onAnimationEnd) {
|
|
// summary:
|
|
// set this component's transformation so that the map is centered on the specified map coordinates
|
|
// centerX: float
|
|
// the X coordinate (in map coordinates) of the new center
|
|
// centerY: float
|
|
// the Y coordinate (in map coordinates) of the new center
|
|
// animate: boolean
|
|
// true if the transform change should be animated
|
|
// onAnimationEnd: function
|
|
// a callback function to be executed when the animation completes (if animate set to true).
|
|
|
|
// call setMapCenterAndScale with current map scale
|
|
var currentScale = this.getMapScale();
|
|
this.setMapCenterAndScale(centerX,centerY,currentScale,animate,onAnimationEnd);
|
|
|
|
},
|
|
|
|
_createAnimation: function(onShape,fromTransform,toTransform,/* callback function */onAnimationEnd) {
|
|
// summary:
|
|
// creates a transform animation object (between two transforms) used internally
|
|
// fromTransform: dojox.gfx.matrix.Matrix2D
|
|
// the start transformation (when animation begins)
|
|
// toTransform: dojox.gfx.matrix.Matrix2D
|
|
// the end transormation (when animation ends)
|
|
// onAnimationEnd: function
|
|
// callback function to be executed when the animation completes.
|
|
var fromDx = fromTransform.dx?fromTransform.dx:0;
|
|
var fromDy = fromTransform.dy?fromTransform.dy:0;
|
|
var toDx = toTransform.dx?toTransform.dx:0;
|
|
var toDy = toTransform.dy?toTransform.dy:0;
|
|
var fromScale = fromTransform.xx?fromTransform.xx:1.0;
|
|
var toScale = toTransform.xx?toTransform.xx:1.0;
|
|
|
|
var anim = gfx.fx.animateTransform({
|
|
duration: 1000,
|
|
shape: onShape,
|
|
transform: [{
|
|
name: "translate",
|
|
start: [fromDx,fromDy],
|
|
end: [toDx,toDy]
|
|
},
|
|
{
|
|
name: "scale",
|
|
start: [fromScale],
|
|
end: [toScale]
|
|
}
|
|
]
|
|
});
|
|
|
|
//install callback
|
|
if (onAnimationEnd) {
|
|
var listener = connect.connect(anim,"onEnd",this,function(event){
|
|
onAnimationEnd(event);
|
|
connect.disconnect(listener);
|
|
});
|
|
}
|
|
|
|
return anim;
|
|
},
|
|
|
|
|
|
setMapCenterAndScale: function(centerX,centerY,scale, animate,/* callback function */onAnimationEnd) {
|
|
|
|
// summary:
|
|
// set this component's transformation so that the map is centered on the specified map coordinates
|
|
// and scaled to the specified scale.
|
|
// centerX: float
|
|
// the X coordinate (in map coordinates) of the new center
|
|
// centerY: float
|
|
// the Y coordinate (in map coordinates) of the new center
|
|
// scale: float
|
|
// the scale of the map
|
|
// animate: boolean
|
|
// true if the transform change should be animated
|
|
// onAnimationEnd: function
|
|
// a callback function to be executed when the animation completes (if animate set to true).
|
|
|
|
|
|
// compute matrix parameters
|
|
var bbox = this.mapObj.boundBox;
|
|
var containerBounds = this._getContainerBounds();
|
|
var offsetX = containerBounds.w/2 - scale * (centerX - bbox.x);
|
|
var offsetY = containerBounds.h/2 - scale * (centerY - bbox.y);
|
|
var newTransform = new gfx.matrix.Matrix2D({xx: scale, yy: scale, dx:offsetX, dy:offsetY});
|
|
|
|
|
|
var currentTransform = this.mapObj.getTransform();
|
|
|
|
// can animate only if specified AND curentTransform exists
|
|
if (!animate || !currentTransform) {
|
|
this.mapObj.setTransform(newTransform);
|
|
} else {
|
|
var anim = this._createAnimation(this.mapObj,currentTransform,newTransform,onAnimationEnd);
|
|
anim.play();
|
|
}
|
|
},
|
|
|
|
getMapCenter: function() {
|
|
// summary:
|
|
// returns the map coordinates of the center of this Map component.
|
|
// returns: {x:,y:}
|
|
// the center in map coordinates
|
|
var containerBounds = this._getContainerBounds();
|
|
return this.screenCoordsToMapCoords(containerBounds.w/2,containerBounds.h/2);
|
|
},
|
|
|
|
setMapScale: function(scale,animate,/* callback function */onAnimationEnd) {
|
|
// summary:
|
|
// set this component's transformation so that the map is scaled to the specified scale.
|
|
// animate: boolean
|
|
// true if the transform change should be animated
|
|
// onAnimationEnd: function
|
|
// a callback function to be executed when the animation completes (if animate set to true).
|
|
|
|
|
|
// default invariant is map center
|
|
var containerBounds = this._getContainerBounds();
|
|
var invariantMapPoint = this.screenCoordsToMapCoords(containerBounds.w/2,containerBounds.h/2);
|
|
this.setMapScaleAt(scale,invariantMapPoint.x,invariantMapPoint.y,animate,onAnimationEnd);
|
|
},
|
|
|
|
setMapScaleAt: function(scale,fixedMapX,fixedMapY,animate,/* callback function */onAnimationEnd) {
|
|
// summary:
|
|
// set this component's transformation so that the map is scaled to the specified scale, and the specified
|
|
// point (in map coordinates) stays fixed on this Map component
|
|
// fixedMapX: float
|
|
// the X coordinate (in map coordinates) of the fixed screen point
|
|
// fixedMapY: float
|
|
// the Y coordinate (in map coordinates) of the fixed screen point
|
|
// animate: boolean
|
|
// true if the transform change should be animated
|
|
// onAnimationEnd: function
|
|
// a callback function to be executed when the animation completes (if animate set to true).
|
|
|
|
|
|
var invariantMapPoint = null;
|
|
var invariantScreenPoint = null;
|
|
|
|
invariantMapPoint = {x: fixedMapX, y: fixedMapY};
|
|
invariantScreenPoint = this.mapCoordsToScreenCoords(invariantMapPoint.x,invariantMapPoint.y);
|
|
|
|
// compute matrix parameters
|
|
var bbox = this.mapObj.boundBox;
|
|
var offsetX = invariantScreenPoint.x - scale * (invariantMapPoint.x - bbox.x);
|
|
var offsetY = invariantScreenPoint.y - scale * (invariantMapPoint.y - bbox.y);
|
|
var newTransform = new gfx.matrix.Matrix2D({xx: scale, yy: scale, dx:offsetX, dy:offsetY});
|
|
|
|
var currentTransform = this.mapObj.getTransform();
|
|
|
|
// can animate only if specified AND curentTransform exists
|
|
if (!animate || !currentTransform) {
|
|
this.mapObj.setTransform(newTransform);
|
|
} else {
|
|
var anim = this._createAnimation(this.mapObj,currentTransform,newTransform,onAnimationEnd);
|
|
anim.play();
|
|
}
|
|
},
|
|
|
|
getMapScale: function() {
|
|
// summary:
|
|
// returns the scale of this Map component.
|
|
// returns: float
|
|
// the scale
|
|
var mat = this.mapObj.getTransform();
|
|
var scale = mat?mat.xx:1.0;
|
|
return scale;
|
|
},
|
|
|
|
mapCoordsToScreenCoords: function(mapX,mapY) {
|
|
// summary:
|
|
// converts map coordinates to screen coordinates given the current transform of this Map component
|
|
// returns: {x:,y:}
|
|
// the screen coordinates correspondig to the specified map coordinates.
|
|
var matrix = this.mapObj.getTransform();
|
|
var screenPoint = gfx.matrix.multiplyPoint(matrix, mapX, mapY);
|
|
return screenPoint;
|
|
},
|
|
|
|
screenCoordsToMapCoords: function(screenX, screenY) {
|
|
// summary:
|
|
// converts screen coordinates to map coordinates given the current transform of this Map component
|
|
// returns: {x:,y:}
|
|
// the map coordinates corresponding to the specified screen coordinates.
|
|
var invMatrix = gfx.matrix.invert(this.mapObj.getTransform());
|
|
var mapPoint = gfx.matrix.multiplyPoint(invMatrix, screenX, screenY);
|
|
return mapPoint;
|
|
},
|
|
deselectAll: function(){
|
|
// summary:
|
|
// deselect all features of map
|
|
for(var name in this.mapObj.features){
|
|
this.mapObj.features[name].select(false);
|
|
}
|
|
this.selectedFeature = null;
|
|
this.focused = false;
|
|
},
|
|
|
|
_init: function(shapeData){
|
|
|
|
// summary:
|
|
// inits this Map component.
|
|
|
|
//transform map to fit container
|
|
this.mapObj.boundBox = {x: shapeData.layerExtent[0],
|
|
y: shapeData.layerExtent[1],
|
|
w: (shapeData.layerExtent[2] - shapeData.layerExtent[0]),
|
|
h: shapeData.layerExtent[3] - shapeData.layerExtent[1]};
|
|
this.fitToMapContents(3);
|
|
|
|
|
|
// if there are "features", then implement them now.
|
|
arr.forEach(shapeData.featureNames, function(item){
|
|
var featureShape = shapeData.features[item];
|
|
featureShape.bbox.x = featureShape.bbox[0];
|
|
featureShape.bbox.y = featureShape.bbox[1];
|
|
featureShape.bbox.w = featureShape.bbox[2];
|
|
featureShape.bbox.h = featureShape.bbox[3];
|
|
var feature = new Feature(this, item, featureShape);
|
|
feature.init();
|
|
this.mapObj.features[item] = feature;
|
|
}, this);
|
|
|
|
|
|
// set up a marker.
|
|
this.mapObj.marker = new Marker({}, this);
|
|
},
|
|
_appendMarker: function(markerData){
|
|
this.mapObj.marker = new Marker(markerData, this);
|
|
},
|
|
_createZoomingCursor: function(){
|
|
if(!dom.byId("mapZoomCursor")){
|
|
var mapZoomCursor = win.doc.createElement("div");
|
|
html.attr(mapZoomCursor,"id","mapZoomCursor");
|
|
domClass.add(mapZoomCursor,"mapZoomIn");
|
|
html.style(mapZoomCursor,"display","none");
|
|
win.body().appendChild(mapZoomCursor);
|
|
}
|
|
},
|
|
onFeatureClick: function(feature){
|
|
},
|
|
onFeatureOver: function(feature){
|
|
},
|
|
onZoomEnd:function(feature){
|
|
}
|
|
});
|
|
}); |