source: Dev/branches/rest-dojo-ui/client/dojox/geo/charting/Map.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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