1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
|
//>>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){
}
});
});
|