source: Dev/trunk/src/client/dojox/geo/charting/Map.js @ 483

Last change on this file since 483 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

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