Map.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. define("dojox/geo/charting/Map", ["dojo/_base/lang","dojo/_base/array","dojo/_base/declare","dojo/_base/html","dojo/dom",
  2. "dojo/dom-geometry","dojo/dom-class", "dojo/_base/xhr","dojo/_base/connect","dojo/_base/window", "dojox/gfx",
  3. "dojox/geo/charting/_base","dojox/geo/charting/Feature","dojox/geo/charting/_Marker","dojo/number","dojo/_base/sniff"],
  4. function(lang, arr, declare, html, dom, domGeom, domClass, xhr, connect, win, gfx, base,
  5. Feature, Marker, number, has) {
  6. return declare("dojox.geo.charting.Map", null, {
  7. // summary:
  8. // Map widget interacted with charting.
  9. // description:
  10. // Support rendering Americas, AsiaPacific, ContinentalEurope, EuropeMiddleEastAfrica,
  11. // USStates, WorldCountries, and WorldCountriesMercator by default.
  12. // example:
  13. // | var usaMap = new dojox.geo.charting.Map(srcNode, "dojotoolkit/dojox/geo/charting/resources/data/USStates.json");
  14. // | <div id="map" style="width:600px;height:400px;"></div>
  15. // defaultColor: String
  16. // Default map feature color, e.g: "#B7B7B7"
  17. defaultColor:"#B7B7B7",
  18. // highlightColor: String
  19. // Map feature color when mouse over it, e.g: "#"
  20. highlightColor:"#D5D5D5",
  21. // series: Array
  22. // stack to data range, e.g: [{name:'label 1', min:20, max:70, color:'#DDDDDD'},{...},...]
  23. series:[],
  24. dataBindingAttribute:null,
  25. dataBindingValueFunction:null,
  26. dataStore:null,
  27. showTooltips: true,
  28. enableFeatureZoom: true,
  29. colorAnimationDuration:0,
  30. _idAttributes:null,
  31. _onSetListener:null,
  32. _onNewListener:null,
  33. _onDeleteListener:null,
  34. constructor: function(/*HTML Node*/container, /*String or Json object*/shapeData){
  35. // container:
  36. // map container html node/id
  37. // shapeData:
  38. // map shape data json object, or url to json file
  39. html.style(container, "display", "block");
  40. this.container = container;
  41. var containerBounds = this._getContainerBounds();
  42. // get map container coords
  43. this.surface = gfx.createSurface(container, containerBounds.w, containerBounds.h);
  44. this._createZoomingCursor();
  45. // add transparent background for event capture
  46. this.mapBackground = this.surface.createRect({x: 0, y: 0, width: containerBounds.w, height: containerBounds.w}).setFill("rgba(0,0,0,0)");
  47. this.mapObj = this.surface.createGroup();
  48. this.mapObj.features = {};
  49. if (typeof shapeData == "object") {
  50. this._init(shapeData);
  51. } else {
  52. // load map shape file
  53. if (typeof shapeData == "string" && shapeData.length > 0) {
  54. xhr.get({
  55. url: shapeData,
  56. handleAs: "json",
  57. sync: true,
  58. load: lang.hitch(this, "_init")
  59. });
  60. }
  61. }
  62. },
  63. _getContainerBounds: function() {
  64. // summary:
  65. // returns the bounds {x:, y:, w: ,h:} of the DOM node container in absolute coordinates
  66. // tags:
  67. // private
  68. var position = domGeom.position(this.container,true);
  69. var marginBox = domGeom.getMarginBox(this.container);
  70. // use contentBox for correct width and height - surface spans outside border otherwise
  71. var contentBox = domGeom.getContentBox(this.container);
  72. this._storedContainerBounds = {
  73. x: position.x,
  74. y: position.y,
  75. w: contentBox.w || 100,
  76. h: contentBox.h || 100
  77. };
  78. return this._storedContainerBounds;
  79. },
  80. resize: function(/**boolean**/ adjustMapCenter/**boolean**/,adjustMapScale,/**boolean**/ animate) {
  81. // summary:
  82. // resize the underlying GFX surface to accommodate to parent DOM Node size change
  83. // adjustMapCenter: boolean
  84. // keeps the center of the map when resizing the surface
  85. // adjustMapScale: boolean
  86. // adjusts the map scale to keep the visible portion of the map as much as possible
  87. var oldBounds = this._storedContainerBounds;
  88. var newBounds = this._getContainerBounds();
  89. if ((oldBounds.w == newBounds.w) && (oldBounds.h == newBounds.h)) {
  90. return;
  91. }
  92. // set surface dimensions, and background
  93. this.mapBackground.setShape({width:newBounds.w, height:newBounds.h});
  94. this.surface.setDimensions(newBounds.w,newBounds.h);
  95. this.mapObj.marker.hide();
  96. this.mapObj.marker._needTooltipRefresh = true;
  97. if (adjustMapCenter) {
  98. var mapScale = this.getMapScale();
  99. var newScale = mapScale;
  100. if (adjustMapScale) {
  101. var bbox = this.mapObj.boundBox;
  102. var widthFactor = newBounds.w / oldBounds.w;
  103. var heightFactor = newBounds.h / oldBounds.h;
  104. newScale = mapScale * Math.sqrt(widthFactor * heightFactor);
  105. }
  106. // current map center
  107. var invariantMapPoint = this.screenCoordsToMapCoords(oldBounds.w/2,oldBounds.h/2);
  108. // apply new parameters
  109. this.setMapCenterAndScale(invariantMapPoint.x,invariantMapPoint.y,newScale,animate);
  110. }
  111. },
  112. _isMobileDevice: function() {
  113. // summary:
  114. // tests whether the application is running on a mobile device (android or iOS)
  115. // tags:
  116. // private
  117. return (has("safari")
  118. && (navigator.userAgent.indexOf("iPhone") > -1 ||
  119. navigator.userAgent.indexOf("iPod") > -1 ||
  120. navigator.userAgent.indexOf("iPad") > -1
  121. )) || (navigator.userAgent.toLowerCase().indexOf("android") > -1);
  122. },
  123. setMarkerData: function(/*String*/ markerFile){
  124. // summary:
  125. // import markers from outside file, associate with map feature by feature id
  126. // which identified in map shape file, e.g: "NY":"New York"
  127. // markerFile:
  128. // outside marker data url, handled as json style.
  129. // data format: {"NY":"New York",.....}
  130. xhr.get({
  131. url: markerFile,
  132. handleAs: "json",
  133. handle: lang.hitch(this, "_appendMarker")
  134. });
  135. },
  136. setDataBindingAttribute: function(/*String*/prop) {
  137. // summary:
  138. // sets the property name of the dataStore items to use as value (see Feature.setValue function)
  139. // prop:
  140. // the property
  141. this.dataBindingAttribute = prop;
  142. // refresh data
  143. if (this.dataStore) {
  144. this._queryDataStore();
  145. }
  146. },
  147. setDataBindingValueFunction: function(/* function */valueFunction) {
  148. // summary:
  149. // sets the function that extracts values from dataStore items,to use as Feature values (see Feature.setValue function)
  150. // prop:
  151. // the function
  152. this.dataBindingValueFunction = valueFunction;
  153. // refresh data
  154. if (this.dataStore) {
  155. this._queryDataStore();
  156. }
  157. },
  158. _queryDataStore: function() {
  159. if (!this.dataBindingAttribute || (this.dataBindingAttribute.length == 0))
  160. return;
  161. var mapInstance = this;
  162. this.dataStore.fetch({
  163. scope: this,
  164. onComplete: function(items){
  165. this._idAttributes = mapInstance.dataStore.getIdentityAttributes({});
  166. arr.forEach(items, function(item) {
  167. var id = mapInstance.dataStore.getValue(item, this._idAttributes[0]);
  168. if(mapInstance.mapObj.features[id]){
  169. var val = null;
  170. var itemVal = mapInstance.dataStore.getValue(item, mapInstance.dataBindingAttribute);
  171. if (itemVal) {
  172. if (this.dataBindingValueFunction) {
  173. val = this.dataBindingValueFunction(itemVal);
  174. } else {
  175. if (isNaN(val)) {
  176. // regular parse
  177. val=number.parse(itemVal);
  178. } else {
  179. val = itemVal;
  180. }
  181. }
  182. }
  183. if (val)
  184. mapInstance.mapObj.features[id].setValue(val);
  185. }
  186. },this);
  187. }
  188. });
  189. },
  190. _onSet:function(item,attribute,oldValue,newValue){
  191. // look for matching feature
  192. var id = this.dataStore.getValue(item, this._idAttributes[0]);
  193. var feature = this.mapObj.features[id];
  194. if (feature && (attribute == this.dataBindingAttribute)) {
  195. if (newValue)
  196. feature.setValue(newValue);
  197. else
  198. feature.unsetValue();
  199. }
  200. },
  201. _onNew:function(newItem, parentItem){
  202. var id = this.dataStore.getValue(item, this._idAttributes[0]);
  203. var feature = this.mapObj.features[id];
  204. if (feature && (attribute == this.dataBindingAttribute)) {
  205. feature.setValue(newValue);
  206. }
  207. },
  208. _onDelete:function(item){
  209. var id = item[this._idAttributes[0]];
  210. var feature = this.mapObj.features[id];
  211. if (feature) {
  212. feature.unsetValue();
  213. }
  214. },
  215. setDataStore: function(/*ItemFileReadStore*/ dataStore, /*String*/ dataBindingProp){
  216. // summary:
  217. // populate data for each map feature from fetched data store
  218. // dataStore:
  219. // the dataStore to fetch the information from
  220. // dataBindingProp:
  221. // sets the property name of the dataStore items to use as value
  222. if (this.dataStore != dataStore) {
  223. // disconnect previous listener if any
  224. if (this._onSetListener) {
  225. connect.disconnect(this._onSetListener);
  226. connect.disconnect(this._onNewListener);
  227. connect.disconnect(this._onDeleteListener);
  228. }
  229. // set new dataStore
  230. this.dataStore = dataStore;
  231. // install listener on new dataStore
  232. if (dataStore) {
  233. _onSetListener = connect.connect(this.dataStore,"onSet",this,this._onSet);
  234. _onNewListener = connect.connect(this.dataStore,"onNew",this,this._onNew);
  235. _onDeleteListener = connect.connect(this.dataStore,"onDelete",this,this._onDelete);
  236. }
  237. }
  238. if (dataBindingProp)
  239. this.setDataBindingAttribute(dataBindingProp);
  240. },
  241. addSeries: function(/*url or Json Object*/ series){
  242. // summary:
  243. // sets ranges of data values (associated with label, color) to style map data values
  244. // series:
  245. // array of range objects such as : [{name:'label 1', min:20, max:70, color:'#DDDDDD'},{...},...]
  246. if (typeof series == "object") {
  247. this._addSeriesImpl(series);
  248. } else {
  249. // load series file
  250. if (typeof series == "string" && series.length > 0) {
  251. xhr.get({
  252. url: series,
  253. handleAs: "json",
  254. sync: true,
  255. load: lang.hitch(this, function(content){
  256. this._addSeriesImpl(content.series);
  257. })
  258. });
  259. }
  260. }
  261. },
  262. _addSeriesImpl: function(/*Json object*/series) {
  263. this.series = series;
  264. // refresh color scheme
  265. for (var item in this.mapObj.features) {
  266. var feature = this.mapObj.features[item];
  267. feature.setValue(feature.value);
  268. }
  269. },
  270. fitToMapArea: function(/*bbox: {x,y,w,h}*/mapArea,pixelMargin,animate,/* callback function */onAnimationEnd){
  271. // summary:
  272. // set this component's transformation so that the specified area fits in the component (centered)
  273. // mapArea:
  274. // the map area that needs to fill the component
  275. // pixelMargin: int
  276. // a margin (in pixels) from the borders of the Map component.
  277. // animate: boolean
  278. // true if the transform change should be animated
  279. // onAnimationEnd: function
  280. // a callback function to be executed when the animation completes (if animate set to true).
  281. if(!pixelMargin){
  282. pixelMargin = 0;
  283. }
  284. var width = mapArea.w,
  285. height = mapArea.h,
  286. containerBounds = this._getContainerBounds(),
  287. scale = Math.min((containerBounds.w - 2 * pixelMargin) / width,
  288. (containerBounds.h - 2 * pixelMargin) / height);
  289. this.setMapCenterAndScale(mapArea.x + mapArea.w / 2,mapArea.y + mapArea.h / 2,scale,animate,onAnimationEnd);
  290. },
  291. fitToMapContents: function(pixelMargin,animate,/* callback function */onAnimationEnd){
  292. // summary:
  293. // set this component's transformation so that the whole map data fits in the component (centered)
  294. // pixelMargin: int
  295. // a margin (in pixels) from the borders of the Map component.
  296. // animate: boolean
  297. // true if the transform change should be animated
  298. // onAnimationEnd: function
  299. // a callback function to be executed when the animation completes (if animate set to true).
  300. //transform map to fit container
  301. var bbox = this.mapObj.boundBox;
  302. this.fitToMapArea(bbox,pixelMargin,animate,onAnimationEnd);
  303. },
  304. setMapCenter: function(centerX,centerY,animate,/* callback function */onAnimationEnd) {
  305. // summary:
  306. // set this component's transformation so that the map is centered on the specified map coordinates
  307. // centerX: float
  308. // the X coordinate (in map coordinates) of the new center
  309. // centerY: float
  310. // the Y coordinate (in map coordinates) of the new center
  311. // animate: boolean
  312. // true if the transform change should be animated
  313. // onAnimationEnd: function
  314. // a callback function to be executed when the animation completes (if animate set to true).
  315. // call setMapCenterAndScale with current map scale
  316. var currentScale = this.getMapScale();
  317. this.setMapCenterAndScale(centerX,centerY,currentScale,animate,onAnimationEnd);
  318. },
  319. _createAnimation: function(onShape,fromTransform,toTransform,/* callback function */onAnimationEnd) {
  320. // summary:
  321. // creates a transform animation object (between two transforms) used internally
  322. // fromTransform: dojox.gfx.matrix.Matrix2D
  323. // the start transformation (when animation begins)
  324. // toTransform: dojox.gfx.matrix.Matrix2D
  325. // the end transormation (when animation ends)
  326. // onAnimationEnd: function
  327. // callback function to be executed when the animation completes.
  328. var fromDx = fromTransform.dx?fromTransform.dx:0;
  329. var fromDy = fromTransform.dy?fromTransform.dy:0;
  330. var toDx = toTransform.dx?toTransform.dx:0;
  331. var toDy = toTransform.dy?toTransform.dy:0;
  332. var fromScale = fromTransform.xx?fromTransform.xx:1.0;
  333. var toScale = toTransform.xx?toTransform.xx:1.0;
  334. var anim = gfx.fx.animateTransform({
  335. duration: 1000,
  336. shape: onShape,
  337. transform: [{
  338. name: "translate",
  339. start: [fromDx,fromDy],
  340. end: [toDx,toDy]
  341. },
  342. {
  343. name: "scale",
  344. start: [fromScale],
  345. end: [toScale]
  346. }
  347. ]
  348. });
  349. //install callback
  350. if (onAnimationEnd) {
  351. var listener = connect.connect(anim,"onEnd",this,function(event){
  352. onAnimationEnd(event);
  353. connect.disconnect(listener);
  354. });
  355. }
  356. return anim;
  357. },
  358. setMapCenterAndScale: function(centerX,centerY,scale, animate,/* callback function */onAnimationEnd) {
  359. // summary:
  360. // set this component's transformation so that the map is centered on the specified map coordinates
  361. // and scaled to the specified scale.
  362. // centerX: float
  363. // the X coordinate (in map coordinates) of the new center
  364. // centerY: float
  365. // the Y coordinate (in map coordinates) of the new center
  366. // scale: float
  367. // the scale of the map
  368. // animate: boolean
  369. // true if the transform change should be animated
  370. // onAnimationEnd: function
  371. // a callback function to be executed when the animation completes (if animate set to true).
  372. // compute matrix parameters
  373. var bbox = this.mapObj.boundBox;
  374. var containerBounds = this._getContainerBounds();
  375. var offsetX = containerBounds.w/2 - scale * (centerX - bbox.x);
  376. var offsetY = containerBounds.h/2 - scale * (centerY - bbox.y);
  377. var newTransform = new gfx.matrix.Matrix2D({xx: scale, yy: scale, dx:offsetX, dy:offsetY});
  378. var currentTransform = this.mapObj.getTransform();
  379. // can animate only if specified AND curentTransform exists
  380. if (!animate || !currentTransform) {
  381. this.mapObj.setTransform(newTransform);
  382. } else {
  383. var anim = this._createAnimation(this.mapObj,currentTransform,newTransform,onAnimationEnd);
  384. anim.play();
  385. }
  386. },
  387. getMapCenter: function() {
  388. // summary:
  389. // returns the map coordinates of the center of this Map component.
  390. // returns: {x:,y:}
  391. // the center in map coordinates
  392. var containerBounds = this._getContainerBounds();
  393. return this.screenCoordsToMapCoords(containerBounds.w/2,containerBounds.h/2);
  394. },
  395. setMapScale: function(scale,animate,/* callback function */onAnimationEnd) {
  396. // summary:
  397. // set this component's transformation so that the map is scaled to the specified scale.
  398. // animate: boolean
  399. // true if the transform change should be animated
  400. // onAnimationEnd: function
  401. // a callback function to be executed when the animation completes (if animate set to true).
  402. // default invariant is map center
  403. var containerBounds = this._getContainerBounds();
  404. var invariantMapPoint = this.screenCoordsToMapCoords(containerBounds.w/2,containerBounds.h/2);
  405. this.setMapScaleAt(scale,invariantMapPoint.x,invariantMapPoint.y,animate,onAnimationEnd);
  406. },
  407. setMapScaleAt: function(scale,fixedMapX,fixedMapY,animate,/* callback function */onAnimationEnd) {
  408. // summary:
  409. // set this component's transformation so that the map is scaled to the specified scale, and the specified
  410. // point (in map coordinates) stays fixed on this Map component
  411. // fixedMapX: float
  412. // the X coordinate (in map coordinates) of the fixed screen point
  413. // fixedMapY: float
  414. // the Y coordinate (in map coordinates) of the fixed screen point
  415. // animate: boolean
  416. // true if the transform change should be animated
  417. // onAnimationEnd: function
  418. // a callback function to be executed when the animation completes (if animate set to true).
  419. var invariantMapPoint = null;
  420. var invariantScreenPoint = null;
  421. invariantMapPoint = {x: fixedMapX, y: fixedMapY};
  422. invariantScreenPoint = this.mapCoordsToScreenCoords(invariantMapPoint.x,invariantMapPoint.y);
  423. // compute matrix parameters
  424. var bbox = this.mapObj.boundBox;
  425. var offsetX = invariantScreenPoint.x - scale * (invariantMapPoint.x - bbox.x);
  426. var offsetY = invariantScreenPoint.y - scale * (invariantMapPoint.y - bbox.y);
  427. var newTransform = new gfx.matrix.Matrix2D({xx: scale, yy: scale, dx:offsetX, dy:offsetY});
  428. var currentTransform = this.mapObj.getTransform();
  429. // can animate only if specified AND curentTransform exists
  430. if (!animate || !currentTransform) {
  431. this.mapObj.setTransform(newTransform);
  432. } else {
  433. var anim = this._createAnimation(this.mapObj,currentTransform,newTransform,onAnimationEnd);
  434. anim.play();
  435. }
  436. },
  437. getMapScale: function() {
  438. // summary:
  439. // returns the scale of this Map component.
  440. // returns: float
  441. // the scale
  442. var mat = this.mapObj.getTransform();
  443. var scale = mat?mat.xx:1.0;
  444. return scale;
  445. },
  446. mapCoordsToScreenCoords: function(mapX,mapY) {
  447. // summary:
  448. // converts map coordinates to screen coordinates given the current transform of this Map component
  449. // returns: {x:,y:}
  450. // the screen coordinates correspondig to the specified map coordinates.
  451. var matrix = this.mapObj.getTransform();
  452. var screenPoint = gfx.matrix.multiplyPoint(matrix, mapX, mapY);
  453. return screenPoint;
  454. },
  455. screenCoordsToMapCoords: function(screenX, screenY) {
  456. // summary:
  457. // converts screen coordinates to map coordinates given the current transform of this Map component
  458. // returns: {x:,y:}
  459. // the map coordinates corresponding to the specified screen coordinates.
  460. var invMatrix = gfx.matrix.invert(this.mapObj.getTransform());
  461. var mapPoint = gfx.matrix.multiplyPoint(invMatrix, screenX, screenY);
  462. return mapPoint;
  463. },
  464. deselectAll: function(){
  465. // summary:
  466. // deselect all features of map
  467. for(var name in this.mapObj.features){
  468. this.mapObj.features[name].select(false);
  469. }
  470. this.selectedFeature = null;
  471. this.focused = false;
  472. },
  473. _init: function(shapeData){
  474. // summary:
  475. // inits this Map component.
  476. //transform map to fit container
  477. this.mapObj.boundBox = {x: shapeData.layerExtent[0],
  478. y: shapeData.layerExtent[1],
  479. w: (shapeData.layerExtent[2] - shapeData.layerExtent[0]),
  480. h: shapeData.layerExtent[3] - shapeData.layerExtent[1]};
  481. this.fitToMapContents(3);
  482. // if there are "features", then implement them now.
  483. arr.forEach(shapeData.featureNames, function(item){
  484. var featureShape = shapeData.features[item];
  485. featureShape.bbox.x = featureShape.bbox[0];
  486. featureShape.bbox.y = featureShape.bbox[1];
  487. featureShape.bbox.w = featureShape.bbox[2];
  488. featureShape.bbox.h = featureShape.bbox[3];
  489. var feature = new Feature(this, item, featureShape);
  490. feature.init();
  491. this.mapObj.features[item] = feature;
  492. }, this);
  493. // set up a marker.
  494. this.mapObj.marker = new Marker({}, this);
  495. },
  496. _appendMarker: function(markerData){
  497. this.mapObj.marker = new Marker(markerData, this);
  498. },
  499. _createZoomingCursor: function(){
  500. if(!dom.byId("mapZoomCursor")){
  501. var mapZoomCursor = win.doc.createElement("div");
  502. html.attr(mapZoomCursor,"id","mapZoomCursor");
  503. domClass.add(mapZoomCursor,"mapZoomIn");
  504. html.style(mapZoomCursor,"display","none");
  505. win.body().appendChild(mapZoomCursor);
  506. }
  507. },
  508. onFeatureClick: function(feature){
  509. },
  510. onFeatureOver: function(feature){
  511. },
  512. onZoomEnd:function(feature){
  513. }
  514. });
  515. });