GPQAPP

Форк
0
2781 строка · 120.1 Кб
1
/*!
2
 *
3
 * Jquery Mapael - Dynamic maps jQuery plugin (based on raphael.js)
4
 * Requires jQuery, raphael.js and jquery.mousewheel
5
 *
6
 * Version: 2.2.0
7
 *
8
 * Copyright (c) 2017 Vincent Brouté (https://www.vincentbroute.fr/mapael)
9
 * Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php).
10
 *
11
 * Thanks to Indigo744
12
 *
13
 */
14
(function (factory) {
15
    if (typeof exports === 'object') {
16
        // CommonJS
17
        module.exports = factory(require('jquery'), require('raphael'), require('jquery-mousewheel'));
18
    } else if (typeof define === 'function' && define.amd) {
19
        // AMD. Register as an anonymous module.
20
        define(['jquery', 'raphael', 'mousewheel'], factory);
21
    } else {
22
        // Browser globals
23
        factory(jQuery, Raphael, jQuery.fn.mousewheel);
24
    }
25
}(function ($, Raphael, mousewheel, undefined) {
26

27
    "use strict";
28

29
    // The plugin name (used on several places)
30
    var pluginName = "mapael";
31

32
    // Version number of jQuery Mapael. See http://semver.org/ for more information.
33
    var version = "2.2.0";
34

35
    /*
36
     * Mapael constructor
37
     * Init instance vars and call init()
38
     * @param container the DOM element on which to apply the plugin
39
     * @param options the complete options to use
40
     */
41
    var Mapael = function (container, options) {
42
        var self = this;
43

44
        // the global container (DOM element object)
45
        self.container = container;
46

47
        // the global container (jQuery object)
48
        self.$container = $(container);
49

50
        // the global options
51
        self.options = self.extendDefaultOptions(options);
52

53
        // zoom TimeOut handler (used to set and clear)
54
        self.zoomTO = 0;
55

56
        // zoom center coordinate (set at touchstart)
57
        self.zoomCenterX = 0;
58
        self.zoomCenterY = 0;
59

60
        // Zoom pinch (set at touchstart and touchmove)
61
        self.previousPinchDist = 0;
62

63
        // Zoom data
64
        self.zoomData = {
65
            zoomLevel: 0,
66
            zoomX: 0,
67
            zoomY: 0,
68
            panX: 0,
69
            panY: 0
70
        };
71

72
        self.currentViewBox = {
73
            x: 0, y: 0, w: 0, h: 0
74
        };
75

76
        // Panning: tell if panning action is in progress
77
        self.panning = false;
78

79
        // Animate view box
80
        self.zoomAnimID = null; // Interval handler (used to set and clear)
81
        self.zoomAnimStartTime = null; // Animation start time
82
        self.zoomAnimCVBTarget = null; // Current ViewBox target
83

84
        // Map subcontainer jQuery object
85
        self.$map = $("." + self.options.map.cssClass, self.container);
86

87
        // Save initial HTML content (used by destroy method)
88
        self.initialMapHTMLContent = self.$map.html();
89

90
        // The tooltip jQuery object
91
        self.$tooltip = {};
92

93
        // The paper Raphael object
94
        self.paper = {};
95

96
        // The areas object list
97
        self.areas = {};
98

99
        // The plots object list
100
        self.plots = {};
101

102
        // The links object list
103
        self.links = {};
104

105
        // The legends list
106
        self.legends = {};
107

108
        // The map configuration object (taken from map file)
109
        self.mapConf = {};
110

111
        // Holds all custom event handlers
112
        self.customEventHandlers = {};
113

114
        // Let's start the initialization
115
        self.init();
116
    };
117

118
    /*
119
     * Mapael Prototype
120
     * Defines all methods and properties needed by Mapael
121
     * Each mapael object inherits their properties and methods from this prototype
122
     */
123
    Mapael.prototype = {
124

125
        /* Filtering TimeOut value in ms
126
         * Used for mouseover trigger over elements */
127
        MouseOverFilteringTO: 120,
128
        /* Filtering TimeOut value in ms
129
         * Used for afterPanning trigger when panning */
130
        panningFilteringTO: 150,
131
        /* Filtering TimeOut value in ms
132
         * Used for mouseup/touchend trigger when panning */
133
        panningEndFilteringTO: 50,
134
        /* Filtering TimeOut value in ms
135
         * Used for afterZoom trigger when zooming */
136
        zoomFilteringTO: 150,
137
        /* Filtering TimeOut value in ms
138
         * Used for when resizing window */
139
        resizeFilteringTO: 150,
140

141
        /*
142
         * Initialize the plugin
143
         * Called by the constructor
144
         */
145
        init: function () {
146
            var self = this;
147

148
            // Init check for class existence
149
            if (self.options.map.cssClass === "" || $("." + self.options.map.cssClass, self.container).length === 0) {
150
                throw new Error("The map class `" + self.options.map.cssClass + "` doesn't exists");
151
            }
152

153
            // Create the tooltip container
154
            self.$tooltip = $("<div>").addClass(self.options.map.tooltip.cssClass).css("display", "none");
155

156
            // Get the map container, empty it then append tooltip
157
            self.$map.empty().append(self.$tooltip);
158

159
            // Get the map from $.mapael or $.fn.mapael (backward compatibility)
160
            if ($[pluginName] && $[pluginName].maps && $[pluginName].maps[self.options.map.name]) {
161
                // Mapael version >= 2.x
162
                self.mapConf = $[pluginName].maps[self.options.map.name];
163
            } else if ($.fn[pluginName] && $.fn[pluginName].maps && $.fn[pluginName].maps[self.options.map.name]) {
164
                // Mapael version <= 1.x - DEPRECATED
165
                self.mapConf = $.fn[pluginName].maps[self.options.map.name];
166
                if (window.console && window.console.warn) {
167
                    window.console.warn("Extending $.fn.mapael is deprecated (map '" + self.options.map.name + "')");
168
                }
169
            } else {
170
                throw new Error("Unknown map '" + self.options.map.name + "'");
171
            }
172

173
            // Create Raphael paper
174
            self.paper = new Raphael(self.$map[0], self.mapConf.width, self.mapConf.height);
175

176
            // issue #135: Check for Raphael bug on text element boundaries
177
            if (self.isRaphaelBBoxBugPresent() === true) {
178
                self.destroy();
179
                throw new Error("Can't get boundary box for text (is your container hidden? See #135)");
180
            }
181

182
            // add plugin class name on element
183
            self.$container.addClass(pluginName);
184

185
            if (self.options.map.tooltip.css) self.$tooltip.css(self.options.map.tooltip.css);
186
            self.setViewBox(0, 0, self.mapConf.width, self.mapConf.height);
187

188
            // Handle map size
189
            if (self.options.map.width) {
190
                // NOT responsive: map has a fixed width
191
                self.paper.setSize(self.options.map.width, self.mapConf.height * (self.options.map.width / self.mapConf.width));
192
            } else {
193
                // Responsive: handle resizing of the map
194
                self.initResponsiveSize();
195
            }
196

197
            // Draw map areas
198
            $.each(self.mapConf.elems, function (id) {
199
                // Init area object
200
                self.areas[id] = {};
201
                // Set area options
202
                self.areas[id].options = self.getElemOptions(
203
                    self.options.map.defaultArea,
204
                    (self.options.areas[id] ? self.options.areas[id] : {}),
205
                    self.options.legend.area
206
                );
207
                // draw area
208
                self.areas[id].mapElem = self.paper.path(self.mapConf.elems[id]);
209
            });
210

211
            // Hook that allows to add custom processing on the map
212
            if (self.options.map.beforeInit) self.options.map.beforeInit(self.$container, self.paper, self.options);
213

214
            // Init map areas in a second loop
215
            // Allows text to be added after ALL areas and prevent them from being hidden
216
            $.each(self.mapConf.elems, function (id) {
217
                self.initElem(id, 'area', self.areas[id]);
218
            });
219

220
            // Draw links
221
            self.links = self.drawLinksCollection(self.options.links);
222

223
            // Draw plots
224
            $.each(self.options.plots, function (id) {
225
                self.plots[id] = self.drawPlot(id);
226
            });
227

228
            // Attach zoom event
229
            self.$container.on("zoom." + pluginName, function (e, zoomOptions) {
230
                self.onZoomEvent(e, zoomOptions);
231
            });
232

233
            if (self.options.map.zoom.enabled) {
234
                // Enable zoom
235
                self.initZoom(self.mapConf.width, self.mapConf.height, self.options.map.zoom);
236
            }
237

238
            // Set initial zoom
239
            if (self.options.map.zoom.init !== undefined) {
240
                if (self.options.map.zoom.init.animDuration === undefined) {
241
                    self.options.map.zoom.init.animDuration = 0;
242
                }
243
                self.$container.trigger("zoom", self.options.map.zoom.init);
244
            }
245

246
            // Create the legends for areas
247
            self.createLegends("area", self.areas, 1);
248

249
            // Create the legends for plots taking into account the scale of the map
250
            self.createLegends("plot", self.plots, self.paper.width / self.mapConf.width);
251

252
            // Attach update event
253
            self.$container.on("update." + pluginName, function (e, opt) {
254
                self.onUpdateEvent(e, opt);
255
            });
256

257
            // Attach showElementsInRange event
258
            self.$container.on("showElementsInRange." + pluginName, function (e, opt) {
259
                self.onShowElementsInRange(e, opt);
260
            });
261

262
            // Attach delegated events
263
            self.initDelegatedMapEvents();
264
            // Attach delegated custom events
265
            self.initDelegatedCustomEvents();
266

267
            // Hook that allows to add custom processing on the map
268
            if (self.options.map.afterInit) self.options.map.afterInit(self.$container, self.paper, self.areas, self.plots, self.options);
269

270
            $(self.paper.desc).append(" and Mapael " + self.version + " (https://www.vincentbroute.fr/mapael/)");
271
        },
272

273
        /*
274
         * Destroy mapael
275
         * This function effectively detach mapael from the container
276
         *   - Set the container back to the way it was before mapael instanciation
277
         *   - Remove all data associated to it (memory can then be free'ed by browser)
278
         *
279
         * This method can be call directly by user:
280
         *     $(".mapcontainer").data("mapael").destroy();
281
         *
282
         * This method is also automatically called if the user try to call mapael
283
         * on a container already containing a mapael instance
284
         */
285
        destroy: function () {
286
            var self = this;
287

288
            // Detach all event listeners attached to the container
289
            self.$container.off("." + pluginName);
290
            self.$map.off("." + pluginName);
291

292
            // Detach the global resize event handler
293
            if (self.onResizeEvent) $(window).off("resize." + pluginName, self.onResizeEvent);
294

295
            // Empty the container (this will also detach all event listeners)
296
            self.$map.empty();
297

298
            // Replace initial HTML content
299
            self.$map.html(self.initialMapHTMLContent);
300

301
            // Empty legend containers and replace initial HTML content
302
            $.each(self.legends, function(legendType) {
303
                $.each(self.legends[legendType], function(legendIndex) {
304
                    var legend = self.legends[legendType][legendIndex];
305
                    legend.container.empty();
306
                    legend.container.html(legend.initialHTMLContent);
307
                });
308
            });
309

310
            // Remove mapael class
311
            self.$container.removeClass(pluginName);
312

313
            // Remove the data
314
            self.$container.removeData(pluginName);
315

316
            // Remove all internal reference
317
            self.container = undefined;
318
            self.$container = undefined;
319
            self.options = undefined;
320
            self.paper = undefined;
321
            self.$map = undefined;
322
            self.$tooltip = undefined;
323
            self.mapConf = undefined;
324
            self.areas = undefined;
325
            self.plots = undefined;
326
            self.links = undefined;
327
            self.customEventHandlers = undefined;
328
        },
329

330
        initResponsiveSize: function () {
331
            var self = this;
332
            var resizeTO = null;
333

334
            // Function that actually handle the resizing
335
            var handleResize = function(isInit) {
336
                var containerWidth = self.$map.width();
337

338
                if (self.paper.width !== containerWidth) {
339
                    var newScale = containerWidth / self.mapConf.width;
340
                    // Set new size
341
                    self.paper.setSize(containerWidth, self.mapConf.height * newScale);
342

343
                    // Create plots legend again to take into account the new scale
344
                    // Do not do this on init (it will be done later)
345
                    if (isInit !== true && self.options.legend.redrawOnResize) {
346
                        self.createLegends("plot", self.plots, newScale);
347
                    }
348
                }
349
            };
350

351
            self.onResizeEvent = function() {
352
                // Clear any previous setTimeout (avoid too much triggering)
353
                clearTimeout(resizeTO);
354
                // setTimeout to wait for the user to finish its resizing
355
                resizeTO = setTimeout(function () {
356
                    handleResize();
357
                }, self.resizeFilteringTO);
358
            };
359

360
            // Attach resize handler
361
            $(window).on("resize." + pluginName, self.onResizeEvent);
362

363
            // Call once
364
            handleResize(true);
365
        },
366

367
        /*
368
         * Extend the user option with the default one
369
         * @param options the user options
370
         * @return new options object
371
         */
372
        extendDefaultOptions: function (options) {
373

374
            // Extend default options with user options
375
            options = $.extend(true, {}, Mapael.prototype.defaultOptions, options);
376

377
            // Extend legend default options
378
            $.each(['area', 'plot'], function (key, type) {
379
                if ($.isArray(options.legend[type])) {
380
                    for (var i = 0; i < options.legend[type].length; ++i)
381
                        options.legend[type][i] = $.extend(true, {}, Mapael.prototype.legendDefaultOptions[type], options.legend[type][i]);
382
                } else {
383
                    options.legend[type] = $.extend(true, {}, Mapael.prototype.legendDefaultOptions[type], options.legend[type]);
384
                }
385
            });
386

387
            return options;
388
        },
389

390
        /*
391
         * Init all delegated events for the whole map:
392
         *  mouseover
393
         *  mousemove
394
         *  mouseout
395
         */
396
        initDelegatedMapEvents: function() {
397
            var self = this;
398

399
            // Mapping between data-type value and the corresponding elements array
400
            // Note: legend-elem and legend-label are not in this table because
401
            //       they need a special processing
402
            var dataTypeToElementMapping = {
403
                'area'  : self.areas,
404
                'area-text' : self.areas,
405
                'plot' : self.plots,
406
                'plot-text' : self.plots,
407
                'link' : self.links,
408
                'link-text' : self.links
409
            };
410

411
            /* Attach mouseover event delegation
412
             * Note: we filter the event with a timeout to reduce the firing when the mouse moves quickly
413
             */
414
            var mapMouseOverTimeoutID;
415
            self.$container.on("mouseover." + pluginName, "[data-id]", function () {
416
                var elem = this;
417
                clearTimeout(mapMouseOverTimeoutID);
418
                mapMouseOverTimeoutID = setTimeout(function() {
419
                    var $elem = $(elem);
420
                    var id = $elem.attr('data-id');
421
                    var type = $elem.attr('data-type');
422

423
                    if (dataTypeToElementMapping[type] !== undefined) {
424
                        self.elemEnter(dataTypeToElementMapping[type][id]);
425
                    } else if (type === 'legend-elem' || type === 'legend-label') {
426
                        var legendIndex = $elem.attr('data-legend-id');
427
                        var legendType = $elem.attr('data-legend-type');
428
                        self.elemEnter(self.legends[legendType][legendIndex].elems[id]);
429
                    }
430
                }, self.MouseOverFilteringTO);
431
            });
432

433
            /* Attach mousemove event delegation
434
             * Note: timeout filtering is small to update the Tooltip position fast
435
             */
436
            var mapMouseMoveTimeoutID;
437
            self.$container.on("mousemove." + pluginName, "[data-id]", function (event) {
438
                var elem = this;
439
                clearTimeout(mapMouseMoveTimeoutID);
440
                mapMouseMoveTimeoutID = setTimeout(function() {
441
                    var $elem = $(elem);
442
                    var id = $elem.attr('data-id');
443
                    var type = $elem.attr('data-type');
444

445
                    if (dataTypeToElementMapping[type] !== undefined) {
446
                        self.elemHover(dataTypeToElementMapping[type][id], event);
447
                    } else if (type === 'legend-elem' || type === 'legend-label') {
448
                        /* Nothing to do */
449
                    }
450

451
                }, 0);
452
            });
453

454
            /* Attach mouseout event delegation
455
             * Note: we don't perform any timeout filtering to clear & reset elem ASAP
456
             * Otherwise an element may be stuck in 'hover' state (which is NOT good)
457
             */
458
            self.$container.on("mouseout." + pluginName, "[data-id]", function () {
459
                var elem = this;
460
                // Clear any
461
                clearTimeout(mapMouseOverTimeoutID);
462
                clearTimeout(mapMouseMoveTimeoutID);
463
                var $elem = $(elem);
464
                var id = $elem.attr('data-id');
465
                var type = $elem.attr('data-type');
466

467
                if (dataTypeToElementMapping[type] !== undefined) {
468
                    self.elemOut(dataTypeToElementMapping[type][id]);
469
                } else if (type === 'legend-elem' || type === 'legend-label') {
470
                    var legendIndex = $elem.attr('data-legend-id');
471
                    var legendType = $elem.attr('data-legend-type');
472
                    self.elemOut(self.legends[legendType][legendIndex].elems[id]);
473
                }
474
            });
475

476
            /* Attach click event delegation
477
             * Note: we filter the event with a timeout to avoid double click
478
             */
479
            self.$container.on("click." + pluginName, "[data-id]", function (evt, opts) {
480
                var $elem = $(this);
481
                var id = $elem.attr('data-id');
482
                var type = $elem.attr('data-type');
483

484
                if (dataTypeToElementMapping[type] !== undefined) {
485
                    self.elemClick(dataTypeToElementMapping[type][id]);
486
                } else if (type === 'legend-elem' || type === 'legend-label') {
487
                    var legendIndex = $elem.attr('data-legend-id');
488
                    var legendType = $elem.attr('data-legend-type');
489
                    self.handleClickOnLegendElem(self.legends[legendType][legendIndex].elems[id], id, legendIndex, legendType, opts);
490
                }
491
            });
492
        },
493

494
        /*
495
         * Init all delegated custom events
496
         */
497
        initDelegatedCustomEvents: function() {
498
            var self = this;
499

500
            $.each(self.customEventHandlers, function(eventName) {
501
                // Namespace the custom event
502
                // This allow to easily unbound only custom events and not regular ones
503
                var fullEventName = eventName + '.' + pluginName + ".custom";
504
                self.$container.off(fullEventName).on(fullEventName, "[data-id]", function (e) {
505
                    var $elem = $(this);
506
                    var id = $elem.attr('data-id');
507
                    var type = $elem.attr('data-type').replace('-text', '');
508

509
                    if (!self.panning &&
510
                        self.customEventHandlers[eventName][type] !== undefined &&
511
                        self.customEventHandlers[eventName][type][id] !== undefined)
512
                    {
513
                        // Get back related elem
514
                        var elem = self.customEventHandlers[eventName][type][id];
515
                        // Run callback provided by user
516
                        elem.options.eventHandlers[eventName](e, id, elem.mapElem, elem.textElem, elem.options);
517
                    }
518
                });
519
            });
520

521
        },
522

523
        /*
524
         * Init the element "elem" on the map (drawing text, setting attributes, events, tooltip, ...)
525
         *
526
         * @param id the id of the element
527
         * @param type the type of the element (area, plot, link)
528
         * @param elem object the element object (with mapElem), it will be updated
529
         */
530
        initElem: function (id, type, elem) {
531
            var self = this;
532
            var $mapElem = $(elem.mapElem.node);
533

534
            // If an HTML link exists for this element, add cursor attributes
535
            if (elem.options.href) {
536
                elem.options.attrs.cursor = "pointer";
537
                if (elem.options.text) elem.options.text.attrs.cursor = "pointer";
538
            }
539

540
            // Set SVG attributes to map element
541
            elem.mapElem.attr(elem.options.attrs);
542
            // Set DOM attributes to map element
543
            $mapElem.attr({
544
                "data-id": id,
545
                "data-type": type
546
            });
547
            if (elem.options.cssClass !== undefined) {
548
                $mapElem.addClass(elem.options.cssClass);
549
            }
550

551
            // Init the label related to the element
552
            if (elem.options.text && elem.options.text.content !== undefined) {
553
                // Set a text label in the area
554
                var textPosition = self.getTextPosition(elem.mapElem.getBBox(), elem.options.text.position, elem.options.text.margin);
555
                elem.options.text.attrs.text = elem.options.text.content;
556
                elem.options.text.attrs.x = textPosition.x;
557
                elem.options.text.attrs.y = textPosition.y;
558
                elem.options.text.attrs['text-anchor'] = textPosition.textAnchor;
559
                // Draw text
560
                elem.textElem = self.paper.text(textPosition.x, textPosition.y, elem.options.text.content);
561
                // Apply SVG attributes to text element
562
                elem.textElem.attr(elem.options.text.attrs);
563
                // Apply DOM attributes
564
                $(elem.textElem.node).attr({
565
                    "data-id": id,
566
                    "data-type": type + '-text'
567
                });
568
            }
569

570
            // Set user event handlers
571
            if (elem.options.eventHandlers) self.setEventHandlers(id, type, elem);
572

573
            // Set hover option for mapElem
574
            self.setHoverOptions(elem.mapElem, elem.options.attrs, elem.options.attrsHover);
575

576
            // Set hover option for textElem
577
            if (elem.textElem) self.setHoverOptions(elem.textElem, elem.options.text.attrs, elem.options.text.attrsHover);
578
        },
579

580
        /*
581
         * Init zoom and panning for the map
582
         * @param mapWidth
583
         * @param mapHeight
584
         * @param zoomOptions
585
         */
586
        initZoom: function (mapWidth, mapHeight, zoomOptions) {
587
            var self = this;
588
            var mousedown = false;
589
            var previousX = 0;
590
            var previousY = 0;
591
            var fnZoomButtons = {
592
                "reset": function () {
593
                    self.$container.trigger("zoom", {"level": 0});
594
                },
595
                "in": function () {
596
                    self.$container.trigger("zoom", {"level": "+1"});
597
                },
598
                "out": function () {
599
                    self.$container.trigger("zoom", {"level": -1});
600
                }
601
            };
602

603
            // init Zoom data
604
            $.extend(self.zoomData, {
605
                zoomLevel: 0,
606
                panX: 0,
607
                panY: 0
608
            });
609

610
            // init zoom buttons
611
            $.each(zoomOptions.buttons, function(type, opt) {
612
                if (fnZoomButtons[type] === undefined) throw new Error("Unknown zoom button '" + type + "'");
613
                // Create div with classes, contents and title (for tooltip)
614
                var $button = $("<div>").addClass(opt.cssClass)
615
                    .html(opt.content)
616
                    .attr("title", opt.title);
617
                // Assign click event
618
                $button.on("click." + pluginName, fnZoomButtons[type]);
619
                // Append to map
620
                self.$map.append($button);
621
            });
622

623
            // Update the zoom level of the map on mousewheel
624
            if (self.options.map.zoom.mousewheel) {
625
                self.$map.on("mousewheel." + pluginName, function (e) {
626
                    var zoomLevel = (e.deltaY > 0) ? 1 : -1;
627
                    var coord = self.mapPagePositionToXY(e.pageX, e.pageY);
628

629
                    self.$container.trigger("zoom", {
630
                        "fixedCenter": true,
631
                        "level": self.zoomData.zoomLevel + zoomLevel,
632
                        "x": coord.x,
633
                        "y": coord.y
634
                    });
635

636
                    e.preventDefault();
637
                });
638
            }
639

640
            // Update the zoom level of the map on touch pinch
641
            if (self.options.map.zoom.touch) {
642
                self.$map.on("touchstart." + pluginName, function (e) {
643
                    if (e.originalEvent.touches.length === 2) {
644
                        self.zoomCenterX = (e.originalEvent.touches[0].pageX + e.originalEvent.touches[1].pageX) / 2;
645
                        self.zoomCenterY = (e.originalEvent.touches[0].pageY + e.originalEvent.touches[1].pageY) / 2;
646
                        self.previousPinchDist = Math.sqrt(Math.pow((e.originalEvent.touches[1].pageX - e.originalEvent.touches[0].pageX), 2) + Math.pow((e.originalEvent.touches[1].pageY - e.originalEvent.touches[0].pageY), 2));
647
                    }
648
                });
649

650
                self.$map.on("touchmove." + pluginName, function (e) {
651
                    var pinchDist = 0;
652
                    var zoomLevel = 0;
653

654
                    if (e.originalEvent.touches.length === 2) {
655
                        pinchDist = Math.sqrt(Math.pow((e.originalEvent.touches[1].pageX - e.originalEvent.touches[0].pageX), 2) + Math.pow((e.originalEvent.touches[1].pageY - e.originalEvent.touches[0].pageY), 2));
656

657
                        if (Math.abs(pinchDist - self.previousPinchDist) > 15) {
658
                            var coord = self.mapPagePositionToXY(self.zoomCenterX, self.zoomCenterY);
659
                            zoomLevel = (pinchDist - self.previousPinchDist) / Math.abs(pinchDist - self.previousPinchDist);
660
                            self.$container.trigger("zoom", {
661
                                "fixedCenter": true,
662
                                "level": self.zoomData.zoomLevel + zoomLevel,
663
                                "x": coord.x,
664
                                "y": coord.y
665
                            });
666
                            self.previousPinchDist = pinchDist;
667
                        }
668
                        return false;
669
                    }
670
                });
671
            }
672

673
            // When the user drag the map, prevent to move the clicked element instead of dragging the map (behaviour seen with Firefox)
674
            self.$map.on("dragstart", function() {
675
                return false;
676
            });
677

678
            // Panning
679
            var panningMouseUpTO = null;
680
            var panningMouseMoveTO = null;
681
            $("body").on("mouseup." + pluginName + (zoomOptions.touch ? " touchend." + pluginName : ""), function () {
682
                mousedown = false;
683
                clearTimeout(panningMouseUpTO);
684
                clearTimeout(panningMouseMoveTO);
685
                panningMouseUpTO = setTimeout(function () {
686
                    self.panning = false;
687
                }, self.panningEndFilteringTO);
688
            });
689

690
            self.$map.on("mousedown." + pluginName + (zoomOptions.touch ? " touchstart." + pluginName : ""), function (e) {
691
                clearTimeout(panningMouseUpTO);
692
                clearTimeout(panningMouseMoveTO);
693
                if (e.pageX !== undefined) {
694
                    mousedown = true;
695
                    previousX = e.pageX;
696
                    previousY = e.pageY;
697
                } else {
698
                    if (e.originalEvent.touches.length === 1) {
699
                        mousedown = true;
700
                        previousX = e.originalEvent.touches[0].pageX;
701
                        previousY = e.originalEvent.touches[0].pageY;
702
                    }
703
                }
704
            }).on("mousemove." + pluginName + (zoomOptions.touch ? " touchmove." + pluginName : ""), function (e) {
705
                var currentLevel = self.zoomData.zoomLevel;
706
                var pageX = 0;
707
                var pageY = 0;
708

709
                clearTimeout(panningMouseUpTO);
710
                clearTimeout(panningMouseMoveTO);
711

712
                if (e.pageX !== undefined) {
713
                    pageX = e.pageX;
714
                    pageY = e.pageY;
715
                } else {
716
                    if (e.originalEvent.touches.length === 1) {
717
                        pageX = e.originalEvent.touches[0].pageX;
718
                        pageY = e.originalEvent.touches[0].pageY;
719
                    } else {
720
                        mousedown = false;
721
                    }
722
                }
723

724
                if (mousedown && currentLevel !== 0) {
725
                    var offsetX = (previousX - pageX) / (1 + (currentLevel * zoomOptions.step)) * (mapWidth / self.paper.width);
726
                    var offsetY = (previousY - pageY) / (1 + (currentLevel * zoomOptions.step)) * (mapHeight / self.paper.height);
727
                    var panX = Math.min(Math.max(0, self.currentViewBox.x + offsetX), (mapWidth - self.currentViewBox.w));
728
                    var panY = Math.min(Math.max(0, self.currentViewBox.y + offsetY), (mapHeight - self.currentViewBox.h));
729

730
                    if (Math.abs(offsetX) > 5 || Math.abs(offsetY) > 5) {
731
                        $.extend(self.zoomData, {
732
                            panX: panX,
733
                            panY: panY,
734
                            zoomX: panX + self.currentViewBox.w / 2,
735
                            zoomY: panY + self.currentViewBox.h / 2
736
                        });
737
                        self.setViewBox(panX, panY, self.currentViewBox.w, self.currentViewBox.h);
738

739
                        panningMouseMoveTO = setTimeout(function () {
740
                            self.$map.trigger("afterPanning", {
741
                                x1: panX,
742
                                y1: panY,
743
                                x2: (panX + self.currentViewBox.w),
744
                                y2: (panY + self.currentViewBox.h)
745
                            });
746
                        }, self.panningFilteringTO);
747

748
                        previousX = pageX;
749
                        previousY = pageY;
750
                        self.panning = true;
751
                    }
752
                    return false;
753
                }
754
            });
755
        },
756

757
        /*
758
         * Map a mouse position to a map position
759
         *      Transformation principle:
760
         *          ** start with (pageX, pageY) absolute mouse coordinate
761
         *          - Apply translation: take into accounts the map offset in the page
762
         *          ** from this point, we have relative mouse coordinate
763
         *          - Apply homothetic transformation: take into accounts initial factor of map sizing (fullWidth / actualWidth)
764
         *          - Apply homothetic transformation: take into accounts the zoom factor
765
         *          ** from this point, we have relative map coordinate
766
         *          - Apply translation: take into accounts the current panning of the map
767
         *          ** from this point, we have absolute map coordinate
768
         * @param pageX: mouse client coordinate on X
769
         * @param pageY: mouse client coordinate on Y
770
         * @return map coordinate {x, y}
771
         */
772
        mapPagePositionToXY: function(pageX, pageY) {
773
            var self = this;
774
            var offset = self.$map.offset();
775
            var initFactor = (self.options.map.width) ? (self.mapConf.width / self.options.map.width) : (self.mapConf.width / self.$map.width());
776
            var zoomFactor = 1 / (1 + (self.zoomData.zoomLevel * self.options.map.zoom.step));
777
            return {
778
                x: (zoomFactor * initFactor * (pageX - offset.left)) + self.zoomData.panX,
779
                y: (zoomFactor * initFactor * (pageY - offset.top)) + self.zoomData.panY
780
            };
781
        },
782

783
        /*
784
         * Zoom on the map
785
         *
786
         * zoomOptions.animDuration zoom duration
787
         *
788
         * zoomOptions.level        level of the zoom between minLevel and maxLevel (absolute number, or relative string +1 or -1)
789
         * zoomOptions.fixedCenter  set to true in order to preserve the position of x,y in the canvas when zoomed
790
         *
791
         * zoomOptions.x            x coordinate of the point to focus on
792
         * zoomOptions.y            y coordinate of the point to focus on
793
         * - OR -
794
         * zoomOptions.latitude     latitude of the point to focus on
795
         * zoomOptions.longitude    longitude of the point to focus on
796
         * - OR -
797
         * zoomOptions.plot         plot ID to focus on
798
         * - OR -
799
         * zoomOptions.area         area ID to focus on
800
         * zoomOptions.areaMargin   margin (in pixels) around the area
801
         *
802
         * If an area ID is specified, the algorithm will override the zoom level to focus on the area
803
         * but it may be limited by the min/max zoom level limits set at initialization.
804
         *
805
         * If no coordinates are specified, the zoom will be focused on the center of the current view box
806
         *
807
         */
808
        onZoomEvent: function (e, zoomOptions) {
809
            var self = this;
810

811
            // new Top/Left corner coordinates
812
            var panX;
813
            var panY;
814
            // new Width/Height viewbox size
815
            var panWidth;
816
            var panHeight;
817

818
            // Zoom level in absolute scale (from 0 to max, by step of 1)
819
            var zoomLevel = self.zoomData.zoomLevel;
820

821
            // Relative zoom level (from 1 to max, by step of 0.25 (default))
822
            var previousRelativeZoomLevel = 1 + self.zoomData.zoomLevel * self.options.map.zoom.step;
823
            var relativeZoomLevel;
824

825
            var animDuration = (zoomOptions.animDuration !== undefined) ? zoomOptions.animDuration : self.options.map.zoom.animDuration;
826

827
            if (zoomOptions.area !== undefined) {
828
                /* An area is given
829
                 * We will define x/y coordinate AND a new zoom level to fill the area
830
                 */
831
                if (self.areas[zoomOptions.area] === undefined) throw new Error("Unknown area '" + zoomOptions.area + "'");
832
                var areaMargin = (zoomOptions.areaMargin !== undefined) ? zoomOptions.areaMargin : 10;
833
                var areaBBox = self.areas[zoomOptions.area].mapElem.getBBox();
834
                var areaFullWidth = areaBBox.width + 2 * areaMargin;
835
                var areaFullHeight = areaBBox.height + 2 * areaMargin;
836

837
                // Compute new x/y focus point (center of area)
838
                zoomOptions.x = areaBBox.cx;
839
                zoomOptions.y = areaBBox.cy;
840

841
                // Compute a new absolute zoomLevel value (inverse of relative -> absolute)
842
                // Take the min between zoomLevel on width vs. height to be able to see the whole area
843
                zoomLevel = Math.min(Math.floor((self.mapConf.width / areaFullWidth - 1) / self.options.map.zoom.step),
844
                                     Math.floor((self.mapConf.height / areaFullHeight - 1) / self.options.map.zoom.step));
845

846
            } else {
847

848
                // Get user defined zoom level
849
                if (zoomOptions.level !== undefined) {
850
                    if (typeof zoomOptions.level === "string") {
851
                        // level is a string, either "n", "+n" or "-n"
852
                        if ((zoomOptions.level.slice(0, 1) === '+') || (zoomOptions.level.slice(0, 1) === '-')) {
853
                            // zoomLevel is relative
854
                            zoomLevel = self.zoomData.zoomLevel + parseInt(zoomOptions.level, 10);
855
                        } else {
856
                            // zoomLevel is absolute
857
                            zoomLevel = parseInt(zoomOptions.level, 10);
858
                        }
859
                    } else {
860
                        // level is integer
861
                        if (zoomOptions.level < 0) {
862
                            // zoomLevel is relative
863
                            zoomLevel = self.zoomData.zoomLevel + zoomOptions.level;
864
                        } else {
865
                            // zoomLevel is absolute
866
                            zoomLevel = zoomOptions.level;
867
                        }
868
                    }
869
                }
870

871
                if (zoomOptions.plot !== undefined) {
872
                    if (self.plots[zoomOptions.plot] === undefined) throw new Error("Unknown plot '" + zoomOptions.plot + "'");
873

874
                    zoomOptions.x = self.plots[zoomOptions.plot].coords.x;
875
                    zoomOptions.y = self.plots[zoomOptions.plot].coords.y;
876
                } else {
877
                    if (zoomOptions.latitude !== undefined && zoomOptions.longitude !== undefined) {
878
                        var coords = self.mapConf.getCoords(zoomOptions.latitude, zoomOptions.longitude);
879
                        zoomOptions.x = coords.x;
880
                        zoomOptions.y = coords.y;
881
                    }
882

883
                    if (zoomOptions.x === undefined) {
884
                        zoomOptions.x = self.currentViewBox.x + self.currentViewBox.w / 2;
885
                    }
886

887
                    if (zoomOptions.y === undefined) {
888
                        zoomOptions.y = self.currentViewBox.y + self.currentViewBox.h / 2;
889
                    }
890
                }
891
            }
892

893
            // Make sure we stay in the zoom level boundaries
894
            zoomLevel = Math.min(Math.max(zoomLevel, self.options.map.zoom.minLevel), self.options.map.zoom.maxLevel);
895

896
            // Compute relative zoom level
897
            relativeZoomLevel = 1 + zoomLevel * self.options.map.zoom.step;
898

899
            // Compute panWidth / panHeight
900
            panWidth = self.mapConf.width / relativeZoomLevel;
901
            panHeight = self.mapConf.height / relativeZoomLevel;
902

903
            if (zoomLevel === 0) {
904
                panX = 0;
905
                panY = 0;
906
            } else {
907
                if (zoomOptions.fixedCenter !== undefined && zoomOptions.fixedCenter === true) {
908
                    panX = self.zoomData.panX + ((zoomOptions.x - self.zoomData.panX) * (relativeZoomLevel - previousRelativeZoomLevel)) / relativeZoomLevel;
909
                    panY = self.zoomData.panY + ((zoomOptions.y - self.zoomData.panY) * (relativeZoomLevel - previousRelativeZoomLevel)) / relativeZoomLevel;
910
                } else {
911
                    panX = zoomOptions.x - panWidth / 2;
912
                    panY = zoomOptions.y - panHeight / 2;
913
                }
914

915
                // Make sure we stay in the map boundaries
916
                panX = Math.min(Math.max(0, panX), self.mapConf.width - panWidth);
917
                panY = Math.min(Math.max(0, panY), self.mapConf.height - panHeight);
918
            }
919

920
            // Update zoom level of the map
921
            if (relativeZoomLevel === previousRelativeZoomLevel && panX === self.zoomData.panX && panY === self.zoomData.panY) return;
922

923
            if (animDuration > 0) {
924
                self.animateViewBox(panX, panY, panWidth, panHeight, animDuration, self.options.map.zoom.animEasing);
925
            } else {
926
                self.setViewBox(panX, panY, panWidth, panHeight);
927
                clearTimeout(self.zoomTO);
928
                self.zoomTO = setTimeout(function () {
929
                    self.$map.trigger("afterZoom", {
930
                        x1: panX,
931
                        y1: panY,
932
                        x2: panX + panWidth,
933
                        y2: panY + panHeight
934
                    });
935
                }, self.zoomFilteringTO);
936
            }
937

938
            $.extend(self.zoomData, {
939
                zoomLevel: zoomLevel,
940
                panX: panX,
941
                panY: panY,
942
                zoomX: panX + panWidth / 2,
943
                zoomY: panY + panHeight / 2
944
            });
945
        },
946

947
        /*
948
         * Show some element in range defined by user
949
         * Triggered by user $(".mapcontainer").trigger("showElementsInRange", [opt]);
950
         *
951
         * @param opt the options
952
         *  opt.hiddenOpacity opacity for hidden element (default = 0.3)
953
         *  opt.animDuration animation duration in ms (default = 0)
954
         *  opt.afterShowRange callback
955
         *  opt.ranges the range to show:
956
         *  Example:
957
         *  opt.ranges = {
958
         *      'plot' : {
959
         *          0 : {                        // valueIndex
960
         *              'min': 1000,
961
         *              'max': 1200
962
         *          },
963
         *          1 : {                        // valueIndex
964
         *              'min': 10,
965
         *              'max': 12
966
         *          }
967
         *      },
968
         *      'area' : {
969
         *          {'min': 10, 'max': 20}    // No valueIndex, only an object, use 0 as valueIndex (easy case)
970
         *      }
971
         *  }
972
         */
973
        onShowElementsInRange: function(e, opt) {
974
            var self = this;
975

976
            // set animDuration to default if not defined
977
            if (opt.animDuration === undefined) {
978
                opt.animDuration = 0;
979
            }
980

981
            // set hiddenOpacity to default if not defined
982
            if (opt.hiddenOpacity === undefined) {
983
                opt.hiddenOpacity = 0.3;
984
            }
985

986
            // handle area
987
            if (opt.ranges && opt.ranges.area) {
988
                self.showElemByRange(opt.ranges.area, self.areas, opt.hiddenOpacity, opt.animDuration);
989
            }
990

991
            // handle plot
992
            if (opt.ranges && opt.ranges.plot) {
993
                self.showElemByRange(opt.ranges.plot, self.plots, opt.hiddenOpacity, opt.animDuration);
994
            }
995

996
            // handle link
997
            if (opt.ranges && opt.ranges.link) {
998
                self.showElemByRange(opt.ranges.link, self.links, opt.hiddenOpacity, opt.animDuration);
999
            }
1000

1001
            // Call user callback
1002
            if (opt.afterShowRange) opt.afterShowRange();
1003
        },
1004

1005
        /*
1006
         * Show some element in range
1007
         * @param ranges: the ranges
1008
         * @param elems: list of element on which to check against previous range
1009
         * @hiddenOpacity: the opacity when hidden
1010
         * @animDuration: the animation duration
1011
         */
1012
        showElemByRange: function(ranges, elems, hiddenOpacity, animDuration) {
1013
            var self = this;
1014
            // Hold the final opacity value for all elements consolidated after applying each ranges
1015
            // This allow to set the opacity only once for each elements
1016
            var elemsFinalOpacity = {};
1017

1018
            // set object with one valueIndex to 0 if we have directly the min/max
1019
            if (ranges.min !== undefined || ranges.max !== undefined) {
1020
                ranges = {0: ranges};
1021
            }
1022

1023
            // Loop through each valueIndex
1024
            $.each(ranges, function (valueIndex) {
1025
                var range = ranges[valueIndex];
1026
                // Check if user defined at least a min or max value
1027
                if (range.min === undefined && range.max === undefined) {
1028
                    return true; // skip this iteration (each loop), goto next range
1029
                }
1030
                // Loop through each elements
1031
                $.each(elems, function (id) {
1032
                    var elemValue = elems[id].options.value;
1033
                    // set value with one valueIndex to 0 if not object
1034
                    if (typeof elemValue !== "object") {
1035
                        elemValue = [elemValue];
1036
                    }
1037
                    // Check existence of this value index
1038
                    if (elemValue[valueIndex] === undefined) {
1039
                        return true; // skip this iteration (each loop), goto next element
1040
                    }
1041
                    // Check if in range
1042
                    if ((range.min !== undefined && elemValue[valueIndex] < range.min) ||
1043
                        (range.max !== undefined && elemValue[valueIndex] > range.max)) {
1044
                        // Element not in range
1045
                        elemsFinalOpacity[id] = hiddenOpacity;
1046
                    } else {
1047
                        // Element in range
1048
                        elemsFinalOpacity[id] = 1;
1049
                    }
1050
                });
1051
            });
1052
            // Now that we looped through all ranges, we can really assign the final opacity
1053
            $.each(elemsFinalOpacity, function (id) {
1054
                self.setElementOpacity(elems[id], elemsFinalOpacity[id], animDuration);
1055
            });
1056
        },
1057

1058
        /*
1059
         * Set element opacity
1060
         * Handle elem.mapElem and elem.textElem
1061
         * @param elem the element
1062
         * @param opacity the opacity to apply
1063
         * @param animDuration the animation duration to use
1064
         */
1065
        setElementOpacity: function(elem, opacity, animDuration) {
1066
            var self = this;
1067

1068
            // Ensure no animation is running
1069
            //elem.mapElem.stop();
1070
            //if (elem.textElem) elem.textElem.stop();
1071

1072
            // If final opacity is not null, ensure element is shown before proceeding
1073
            if (opacity > 0) {
1074
                elem.mapElem.show();
1075
                if (elem.textElem) elem.textElem.show();
1076
            }
1077

1078
            self.animate(elem.mapElem, {"opacity": opacity}, animDuration, function () {
1079
                // If final attribute is 0, hide
1080
                if (opacity === 0) elem.mapElem.hide();
1081
            });
1082

1083
            self.animate(elem.textElem, {"opacity": opacity}, animDuration, function () {
1084
                // If final attribute is 0, hide
1085
                if (opacity === 0) elem.textElem.hide();
1086
            });
1087
        },
1088

1089
        /*
1090
         * Update the current map
1091
         *
1092
         * Refresh attributes and tooltips for areas and plots
1093
         * @param opt option for the refresh :
1094
         *  opt.mapOptions: options to update for plots and areas
1095
         *  opt.replaceOptions: whether mapsOptions should entirely replace current map options, or just extend it
1096
         *  opt.opt.newPlots new plots to add to the map
1097
         *  opt.newLinks new links to add to the map
1098
         *  opt.deletePlotKeys plots to delete from the map (array, or "all" to remove all plots)
1099
         *  opt.deleteLinkKeys links to remove from the map (array, or "all" to remove all links)
1100
         *  opt.setLegendElemsState the state of legend elements to be set : show (default) or hide
1101
         *  opt.animDuration animation duration in ms (default = 0)
1102
         *  opt.afterUpdate hook that allows to add custom processing on the map
1103
         */
1104
        onUpdateEvent: function (e, opt) {
1105
            var self = this;
1106
            // Abort if opt is undefined
1107
            if (typeof opt !== "object")  return;
1108

1109
            var i = 0;
1110
            var animDuration = (opt.animDuration) ? opt.animDuration : 0;
1111

1112
            // This function remove an element using animation (or not, depending on animDuration)
1113
            // Used for deletePlotKeys and deleteLinkKeys
1114
            var fnRemoveElement = function (elem) {
1115

1116
                self.animate(elem.mapElem, {"opacity": 0}, animDuration, function () {
1117
                    elem.mapElem.remove();
1118
                });
1119

1120
                self.animate(elem.textElem, {"opacity": 0}, animDuration, function () {
1121
                    elem.textElem.remove();
1122
                });
1123
            };
1124

1125
            // This function show an element using animation
1126
            // Used for newPlots and newLinks
1127
            var fnShowElement = function (elem) {
1128
                // Starts with hidden elements
1129
                elem.mapElem.attr({opacity: 0});
1130
                if (elem.textElem) elem.textElem.attr({opacity: 0});
1131
                // Set final element opacity
1132
                self.setElementOpacity(
1133
                    elem,
1134
                    (elem.mapElem.originalAttrs.opacity !== undefined) ? elem.mapElem.originalAttrs.opacity : 1,
1135
                    animDuration
1136
                );
1137
            };
1138

1139
            if (typeof opt.mapOptions === "object") {
1140
                if (opt.replaceOptions === true) self.options = self.extendDefaultOptions(opt.mapOptions);
1141
                else $.extend(true, self.options, opt.mapOptions);
1142

1143
                // IF we update areas, plots or legend, then reset all legend state to "show"
1144
                if (opt.mapOptions.areas !== undefined || opt.mapOptions.plots !== undefined || opt.mapOptions.legend !== undefined) {
1145
                    $("[data-type='legend-elem']", self.$container).each(function (id, elem) {
1146
                        if ($(elem).attr('data-hidden') === "1") {
1147
                            // Toggle state of element by clicking
1148
                            $(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration});
1149
                        }
1150
                    });
1151
                }
1152
            }
1153

1154
            // Delete plots by name if deletePlotKeys is array
1155
            if (typeof opt.deletePlotKeys === "object") {
1156
                for (; i < opt.deletePlotKeys.length; i++) {
1157
                    if (self.plots[opt.deletePlotKeys[i]] !== undefined) {
1158
                        fnRemoveElement(self.plots[opt.deletePlotKeys[i]]);
1159
                        delete self.plots[opt.deletePlotKeys[i]];
1160
                    }
1161
                }
1162
                // Delete ALL plots if deletePlotKeys is set to "all"
1163
            } else if (opt.deletePlotKeys === "all") {
1164
                $.each(self.plots, function (id, elem) {
1165
                    fnRemoveElement(elem);
1166
                });
1167
                // Empty plots object
1168
                self.plots = {};
1169
            }
1170

1171
            // Delete links by name if deleteLinkKeys is array
1172
            if (typeof opt.deleteLinkKeys === "object") {
1173
                for (i = 0; i < opt.deleteLinkKeys.length; i++) {
1174
                    if (self.links[opt.deleteLinkKeys[i]] !== undefined) {
1175
                        fnRemoveElement(self.links[opt.deleteLinkKeys[i]]);
1176
                        delete self.links[opt.deleteLinkKeys[i]];
1177
                    }
1178
                }
1179
                // Delete ALL links if deleteLinkKeys is set to "all"
1180
            } else if (opt.deleteLinkKeys === "all") {
1181
                $.each(self.links, function (id, elem) {
1182
                    fnRemoveElement(elem);
1183
                });
1184
                // Empty links object
1185
                self.links = {};
1186
            }
1187

1188
            // New plots
1189
            if (typeof opt.newPlots === "object") {
1190
                $.each(opt.newPlots, function (id) {
1191
                    if (self.plots[id] === undefined) {
1192
                        self.options.plots[id] = opt.newPlots[id];
1193
                        self.plots[id] = self.drawPlot(id);
1194
                        if (animDuration > 0) {
1195
                            fnShowElement(self.plots[id]);
1196
                        }
1197
                    }
1198
                });
1199
            }
1200

1201
            // New links
1202
            if (typeof opt.newLinks === "object") {
1203
                var newLinks = self.drawLinksCollection(opt.newLinks);
1204
                $.extend(self.links, newLinks);
1205
                $.extend(self.options.links, opt.newLinks);
1206
                if (animDuration > 0) {
1207
                    $.each(newLinks, function (id) {
1208
                        fnShowElement(newLinks[id]);
1209
                    });
1210
                }
1211
            }
1212

1213
            // Update areas attributes and tooltips
1214
            $.each(self.areas, function (id) {
1215
                // Avoid updating unchanged elements
1216
                if ((typeof opt.mapOptions === "object" &&
1217
                    (
1218
                        (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultArea === "object") ||
1219
                        (typeof opt.mapOptions.areas === "object" && typeof opt.mapOptions.areas[id] === "object") ||
1220
                        (typeof opt.mapOptions.legend === "object" && typeof opt.mapOptions.legend.area === "object")
1221
                    )) || opt.replaceOptions === true
1222
                ) {
1223
                    self.areas[id].options = self.getElemOptions(
1224
                        self.options.map.defaultArea,
1225
                        (self.options.areas[id] ? self.options.areas[id] : {}),
1226
                        self.options.legend.area
1227
                    );
1228
                    self.updateElem(self.areas[id], animDuration);
1229
                }
1230
            });
1231

1232
            // Update plots attributes and tooltips
1233
            $.each(self.plots, function (id) {
1234
                // Avoid updating unchanged elements
1235
                if ((typeof opt.mapOptions ==="object" &&
1236
                    (
1237
                        (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultPlot === "object") ||
1238
                        (typeof opt.mapOptions.plots === "object" && typeof opt.mapOptions.plots[id] === "object") ||
1239
                        (typeof opt.mapOptions.legend === "object" && typeof opt.mapOptions.legend.plot === "object")
1240
                    )) || opt.replaceOptions === true
1241
                ) {
1242
                    self.plots[id].options = self.getElemOptions(
1243
                        self.options.map.defaultPlot,
1244
                        (self.options.plots[id] ? self.options.plots[id] : {}),
1245
                        self.options.legend.plot
1246
                    );
1247

1248
                    self.setPlotCoords(self.plots[id]);
1249
                    self.setPlotAttributes(self.plots[id]);
1250

1251
                    self.updateElem(self.plots[id], animDuration);
1252
                }
1253
            });
1254

1255
            // Update links attributes and tooltips
1256
            $.each(self.links, function (id) {
1257
                // Avoid updating unchanged elements
1258
                if ((typeof opt.mapOptions === "object" &&
1259
                    (
1260
                        (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultLink === "object") ||
1261
                        (typeof opt.mapOptions.links === "object" && typeof opt.mapOptions.links[id] === "object")
1262
                    )) || opt.replaceOptions === true
1263
                ) {
1264
                    self.links[id].options = self.getElemOptions(
1265
                        self.options.map.defaultLink,
1266
                        (self.options.links[id] ? self.options.links[id] : {}),
1267
                        {}
1268
                    );
1269

1270
                    self.updateElem(self.links[id], animDuration);
1271
                }
1272
            });
1273

1274
            // Update legends
1275
            if (opt.mapOptions && (
1276
                    (typeof opt.mapOptions.legend === "object") ||
1277
                    (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultArea === "object") ||
1278
                    (typeof opt.mapOptions.map === "object" && typeof opt.mapOptions.map.defaultPlot === "object")
1279
                )) {
1280
                // Show all elements on the map before updating the legends
1281
                $("[data-type='legend-elem']", self.$container).each(function (id, elem) {
1282
                    if ($(elem).attr('data-hidden') === "1") {
1283
                        $(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration});
1284
                    }
1285
                });
1286

1287
                self.createLegends("area", self.areas, 1);
1288
                if (self.options.map.width) {
1289
                    self.createLegends("plot", self.plots, (self.options.map.width / self.mapConf.width));
1290
                } else {
1291
                    self.createLegends("plot", self.plots, (self.$map.width() / self.mapConf.width));
1292
                }
1293
            }
1294

1295
            // Hide/Show all elements based on showlegendElems
1296
            //      Toggle (i.e. click) only if:
1297
            //          - slice legend is shown AND we want to hide
1298
            //          - slice legend is hidden AND we want to show
1299
            if (typeof opt.setLegendElemsState === "object") {
1300
                // setLegendElemsState is an object listing the legend we want to hide/show
1301
                $.each(opt.setLegendElemsState, function (legendCSSClass, action) {
1302
                    // Search for the legend
1303
                    var $legend = self.$container.find("." + legendCSSClass)[0];
1304
                    if ($legend !== undefined) {
1305
                        // Select all elem inside this legend
1306
                        $("[data-type='legend-elem']", $legend).each(function (id, elem) {
1307
                            if (($(elem).attr('data-hidden') === "0" && action === "hide") ||
1308
                                ($(elem).attr('data-hidden') === "1" && action === "show")) {
1309
                                // Toggle state of element by clicking
1310
                                $(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration});
1311
                            }
1312
                        });
1313
                    }
1314
                });
1315
            } else {
1316
                // setLegendElemsState is a string, or is undefined
1317
                // Default : "show"
1318
                var action = (opt.setLegendElemsState === "hide") ? "hide" : "show";
1319

1320
                $("[data-type='legend-elem']", self.$container).each(function (id, elem) {
1321
                    if (($(elem).attr('data-hidden') === "0" && action === "hide") ||
1322
                        ($(elem).attr('data-hidden') === "1" && action === "show")) {
1323
                        // Toggle state of element by clicking
1324
                        $(elem).trigger("click", {hideOtherElems: false, animDuration: animDuration});
1325
                    }
1326
                });
1327
            }
1328

1329
            // Always rebind custom events on update
1330
            self.initDelegatedCustomEvents();
1331

1332
            if (opt.afterUpdate) opt.afterUpdate(self.$container, self.paper, self.areas, self.plots, self.options, self.links);
1333
        },
1334

1335
        /*
1336
         * Set plot coordinates
1337
         * @param plot object plot element
1338
         */
1339
        setPlotCoords: function(plot) {
1340
            var self = this;
1341

1342
            if (plot.options.x !== undefined && plot.options.y !== undefined) {
1343
                plot.coords = {
1344
                    x: plot.options.x,
1345
                    y: plot.options.y
1346
                };
1347
            } else if (plot.options.plotsOn !== undefined && self.areas[plot.options.plotsOn] !== undefined) {
1348
                var areaBBox = self.areas[plot.options.plotsOn].mapElem.getBBox();
1349
                plot.coords = {
1350
                    x: areaBBox.cx,
1351
                    y: areaBBox.cy
1352
                };
1353
            } else {
1354
                plot.coords = self.mapConf.getCoords(plot.options.latitude, plot.options.longitude);
1355
            }
1356
        },
1357

1358
        /*
1359
         * Set plot size attributes according to its type
1360
         * Note: for SVG, plot.mapElem needs to exists beforehand
1361
         * @param plot object plot element
1362
         */
1363
        setPlotAttributes: function(plot) {
1364
            if (plot.options.type === "square") {
1365
                plot.options.attrs.width = plot.options.size;
1366
                plot.options.attrs.height = plot.options.size;
1367
                plot.options.attrs.x = plot.coords.x - (plot.options.size / 2);
1368
                plot.options.attrs.y = plot.coords.y - (plot.options.size / 2);
1369
            } else if (plot.options.type === "image") {
1370
                plot.options.attrs.src = plot.options.url;
1371
                plot.options.attrs.width = plot.options.width;
1372
                plot.options.attrs.height = plot.options.height;
1373
                plot.options.attrs.x = plot.coords.x - (plot.options.width / 2);
1374
                plot.options.attrs.y = plot.coords.y - (plot.options.height / 2);
1375
            } else if (plot.options.type === "svg") {
1376
                plot.options.attrs.path = plot.options.path;
1377

1378
                // Init transform string
1379
                if (plot.options.attrs.transform === undefined) {
1380
                    plot.options.attrs.transform = "";
1381
                }
1382

1383
                // Retrieve original boundary box if not defined
1384
                if (plot.mapElem.originalBBox === undefined) {
1385
                    plot.mapElem.originalBBox = plot.mapElem.getBBox();
1386
                }
1387

1388
                // The base transform will resize the SVG path to the one specified by width/height
1389
                // and also move the path to the actual coordinates
1390
                plot.mapElem.baseTransform = "m" + (plot.options.width / plot.mapElem.originalBBox.width) + ",0,0," +
1391
                                                   (plot.options.height / plot.mapElem.originalBBox.height) + "," +
1392
                                                   (plot.coords.x - plot.options.width / 2) + "," +
1393
                                                   (plot.coords.y - plot.options.height / 2);
1394

1395
                plot.options.attrs.transform = plot.mapElem.baseTransform + plot.options.attrs.transform;
1396

1397
            } else { // Default : circle
1398
                plot.options.attrs.x = plot.coords.x;
1399
                plot.options.attrs.y = plot.coords.y;
1400
                plot.options.attrs.r = plot.options.size / 2;
1401
            }
1402
        },
1403

1404
        /*
1405
         * Draw all links between plots on the paper
1406
         */
1407
        drawLinksCollection: function (linksCollection) {
1408
            var self = this;
1409
            var p1 = {};
1410
            var p2 = {};
1411
            var coordsP1 = {};
1412
            var coordsP2 = {};
1413
            var links = {};
1414

1415
            $.each(linksCollection, function (id) {
1416
                var elemOptions = self.getElemOptions(self.options.map.defaultLink, linksCollection[id], {});
1417

1418
                if (typeof linksCollection[id].between[0] === 'string') {
1419
                    p1 = self.options.plots[linksCollection[id].between[0]];
1420
                } else {
1421
                    p1 = linksCollection[id].between[0];
1422
                }
1423

1424
                if (typeof linksCollection[id].between[1] === 'string') {
1425
                    p2 = self.options.plots[linksCollection[id].between[1]];
1426
                } else {
1427
                    p2 = linksCollection[id].between[1];
1428
                }
1429

1430
                if (p1.plotsOn !== undefined && self.areas[p1.plotsOn] !== undefined) {
1431
                    var p1BBox = self.areas[p1.plotsOn].mapElem.getBBox();
1432
                    coordsP1 = {
1433
                        x: p1BBox.cx,
1434
                        y: p1BBox.cy
1435
                    };
1436
                }
1437
                else if (p1.latitude !== undefined && p1.longitude !== undefined) {
1438
                    coordsP1 = self.mapConf.getCoords(p1.latitude, p1.longitude);
1439
                } else {
1440
                    coordsP1.x = p1.x;
1441
                    coordsP1.y = p1.y;
1442
                }
1443

1444
                if (p2.plotsOn !== undefined && self.areas[p2.plotsOn] !== undefined) {
1445
                    var p2BBox = self.areas[p2.plotsOn].mapElem.getBBox();
1446
                    coordsP2 = {
1447
                        x: p2BBox.cx,
1448
                        y: p2BBox.cy
1449
                    };
1450
                }
1451
                else if (p2.latitude !== undefined && p2.longitude !== undefined) {
1452
                    coordsP2 = self.mapConf.getCoords(p2.latitude, p2.longitude);
1453
                } else {
1454
                    coordsP2.x = p2.x;
1455
                    coordsP2.y = p2.y;
1456
                }
1457
                links[id] = self.drawLink(id, coordsP1.x, coordsP1.y, coordsP2.x, coordsP2.y, elemOptions);
1458
            });
1459
            return links;
1460
        },
1461

1462
        /*
1463
         * Draw a curved link between two couples of coordinates a(xa,ya) and b(xb, yb) on the paper
1464
         */
1465
        drawLink: function (id, xa, ya, xb, yb, elemOptions) {
1466
            var self = this;
1467
            var link = {
1468
                options: elemOptions
1469
            };
1470
            // Compute the "curveto" SVG point, d(x,y)
1471
            // c(xc, yc) is the center of (xa,ya) and (xb, yb)
1472
            var xc = (xa + xb) / 2;
1473
            var yc = (ya + yb) / 2;
1474

1475
            // Equation for (cd) : y = acd * x + bcd (d is the cure point)
1476
            var acd = -1 / ((yb - ya) / (xb - xa));
1477
            var bcd = yc - acd * xc;
1478

1479
            // dist(c,d) = dist(a,b) (=abDist)
1480
            var abDist = Math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya));
1481

1482
            // Solution for equation dist(cd) = sqrt((xd - xc)² + (yd - yc)²)
1483
            // dist(c,d)² = (xd - xc)² + (yd - yc)²
1484
            // We assume that dist(c,d) = dist(a,b)
1485
            // so : (xd - xc)² + (yd - yc)² - dist(a,b)² = 0
1486
            // With the factor : (xd - xc)² + (yd - yc)² - (factor*dist(a,b))² = 0
1487
            // (xd - xc)² + (acd*xd + bcd - yc)² - (factor*dist(a,b))² = 0
1488
            var a = 1 + acd * acd;
1489
            var b = -2 * xc + 2 * acd * bcd - 2 * acd * yc;
1490
            var c = xc * xc + bcd * bcd - bcd * yc - yc * bcd + yc * yc - ((elemOptions.factor * abDist) * (elemOptions.factor * abDist));
1491
            var delta = b * b - 4 * a * c;
1492
            var x = 0;
1493
            var y = 0;
1494

1495
            // There are two solutions, we choose one or the other depending on the sign of the factor
1496
            if (elemOptions.factor > 0) {
1497
                x = (-b + Math.sqrt(delta)) / (2 * a);
1498
                y = acd * x + bcd;
1499
            } else {
1500
                x = (-b - Math.sqrt(delta)) / (2 * a);
1501
                y = acd * x + bcd;
1502
            }
1503

1504
            link.mapElem = self.paper.path("m " + xa + "," + ya + " C " + x + "," + y + " " + xb + "," + yb + " " + xb + "," + yb + "");
1505

1506
            self.initElem(id, 'link', link);
1507

1508
            return link;
1509
        },
1510

1511
        /*
1512
         * Check wether newAttrs object bring modifications to originalAttrs object
1513
         */
1514
        isAttrsChanged: function(originalAttrs, newAttrs) {
1515
            for (var key in newAttrs) {
1516
                if (newAttrs.hasOwnProperty(key) && typeof originalAttrs[key] === 'undefined' || newAttrs[key] !== originalAttrs[key]) {
1517
                    return true;
1518
                }
1519
            }
1520
            return false;
1521
        },
1522

1523
        /*
1524
         * Update the element "elem" on the map with the new options
1525
         */
1526
        updateElem: function (elem, animDuration) {
1527
            var self = this;
1528
            var mapElemBBox;
1529
            var plotOffsetX;
1530
            var plotOffsetY;
1531

1532
            if (elem.options.toFront === true) {
1533
                elem.mapElem.toFront();
1534
            }
1535

1536
            // Set the cursor attribute related to the HTML link
1537
            if (elem.options.href !== undefined) {
1538
                elem.options.attrs.cursor = "pointer";
1539
                if (elem.options.text) elem.options.text.attrs.cursor = "pointer";
1540
            } else {
1541
                // No HTML links, check if a cursor was defined to pointer
1542
                if (elem.mapElem.attrs.cursor === 'pointer') {
1543
                    elem.options.attrs.cursor = "auto";
1544
                    if (elem.options.text) elem.options.text.attrs.cursor = "auto";
1545
                }
1546
            }
1547

1548
            // Update the label
1549
            if (elem.textElem) {
1550
                // Update text attr
1551
                elem.options.text.attrs.text = elem.options.text.content;
1552

1553
                // Get mapElem size, and apply an offset to handle future width/height change
1554
                mapElemBBox = elem.mapElem.getBBox();
1555
                if (elem.options.size || (elem.options.width && elem.options.height)) {
1556
                    if (elem.options.type === "image" || elem.options.type === "svg") {
1557
                        plotOffsetX = (elem.options.width - mapElemBBox.width) / 2;
1558
                        plotOffsetY = (elem.options.height - mapElemBBox.height) / 2;
1559
                    } else {
1560
                        plotOffsetX = (elem.options.size - mapElemBBox.width) / 2;
1561
                        plotOffsetY = (elem.options.size - mapElemBBox.height) / 2;
1562
                    }
1563
                    mapElemBBox.x -= plotOffsetX;
1564
                    mapElemBBox.x2 += plotOffsetX;
1565
                    mapElemBBox.y -= plotOffsetY;
1566
                    mapElemBBox.y2 += plotOffsetY;
1567
                }
1568

1569
                // Update position attr
1570
                var textPosition = self.getTextPosition(mapElemBBox, elem.options.text.position, elem.options.text.margin);
1571
                elem.options.text.attrs.x = textPosition.x;
1572
                elem.options.text.attrs.y = textPosition.y;
1573
                elem.options.text.attrs['text-anchor'] = textPosition.textAnchor;
1574

1575
                // Update text element attrs and attrsHover
1576
                self.setHoverOptions(elem.textElem, elem.options.text.attrs, elem.options.text.attrsHover);
1577

1578
                if (self.isAttrsChanged(elem.textElem.attrs, elem.options.text.attrs)) {
1579
                    self.animate(elem.textElem, elem.options.text.attrs, animDuration);
1580
                }
1581
            }
1582

1583
            // Update elements attrs and attrsHover
1584
            self.setHoverOptions(elem.mapElem, elem.options.attrs, elem.options.attrsHover);
1585

1586
            if (self.isAttrsChanged(elem.mapElem.attrs, elem.options.attrs)) {
1587
                self.animate(elem.mapElem, elem.options.attrs, animDuration);
1588
            }
1589

1590
            // Update the cssClass
1591
            if (elem.options.cssClass !== undefined) {
1592
                $(elem.mapElem.node).removeClass().addClass(elem.options.cssClass);
1593
            }
1594
        },
1595

1596
        /*
1597
         * Draw the plot
1598
         */
1599
        drawPlot: function (id) {
1600
            var self = this;
1601
            var plot = {};
1602

1603
            // Get plot options and store it
1604
            plot.options = self.getElemOptions(
1605
                self.options.map.defaultPlot,
1606
                (self.options.plots[id] ? self.options.plots[id] : {}),
1607
                self.options.legend.plot
1608
            );
1609

1610
            // Set plot coords
1611
            self.setPlotCoords(plot);
1612

1613
            // Draw SVG before setPlotAttributes()
1614
            if (plot.options.type === "svg") {
1615
                plot.mapElem = self.paper.path(plot.options.path);
1616
            }
1617

1618
            // Set plot size attrs
1619
            self.setPlotAttributes(plot);
1620

1621
            // Draw other types of plots
1622
            if (plot.options.type === "square") {
1623
                plot.mapElem = self.paper.rect(
1624
                    plot.options.attrs.x,
1625
                    plot.options.attrs.y,
1626
                    plot.options.attrs.width,
1627
                    plot.options.attrs.height
1628
                );
1629
            } else if (plot.options.type === "image") {
1630
                plot.mapElem = self.paper.image(
1631
                    plot.options.attrs.src,
1632
                    plot.options.attrs.x,
1633
                    plot.options.attrs.y,
1634
                    plot.options.attrs.width,
1635
                    plot.options.attrs.height
1636
                );
1637
            } else if (plot.options.type === "svg") {
1638
                // Nothing to do
1639
            } else {
1640
                // Default = circle
1641
                plot.mapElem = self.paper.circle(
1642
                    plot.options.attrs.x,
1643
                    plot.options.attrs.y,
1644
                    plot.options.attrs.r
1645
                );
1646
            }
1647

1648
            self.initElem(id, 'plot', plot);
1649

1650
            return plot;
1651
        },
1652

1653
        /*
1654
         * Set user defined handlers for events on areas and plots
1655
         * @param id the id of the element
1656
         * @param type the type of the element (area, plot, link)
1657
         * @param elem the element object {mapElem, textElem, options, ...}
1658
         */
1659
        setEventHandlers: function (id, type, elem) {
1660
            var self = this;
1661
            $.each(elem.options.eventHandlers, function (event) {
1662
                if (self.customEventHandlers[event] === undefined) self.customEventHandlers[event] = {};
1663
                if (self.customEventHandlers[event][type] === undefined) self.customEventHandlers[event][type] = {};
1664
                self.customEventHandlers[event][type][id] = elem;
1665
            });
1666
        },
1667

1668
        /*
1669
         * Draw a legend for areas and / or plots
1670
         * @param legendOptions options for the legend to draw
1671
         * @param legendType the type of the legend : "area" or "plot"
1672
         * @param elems collection of plots or areas on the maps
1673
         * @param legendIndex index of the legend in the conf array
1674
         */
1675
        drawLegend: function (legendOptions, legendType, elems, scale, legendIndex) {
1676
            var self = this;
1677
            var $legend = {};
1678
            var legendPaper = {};
1679
            var width = 0;
1680
            var height = 0;
1681
            var title = null;
1682
            var titleBBox = null;
1683
            var legendElems = {};
1684
            var i = 0;
1685
            var x = 0;
1686
            var y = 0;
1687
            var yCenter = 0;
1688
            var sliceOptions = [];
1689

1690
            $legend = $("." + legendOptions.cssClass, self.$container);
1691

1692
            // Save content for later
1693
            var initialHTMLContent = $legend.html();
1694
            $legend.empty();
1695

1696
            legendPaper = new Raphael($legend.get(0));
1697
            // Set some data to object
1698
            $(legendPaper.canvas).attr({"data-legend-type": legendType, "data-legend-id": legendIndex});
1699

1700
            height = width = 0;
1701

1702
            // Set the title of the legend
1703
            if (legendOptions.title && legendOptions.title !== "") {
1704
                title = legendPaper.text(legendOptions.marginLeftTitle, 0, legendOptions.title).attr(legendOptions.titleAttrs);
1705
                titleBBox = title.getBBox();
1706
                title.attr({y: 0.5 * titleBBox.height});
1707

1708
                width = legendOptions.marginLeftTitle + titleBBox.width;
1709
                height += legendOptions.marginBottomTitle + titleBBox.height;
1710
            }
1711

1712
            // Calculate attrs (and width, height and r (radius)) for legend elements, and yCenter for horizontal legends
1713

1714
            for (i = 0; i < legendOptions.slices.length; ++i) {
1715
                var yCenterCurrent = 0;
1716

1717
                sliceOptions[i] = $.extend(true, {}, (legendType === "plot") ? self.options.map.defaultPlot : self.options.map.defaultArea, legendOptions.slices[i]);
1718

1719
                if (legendOptions.slices[i].legendSpecificAttrs === undefined) {
1720
                    legendOptions.slices[i].legendSpecificAttrs = {};
1721
                }
1722

1723
                $.extend(true, sliceOptions[i].attrs, legendOptions.slices[i].legendSpecificAttrs);
1724

1725
                if (legendType === "area") {
1726
                    if (sliceOptions[i].attrs.width === undefined)
1727
                        sliceOptions[i].attrs.width = 30;
1728
                    if (sliceOptions[i].attrs.height === undefined)
1729
                        sliceOptions[i].attrs.height = 20;
1730
                } else if (sliceOptions[i].type === "square") {
1731
                    if (sliceOptions[i].attrs.width === undefined)
1732
                        sliceOptions[i].attrs.width = sliceOptions[i].size;
1733
                    if (sliceOptions[i].attrs.height === undefined)
1734
                        sliceOptions[i].attrs.height = sliceOptions[i].size;
1735
                } else if (sliceOptions[i].type === "image" || sliceOptions[i].type === "svg") {
1736
                    if (sliceOptions[i].attrs.width === undefined)
1737
                        sliceOptions[i].attrs.width = sliceOptions[i].width;
1738
                    if (sliceOptions[i].attrs.height === undefined)
1739
                        sliceOptions[i].attrs.height = sliceOptions[i].height;
1740
                } else {
1741
                    if (sliceOptions[i].attrs.r === undefined)
1742
                        sliceOptions[i].attrs.r = sliceOptions[i].size / 2;
1743
                }
1744

1745
                // Compute yCenter for this legend slice
1746
                yCenterCurrent = legendOptions.marginBottomTitle;
1747
                // Add title height if it exists
1748
                if (title) {
1749
                    yCenterCurrent += titleBBox.height;
1750
                }
1751
                if (legendType === "plot" && (sliceOptions[i].type === undefined || sliceOptions[i].type === "circle")) {
1752
                    yCenterCurrent += scale * sliceOptions[i].attrs.r;
1753
                } else {
1754
                    yCenterCurrent += scale * sliceOptions[i].attrs.height / 2;
1755
                }
1756
                // Update yCenter if current larger
1757
                yCenter = Math.max(yCenter, yCenterCurrent);
1758
            }
1759

1760
            if (legendOptions.mode === "horizontal") {
1761
                width = legendOptions.marginLeft;
1762
            }
1763

1764
            // Draw legend elements (circle, square or image in vertical or horizontal mode)
1765
            for (i = 0; i < sliceOptions.length; ++i) {
1766
                var legendElem = {};
1767
                var legendElemBBox = {};
1768
                var legendLabel = {};
1769

1770
                if (sliceOptions[i].display === undefined || sliceOptions[i].display === true) {
1771
                    if (legendType === "area") {
1772
                        if (legendOptions.mode === "horizontal") {
1773
                            x = width + legendOptions.marginLeft;
1774
                            y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height);
1775
                        } else {
1776
                            x = legendOptions.marginLeft;
1777
                            y = height;
1778
                        }
1779

1780
                        legendElem = legendPaper.rect(x, y, scale * (sliceOptions[i].attrs.width), scale * (sliceOptions[i].attrs.height));
1781
                    } else if (sliceOptions[i].type === "square") {
1782
                        if (legendOptions.mode === "horizontal") {
1783
                            x = width + legendOptions.marginLeft;
1784
                            y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height);
1785
                        } else {
1786
                            x = legendOptions.marginLeft;
1787
                            y = height;
1788
                        }
1789

1790
                        legendElem = legendPaper.rect(x, y, scale * (sliceOptions[i].attrs.width), scale * (sliceOptions[i].attrs.height));
1791

1792
                    } else if (sliceOptions[i].type === "image" || sliceOptions[i].type === "svg") {
1793
                        if (legendOptions.mode === "horizontal") {
1794
                            x = width + legendOptions.marginLeft;
1795
                            y = yCenter - (0.5 * scale * sliceOptions[i].attrs.height);
1796
                        } else {
1797
                            x = legendOptions.marginLeft;
1798
                            y = height;
1799
                        }
1800

1801
                        if (sliceOptions[i].type === "image") {
1802
                            legendElem = legendPaper.image(
1803
                                sliceOptions[i].url, x, y, scale * sliceOptions[i].attrs.width, scale * sliceOptions[i].attrs.height);
1804
                        } else {
1805
                            legendElem = legendPaper.path(sliceOptions[i].path);
1806

1807
                            if (sliceOptions[i].attrs.transform === undefined) {
1808
                                sliceOptions[i].attrs.transform = "";
1809
                            }
1810
                            legendElemBBox = legendElem.getBBox();
1811
                            sliceOptions[i].attrs.transform = "m" + ((scale * sliceOptions[i].width) / legendElemBBox.width) + ",0,0," + ((scale * sliceOptions[i].height) / legendElemBBox.height) + "," + x + "," + y + sliceOptions[i].attrs.transform;
1812
                        }
1813
                    } else {
1814
                        if (legendOptions.mode === "horizontal") {
1815
                            x = width + legendOptions.marginLeft + scale * (sliceOptions[i].attrs.r);
1816
                            y = yCenter;
1817
                        } else {
1818
                            x = legendOptions.marginLeft + scale * (sliceOptions[i].attrs.r);
1819
                            y = height + scale * (sliceOptions[i].attrs.r);
1820
                        }
1821
                        legendElem = legendPaper.circle(x, y, scale * (sliceOptions[i].attrs.r));
1822
                    }
1823

1824
                    // Set attrs to the element drawn above
1825
                    delete sliceOptions[i].attrs.width;
1826
                    delete sliceOptions[i].attrs.height;
1827
                    delete sliceOptions[i].attrs.r;
1828
                    legendElem.attr(sliceOptions[i].attrs);
1829
                    legendElemBBox = legendElem.getBBox();
1830

1831
                    // Draw the label associated with the element
1832
                    if (legendOptions.mode === "horizontal") {
1833
                        x = width + legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel;
1834
                        y = yCenter;
1835
                    } else {
1836
                        x = legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel;
1837
                        y = height + (legendElemBBox.height / 2);
1838
                    }
1839

1840
                    legendLabel = legendPaper.text(x, y, sliceOptions[i].label).attr(legendOptions.labelAttrs);
1841

1842
                    // Update the width and height for the paper
1843
                    if (legendOptions.mode === "horizontal") {
1844
                        var currentHeight = legendOptions.marginBottom + legendElemBBox.height;
1845
                        width += legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel + legendLabel.getBBox().width;
1846
                        if (sliceOptions[i].type !== "image" && legendType !== "area") {
1847
                            currentHeight += legendOptions.marginBottomTitle;
1848
                        }
1849
                        // Add title height if it exists
1850
                        if (title) {
1851
                            currentHeight += titleBBox.height;
1852
                        }
1853
                        height = Math.max(height, currentHeight);
1854
                    } else {
1855
                        width = Math.max(width, legendOptions.marginLeft + legendElemBBox.width + legendOptions.marginLeftLabel + legendLabel.getBBox().width);
1856
                        height += legendOptions.marginBottom + legendElemBBox.height;
1857
                    }
1858

1859
                    // Set some data to elements
1860
                    $(legendElem.node).attr({
1861
                        "data-legend-id": legendIndex,
1862
                        "data-legend-type": legendType,
1863
                        "data-type": "legend-elem",
1864
                        "data-id": i,
1865
                        "data-hidden": 0
1866
                    });
1867
                    $(legendLabel.node).attr({
1868
                        "data-legend-id": legendIndex,
1869
                        "data-legend-type": legendType,
1870
                        "data-type": "legend-label",
1871
                        "data-id": i,
1872
                        "data-hidden": 0
1873
                    });
1874

1875
                    // Set array content
1876
                    // We use similar names like map/plots/links
1877
                    legendElems[i] = {
1878
                        mapElem: legendElem,
1879
                        textElem: legendLabel
1880
                    };
1881

1882
                    // Hide map elements when the user clicks on a legend item
1883
                    if (legendOptions.hideElemsOnClick.enabled) {
1884
                        // Hide/show elements when user clicks on a legend element
1885
                        legendLabel.attr({cursor: "pointer"});
1886
                        legendElem.attr({cursor: "pointer"});
1887

1888
                        self.setHoverOptions(legendElem, sliceOptions[i].attrs, sliceOptions[i].attrs);
1889
                        self.setHoverOptions(legendLabel, legendOptions.labelAttrs, legendOptions.labelAttrsHover);
1890

1891
                        if (sliceOptions[i].clicked !== undefined && sliceOptions[i].clicked === true) {
1892
                            self.handleClickOnLegendElem(legendElems[i], i, legendIndex, legendType, {hideOtherElems: false});
1893
                        }
1894
                    }
1895
                }
1896
            }
1897

1898
            // VMLWidth option allows you to set static width for the legend
1899
            // only for VML render because text.getBBox() returns wrong values on IE6/7
1900
            if (Raphael.type !== "SVG" && legendOptions.VMLWidth)
1901
                width = legendOptions.VMLWidth;
1902

1903
            legendPaper.setSize(width, height);
1904

1905
            return {
1906
                container: $legend,
1907
                initialHTMLContent: initialHTMLContent,
1908
                elems: legendElems
1909
            };
1910
        },
1911

1912
        /*
1913
         * Allow to hide elements of the map when the user clicks on a related legend item
1914
         * @param elem legend element
1915
         * @param id legend element ID
1916
         * @param legendIndex corresponding legend index
1917
         * @param legendType corresponding legend type (area or plot)
1918
         * @param opts object additionnal options
1919
         *          hideOtherElems boolean, if other elems shall be hidden
1920
         *          animDuration duration of animation
1921
         */
1922
        handleClickOnLegendElem: function(elem, id, legendIndex, legendType, opts) {
1923
            var self = this;
1924
            var legendOptions;
1925
            opts = opts || {};
1926

1927
            if (!$.isArray(self.options.legend[legendType])) {
1928
                legendOptions = self.options.legend[legendType];
1929
            } else {
1930
                legendOptions = self.options.legend[legendType][legendIndex];
1931
            }
1932

1933
            var legendElem = elem.mapElem;
1934
            var legendLabel = elem.textElem;
1935
            var $legendElem = $(legendElem.node);
1936
            var $legendLabel = $(legendLabel.node);
1937
            var sliceOptions = legendOptions.slices[id];
1938
            var mapElems = legendType === 'area' ? self.areas : self.plots;
1939
            // Check animDuration: if not set, this is a regular click, use the value specified in options
1940
            var animDuration = opts.animDuration !== undefined ? opts.animDuration : legendOptions.hideElemsOnClick.animDuration ;
1941

1942
            var hidden = $legendElem.attr('data-hidden');
1943
            var hiddenNewAttr = (hidden === '0') ? {"data-hidden": '1'} : {"data-hidden": '0'};
1944

1945
            if (hidden === '0') {
1946
                self.animate(legendLabel, {"opacity": 0.5}, animDuration);
1947
            } else {
1948
                self.animate(legendLabel, {"opacity": 1}, animDuration);
1949
            }
1950

1951
            $.each(mapElems, function (y) {
1952
                var elemValue;
1953

1954
                // Retreive stored data of element
1955
                //      'hidden-by' contains the list of legendIndex that is hiding this element
1956
                var hiddenBy = mapElems[y].mapElem.data('hidden-by');
1957
                // Set to empty object if undefined
1958
                if (hiddenBy === undefined) hiddenBy = {};
1959

1960
                if ($.isArray(mapElems[y].options.value)) {
1961
                    elemValue = mapElems[y].options.value[legendIndex];
1962
                } else {
1963
                    elemValue = mapElems[y].options.value;
1964
                }
1965

1966
                // Hide elements whose value matches with the slice of the clicked legend item
1967
                if (self.getLegendSlice(elemValue, legendOptions) === sliceOptions) {
1968
                    if (hidden === '0') { // we want to hide this element
1969
                        hiddenBy[legendIndex] = true; // add legendIndex to the data object for later use
1970
                        self.setElementOpacity(mapElems[y], legendOptions.hideElemsOnClick.opacity, animDuration);
1971
                    } else { // We want to show this element
1972
                        delete hiddenBy[legendIndex]; // Remove this legendIndex from object
1973
                        // Check if another legendIndex is defined
1974
                        // We will show this element only if no legend is no longer hiding it
1975
                        if ($.isEmptyObject(hiddenBy)) {
1976
                            self.setElementOpacity(
1977
                                mapElems[y],
1978
                                mapElems[y].mapElem.originalAttrs.opacity !== undefined ? mapElems[y].mapElem.originalAttrs.opacity : 1,
1979
                                animDuration
1980
                            );
1981
                        }
1982
                    }
1983
                    // Update elem data with new values
1984
                    mapElems[y].mapElem.data('hidden-by', hiddenBy);
1985
                }
1986
            });
1987

1988
            $legendElem.attr(hiddenNewAttr);
1989
            $legendLabel.attr(hiddenNewAttr);
1990

1991
            if ((opts.hideOtherElems === undefined || opts.hideOtherElems === true) && legendOptions.exclusive === true ) {
1992
                $("[data-type='legend-elem'][data-hidden=0]", self.$container).each(function () {
1993
                    var $elem = $(this);
1994
                    if ($elem.attr('data-id') !== id) {
1995
                        $elem.trigger("click", {hideOtherElems: false});
1996
                    }
1997
                });
1998
            }
1999

2000
        },
2001

2002
        /*
2003
         * Create all legends for a specified type (area or plot)
2004
         * @param legendType the type of the legend : "area" or "plot"
2005
         * @param elems collection of plots or areas displayed on the map
2006
         * @param scale scale ratio of the map
2007
         */
2008
        createLegends: function (legendType, elems, scale) {
2009
            var self = this;
2010
            var legendsOptions = self.options.legend[legendType];
2011

2012
            if (!$.isArray(self.options.legend[legendType])) {
2013
                legendsOptions = [self.options.legend[legendType]];
2014
            }
2015

2016
            self.legends[legendType] = {};
2017
            for (var j = 0; j < legendsOptions.length; ++j) {
2018
                if (legendsOptions[j].display === true  && $.isArray(legendsOptions[j].slices) && legendsOptions[j].slices.length > 0 &&
2019
                    legendsOptions[j].cssClass !== "" && $("." + legendsOptions[j].cssClass, self.$container).length !== 0
2020
                ) {
2021
                    self.legends[legendType][j] = self.drawLegend(legendsOptions[j], legendType, elems, scale, j);
2022
                }
2023
            }
2024
        },
2025

2026
        /*
2027
         * Set the attributes on hover and the attributes to restore for a map element
2028
         * @param elem the map element
2029
         * @param originalAttrs the original attributes to restore on mouseout event
2030
         * @param attrsHover the attributes to set on mouseover event
2031
         */
2032
        setHoverOptions: function (elem, originalAttrs, attrsHover) {
2033
            // Disable transform option on hover for VML (IE<9) because of several bugs
2034
            if (Raphael.type !== "SVG") delete attrsHover.transform;
2035
            elem.attrsHover = attrsHover;
2036

2037
            if (elem.attrsHover.transform) elem.originalAttrs = $.extend({transform: "s1"}, originalAttrs);
2038
            else elem.originalAttrs = originalAttrs;
2039
        },
2040

2041
        /*
2042
         * Set the behaviour when mouse enters element ("mouseover" event)
2043
         * It may be an area, a plot, a link or a legend element
2044
         * @param elem the map element
2045
         */
2046
        elemEnter: function (elem) {
2047
            var self = this;
2048
            if (elem === undefined) return;
2049

2050
            /* Handle mapElem Hover attributes */
2051
            if (elem.mapElem !== undefined) {
2052
                self.animate(elem.mapElem, elem.mapElem.attrsHover, elem.mapElem.attrsHover.animDuration);
2053
            }
2054

2055
            /* Handle textElem Hover attributes */
2056
            if (elem.textElem !== undefined) {
2057
                self.animate(elem.textElem, elem.textElem.attrsHover, elem.textElem.attrsHover.animDuration);
2058
            }
2059

2060
            /* Handle tooltip init */
2061
            if (elem.options && elem.options.tooltip !== undefined) {
2062
                var content = '';
2063
                // Reset classes
2064
                self.$tooltip.removeClass().addClass(self.options.map.tooltip.cssClass);
2065
                // Get content
2066
                if (elem.options.tooltip.content !== undefined) {
2067
                    // if tooltip.content is function, call it. Otherwise, assign it directly.
2068
                    if (typeof elem.options.tooltip.content === "function") content = elem.options.tooltip.content(elem.mapElem);
2069
                    else content = elem.options.tooltip.content;
2070
                }
2071
                if (elem.options.tooltip.cssClass !== undefined) {
2072
                    self.$tooltip.addClass(elem.options.tooltip.cssClass);
2073
                }
2074
                self.$tooltip.html(content).css("display", "block");
2075
            }
2076

2077
            // workaround for older version of Raphael
2078
            if (elem.mapElem !== undefined || elem.textElem !== undefined) {
2079
                if (self.paper.safari) self.paper.safari();
2080
            }
2081
        },
2082

2083
        /*
2084
         * Set the behaviour when mouse moves in element ("mousemove" event)
2085
         * @param elem the map element
2086
         */
2087
        elemHover: function (elem, event) {
2088
            var self = this;
2089
            if (elem === undefined) return;
2090

2091
            /* Handle tooltip position update */
2092
            if (elem.options.tooltip !== undefined) {
2093
                var mouseX = event.pageX;
2094
                var mouseY = event.pageY;
2095

2096
                var offsetLeft = 10;
2097
                var offsetTop = 20;
2098
                if (typeof elem.options.tooltip.offset === "object") {
2099
                    if (typeof elem.options.tooltip.offset.left !== "undefined") {
2100
                        offsetLeft = elem.options.tooltip.offset.left;
2101
                    }
2102
                    if (typeof elem.options.tooltip.offset.top !== "undefined") {
2103
                        offsetTop = elem.options.tooltip.offset.top;
2104
                    }
2105
                }
2106

2107
                var tooltipPosition = {
2108
                    "left": Math.min(self.$map.width() - self.$tooltip.outerWidth() - 5,
2109
                                     mouseX - self.$map.offset().left + offsetLeft),
2110
                    "top": Math.min(self.$map.height() - self.$tooltip.outerHeight() - 5,
2111
                                    mouseY - self.$map.offset().top + offsetTop)
2112
                };
2113

2114
                if (typeof elem.options.tooltip.overflow === "object") {
2115
                    if (elem.options.tooltip.overflow.right === true) {
2116
                        tooltipPosition.left = mouseX - self.$map.offset().left + 10;
2117
                    }
2118
                    if (elem.options.tooltip.overflow.bottom === true) {
2119
                        tooltipPosition.top = mouseY - self.$map.offset().top + 20;
2120
                    }
2121
                }
2122

2123
                self.$tooltip.css(tooltipPosition);
2124
            }
2125
        },
2126

2127
        /*
2128
         * Set the behaviour when mouse leaves element ("mouseout" event)
2129
         * It may be an area, a plot, a link or a legend element
2130
         * @param elem the map element
2131
         */
2132
        elemOut: function (elem) {
2133
            var self = this;
2134
            if (elem === undefined) return;
2135

2136
            /* reset mapElem attributes */
2137
            if (elem.mapElem !== undefined) {
2138
                self.animate(elem.mapElem, elem.mapElem.originalAttrs, elem.mapElem.attrsHover.animDuration);
2139
            }
2140

2141
            /* reset textElem attributes */
2142
            if (elem.textElem !== undefined) {
2143
                self.animate(elem.textElem, elem.textElem.originalAttrs, elem.textElem.attrsHover.animDuration);
2144
            }
2145

2146
            /* reset tooltip */
2147
            if (elem.options && elem.options.tooltip !== undefined) {
2148
                self.$tooltip.css({
2149
                    'display': 'none',
2150
                    'top': -1000,
2151
                    'left': -1000
2152
                });
2153
            }
2154

2155
            // workaround for older version of Raphael
2156
            if (elem.mapElem !== undefined || elem.textElem !== undefined) {
2157
                if (self.paper.safari) self.paper.safari();
2158
            }
2159
        },
2160

2161
        /*
2162
         * Set the behaviour when mouse clicks element ("click" event)
2163
         * It may be an area, a plot or a link (but not a legend element which has its own function)
2164
         * @param elem the map element
2165
         */
2166
        elemClick: function (elem) {
2167
            var self = this;
2168
            if (elem === undefined) return;
2169

2170
            /* Handle click when href defined */
2171
            if (!self.panning && elem.options.href !== undefined) {
2172
                window.open(elem.options.href, elem.options.target);
2173
            }
2174
        },
2175

2176
        /*
2177
         * Get element options by merging default options, element options and legend options
2178
         * @param defaultOptions
2179
         * @param elemOptions
2180
         * @param legendOptions
2181
         */
2182
        getElemOptions: function (defaultOptions, elemOptions, legendOptions) {
2183
            var self = this;
2184
            var options = $.extend(true, {}, defaultOptions, elemOptions);
2185
            if (options.value !== undefined) {
2186
                if ($.isArray(legendOptions)) {
2187
                    for (var i = 0; i < legendOptions.length; ++i) {
2188
                        options = $.extend(true, {}, options, self.getLegendSlice(options.value[i], legendOptions[i]));
2189
                    }
2190
                } else {
2191
                    options = $.extend(true, {}, options, self.getLegendSlice(options.value, legendOptions));
2192
                }
2193
            }
2194
            return options;
2195
        },
2196

2197
        /*
2198
         * Get the coordinates of the text relative to a bbox and a position
2199
         * @param bbox the boundary box of the element
2200
         * @param textPosition the wanted text position (inner, right, left, top or bottom)
2201
         * @param margin number or object {x: val, y:val} margin between the bbox and the text
2202
         */
2203
        getTextPosition: function (bbox, textPosition, margin) {
2204
            var textX = 0;
2205
            var textY = 0;
2206
            var textAnchor = "";
2207

2208
            if (typeof margin === "number") {
2209
                if (textPosition === "bottom" || textPosition === "top") {
2210
                    margin = {x: 0, y: margin};
2211
                } else if (textPosition === "right" || textPosition === "left") {
2212
                    margin = {x: margin, y: 0};
2213
                } else {
2214
                    margin = {x: 0, y: 0};
2215
                }
2216
            }
2217

2218
            switch (textPosition) {
2219
                case "bottom" :
2220
                    textX = ((bbox.x + bbox.x2) / 2) + margin.x;
2221
                    textY = bbox.y2 + margin.y;
2222
                    textAnchor = "middle";
2223
                    break;
2224
                case "top" :
2225
                    textX = ((bbox.x + bbox.x2) / 2) + margin.x;
2226
                    textY = bbox.y - margin.y;
2227
                    textAnchor = "middle";
2228
                    break;
2229
                case "left" :
2230
                    textX = bbox.x - margin.x;
2231
                    textY = ((bbox.y + bbox.y2) / 2) + margin.y;
2232
                    textAnchor = "end";
2233
                    break;
2234
                case "right" :
2235
                    textX = bbox.x2 + margin.x;
2236
                    textY = ((bbox.y + bbox.y2) / 2) + margin.y;
2237
                    textAnchor = "start";
2238
                    break;
2239
                default : // "inner" position
2240
                    textX = ((bbox.x + bbox.x2) / 2) + margin.x;
2241
                    textY = ((bbox.y + bbox.y2) / 2) + margin.y;
2242
                    textAnchor = "middle";
2243
            }
2244
            return {"x": textX, "y": textY, "textAnchor": textAnchor};
2245
        },
2246

2247
        /*
2248
         * Get the legend conf matching with the value
2249
         * @param value the value to match with a slice in the legend
2250
         * @param legend the legend params object
2251
         * @return the legend slice matching with the value
2252
         */
2253
        getLegendSlice: function (value, legend) {
2254
            for (var i = 0; i < legend.slices.length; ++i) {
2255
                if ((legend.slices[i].sliceValue !== undefined && value === legend.slices[i].sliceValue) ||
2256
                    ((legend.slices[i].sliceValue === undefined) &&
2257
                        (legend.slices[i].min === undefined || value >= legend.slices[i].min) &&
2258
                        (legend.slices[i].max === undefined || value <= legend.slices[i].max))
2259
                ) {
2260
                    return legend.slices[i];
2261
                }
2262
            }
2263
            return {};
2264
        },
2265

2266
        /*
2267
         * Animated view box changes
2268
         * As from http://code.voidblossom.com/animating-viewbox-easing-formulas/,
2269
         * (from https://github.com/theshaun works on mapael)
2270
         * @param x coordinate of the point to focus on
2271
         * @param y coordinate of the point to focus on
2272
         * @param w map defined width
2273
         * @param h map defined height
2274
         * @param duration defined length of time for animation
2275
         * @param easingFunction defined Raphael supported easing_formula to use
2276
         */
2277
        animateViewBox: function (targetX, targetY, targetW, targetH, duration, easingFunction) {
2278
            var self = this;
2279

2280
            var cx = self.currentViewBox.x;
2281
            var dx = targetX - cx;
2282
            var cy = self.currentViewBox.y;
2283
            var dy = targetY - cy;
2284
            var cw = self.currentViewBox.w;
2285
            var dw = targetW - cw;
2286
            var ch = self.currentViewBox.h;
2287
            var dh = targetH - ch;
2288

2289
            // Init current ViewBox target if undefined
2290
            if (!self.zoomAnimCVBTarget) {
2291
                self.zoomAnimCVBTarget = {
2292
                    x: targetX, y: targetY, w: targetW, h: targetH
2293
                };
2294
            }
2295

2296
            // Determine zoom direction by comparig current vs. target width
2297
            var zoomDir = (cw > targetW) ? 'in' : 'out';
2298

2299
            var easingFormula = Raphael.easing_formulas[easingFunction || "linear"];
2300

2301
            // To avoid another frame when elapsed time approach end (2%)
2302
            var durationWithMargin = duration - (duration * 2 / 100);
2303

2304
            // Save current zoomAnimStartTime before assigning a new one
2305
            var oldZoomAnimStartTime = self.zoomAnimStartTime;
2306
            self.zoomAnimStartTime = (new Date()).getTime();
2307

2308
            /* Actual function to animate the ViewBox
2309
             * Uses requestAnimationFrame to schedule itself again until animation is over
2310
             */
2311
            var computeNextStep = function () {
2312
                // Cancel any remaining animationFrame
2313
                // It means this new step will take precedence over the old one scheduled
2314
                // This is the case when the user is triggering the zoom fast (e.g. with a big mousewheel run)
2315
                // This actually does nothing when performing a single zoom action
2316
                self.cancelAnimationFrame(self.zoomAnimID);
2317
                // Compute elapsed time
2318
                var elapsed = (new Date()).getTime() - self.zoomAnimStartTime;
2319
                // Check if animation should finish
2320
                if (elapsed < durationWithMargin) {
2321
                    // Hold the future ViewBox values
2322
                    var x, y, w, h;
2323

2324
                    // There are two ways to compute the next ViewBox size
2325
                    //  1. If the target ViewBox has changed between steps (=> ADAPTATION step)
2326
                    //  2. Or if the target ViewBox is the same (=> NORMAL step)
2327
                    //
2328
                    // A change of ViewBox target between steps means the user is triggering
2329
                    // the zoom fast (like a big scroll with its mousewheel)
2330
                    //
2331
                    // The new animation step with the new target will always take precedence over the
2332
                    // last one and start from 0 (we overwrite zoomAnimStartTime and cancel the scheduled frame)
2333
                    //
2334
                    // So if we don't detect the change of target and adapt our computation,
2335
                    // the user will see a delay at beginning the ratio will stays at 0 for some frames
2336
                    //
2337
                    // Hence when detecting the change of target, we animate from the previous target.
2338
                    //
2339
                    // The next step will then take the lead and continue from there, achieving a nicer
2340
                    // experience for user.
2341

2342
                    // Change of target IF: an old animation start value exists AND the target has actually changed
2343
                    if (oldZoomAnimStartTime && self.zoomAnimCVBTarget && self.zoomAnimCVBTarget.w !== targetW) {
2344
                        // Compute the real time elapsed with the last step
2345
                        var realElapsed = (new Date()).getTime() - oldZoomAnimStartTime;
2346
                        // Compute then the actual ratio we're at
2347
                        var realRatio = easingFormula(realElapsed / duration);
2348
                        // Compute new ViewBox values
2349
                        // The difference with the normal function is regarding the delta  value used
2350
                        // We don't take the current (dx, dy, dw, dh) values yet because they are related to the new target
2351
                        // But we take the old target
2352
                        x = cx + (self.zoomAnimCVBTarget.x - cx) * realRatio;
2353
                        y = cy + (self.zoomAnimCVBTarget.y - cy) * realRatio;
2354
                        w = cw + (self.zoomAnimCVBTarget.w - cw) * realRatio;
2355
                        h = ch + (self.zoomAnimCVBTarget.h - ch) * realRatio;
2356
                        // Update cw, cy, cw and ch so the next step take animation from here
2357
                        cx = x;
2358
                        dx = targetX - cx;
2359
                        cy = y;
2360
                        dy = targetY - cy;
2361
                        cw = w;
2362
                        dw = targetW - cw;
2363
                        ch = h;
2364
                        dh = targetH - ch;
2365
                        // Update the current ViewBox target
2366
                        self.zoomAnimCVBTarget = {
2367
                            x: targetX, y: targetY, w: targetW, h: targetH
2368
                        };
2369
                    } else {
2370
                        // This is the classical approach when nothing come interrupting the zoom
2371
                        // Compute ratio according to elasped time and easing formula
2372
                        var ratio = easingFormula(elapsed / duration);
2373
                        // From the current value, we add a delta with a ratio that will leads us to the target
2374
                        x = cx + dx * ratio;
2375
                        y = cy + dy * ratio;
2376
                        w = cw + dw * ratio;
2377
                        h = ch + dh * ratio;
2378
                    }
2379

2380
                    // Some checks before applying the new viewBox
2381
                    if (zoomDir === 'in' && (w > self.currentViewBox.w || w < targetW)) {
2382
                        // Zooming IN and the new ViewBox seems larger than the current value, or smaller than target value
2383
                        // We do NOT set the ViewBox with this value
2384
                        // Otherwise, the user would see the camera going back and forth
2385
                    } else if (zoomDir === 'out' && (w < self.currentViewBox.w || w > targetW)) {
2386
                        // Zooming OUT and the new ViewBox seems smaller than the current value, or larger than target value
2387
                        // We do NOT set the ViewBox with this value
2388
                        // Otherwise, the user would see the camera going back and forth
2389
                    } else {
2390
                        // New values look good, applying
2391
                        self.setViewBox(x, y, w, h);
2392
                    }
2393

2394
                    // Schedule the next step
2395
                    self.zoomAnimID = self.requestAnimationFrame(computeNextStep);
2396
                } else {
2397
                    /* Zoom animation done ! */
2398
                    // Perform some cleaning
2399
                    self.zoomAnimStartTime = null;
2400
                    self.zoomAnimCVBTarget = null;
2401
                    // Make sure the ViewBox hits the target!
2402
                    if (self.currentViewBox.w !== targetW) {
2403
                        self.setViewBox(targetX, targetY, targetW, targetH);
2404
                    }
2405
                    // Finally trigger afterZoom event
2406
                    self.$map.trigger("afterZoom", {
2407
                        x1: targetX, y1: targetY,
2408
                        x2: (targetX + targetW), y2: (targetY + targetH)
2409
                    });
2410
                }
2411
            };
2412

2413
            // Invoke the first step directly
2414
            computeNextStep();
2415
        },
2416

2417
        /*
2418
         * requestAnimationFrame/cancelAnimationFrame polyfill
2419
         * Based on https://gist.github.com/jlmakes/47eba84c54bc306186ac1ab2ffd336d4
2420
         * and also https://gist.github.com/paulirish/1579671
2421
         *
2422
         * _requestAnimationFrameFn and _cancelAnimationFrameFn hold the current functions
2423
         * But requestAnimationFrame and cancelAnimationFrame shall be called since
2424
         * in order to be in window context
2425
         */
2426
        // The function to use for requestAnimationFrame
2427
        requestAnimationFrame: function(callback) {
2428
            return this._requestAnimationFrameFn.call(window, callback);
2429
        },
2430
        // The function to use for cancelAnimationFrame
2431
        cancelAnimationFrame: function(id) {
2432
            this._cancelAnimationFrameFn.call(window, id);
2433
        },
2434
        // The requestAnimationFrame polyfill'd function
2435
        // Value set by self-invoking function, will be run only once
2436
        _requestAnimationFrameFn: (function () {
2437
            var polyfill = (function () {
2438
                var clock = (new Date()).getTime();
2439

2440
                return function (callback) {
2441
                    var currentTime = (new Date()).getTime();
2442

2443
                    // requestAnimationFrame strive to run @60FPS
2444
                    // (e.g. every 16 ms)
2445
                    if (currentTime - clock > 16) {
2446
                        clock = currentTime;
2447
                        callback(currentTime);
2448
                    } else {
2449
                        // Ask browser to schedule next callback when possible
2450
                        return setTimeout(function () {
2451
                            polyfill(callback);
2452
                        }, 0);
2453
                    }
2454
                };
2455
            })();
2456

2457
            return window.requestAnimationFrame ||
2458
                window.webkitRequestAnimationFrame ||
2459
                window.mozRequestAnimationFrame ||
2460
                window.msRequestAnimationFrame ||
2461
                window.oRequestAnimationFrame ||
2462
                polyfill;
2463
        })(),
2464
        // The CancelAnimationFrame polyfill'd function
2465
        // Value set by self-invoking function, will be run only once
2466
        _cancelAnimationFrameFn: (function () {
2467
            return window.cancelAnimationFrame ||
2468
                window.webkitCancelAnimationFrame ||
2469
                window.webkitCancelRequestAnimationFrame ||
2470
                window.mozCancelAnimationFrame ||
2471
                window.mozCancelRequestAnimationFrame ||
2472
                window.msCancelAnimationFrame ||
2473
                window.msCancelRequestAnimationFrame ||
2474
                window.oCancelAnimationFrame ||
2475
                window.oCancelRequestAnimationFrame ||
2476
                clearTimeout;
2477
        })(),
2478

2479
        /*
2480
         * SetViewBox wrapper
2481
         * Apply new viewbox values and keep track of them
2482
         *
2483
         * This avoid using the internal variable paper._viewBox which
2484
         * may not be present in future version of Raphael
2485
         */
2486
        setViewBox: function(x, y, w, h) {
2487
            var self = this;
2488
            // Update current value
2489
            self.currentViewBox.x = x;
2490
            self.currentViewBox.y = y;
2491
            self.currentViewBox.w = w;
2492
            self.currentViewBox.h = h;
2493
            // Perform set view box
2494
            self.paper.setViewBox(x, y, w, h, false);
2495
        },
2496

2497
        /*
2498
         * Animate wrapper for Raphael element
2499
         *
2500
         * Perform an animation and ensure the non-animated attr are set.
2501
         * This is needed for specific attributes like cursor who will not
2502
         * be animated, and thus not set.
2503
         *
2504
         * If duration is set to 0 (or not set), no animation are performed
2505
         * and attributes are directly set (and the callback directly called)
2506
         */
2507
        // List extracted from Raphael internal vars
2508
        // Diff between Raphael.availableAttrs  and  Raphael._availableAnimAttrs
2509
        _nonAnimatedAttrs: [
2510
            "arrow-end", "arrow-start", "gradient",
2511
            "class", "cursor", "text-anchor",
2512
            "font", "font-family", "font-style", "font-weight", "letter-spacing",
2513
            "src", "href", "target", "title",
2514
            "stroke-dasharray", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit"
2515
        ],
2516
        /*
2517
         * @param element Raphael element
2518
         * @param attrs Attributes object to animate
2519
         * @param duration Animation duration in ms
2520
         * @param callback Callback to eventually call after animation is done
2521
         */
2522
        animate: function(element, attrs, duration, callback) {
2523
            var self = this;
2524
            // Check element
2525
            if (!element) return;
2526
            if (duration > 0) {
2527
                // Filter out non-animated attributes
2528
                // Note: we don't need to delete from original attribute (they won't be set anyway)
2529
                var attrsNonAnimated = {};
2530
                for (var i=0 ; i < self._nonAnimatedAttrs.length ; i++) {
2531
                    var attrName = self._nonAnimatedAttrs[i];
2532
                    if (attrs[attrName] !== undefined) {
2533
                        attrsNonAnimated[attrName] = attrs[attrName];
2534
                    }
2535
                }
2536
                // Set non-animated attributes
2537
                element.attr(attrsNonAnimated);
2538
                // Start animation for all attributes
2539
                element.animate(attrs, duration, 'linear', function() {
2540
                    if (callback) callback();
2541
                });
2542
            } else {
2543
                // No animation: simply set all attributes...
2544
                element.attr(attrs);
2545
                // ... and call the callback if needed
2546
                if (callback) callback();
2547
            }
2548
        },
2549

2550
        /*
2551
         * Check for Raphael bug regarding drawing while beeing hidden (under display:none)
2552
         * See https://github.com/neveldo/jQuery-Mapael/issues/135
2553
         * @return true/false
2554
         *
2555
         * Wants to override this behavior? Use prototype overriding:
2556
         *     $.mapael.prototype.isRaphaelBBoxBugPresent = function() {return false;};
2557
         */
2558
        isRaphaelBBoxBugPresent: function() {
2559
            var self = this;
2560
            // Draw text, then get its boundaries
2561
            var textElem = self.paper.text(-50, -50, "TEST");
2562
            var textElemBBox = textElem.getBBox();
2563
            // remove element
2564
            textElem.remove();
2565
            // If it has no height and width, then the paper is hidden
2566
            return (textElemBBox.width === 0 && textElemBBox.height === 0);
2567
        },
2568

2569
        // Default map options
2570
        defaultOptions: {
2571
            map: {
2572
                cssClass: "map",
2573
                tooltip: {
2574
                    cssClass: "mapTooltip"
2575
                },
2576
                defaultArea: {
2577
                    attrs: {
2578
                        fill: "#343434",
2579
                        stroke: "#5d5d5d",
2580
                        "stroke-width": 1,
2581
                        "stroke-linejoin": "round"
2582
                    },
2583
                    attrsHover: {
2584
                        fill: "#f38a03",
2585
                        animDuration: 300
2586
                    },
2587
                    text: {
2588
                        position: "inner",
2589
                        margin: 10,
2590
                        attrs: {
2591
                            "font-size": 15,
2592
                            fill: "#c7c7c7"
2593
                        },
2594
                        attrsHover: {
2595
                            fill: "#eaeaea",
2596
                            "animDuration": 300
2597
                        }
2598
                    },
2599
                    target: "_self",
2600
                    cssClass: "area"
2601
                },
2602
                defaultPlot: {
2603
                    type: "circle",
2604
                    size: 15,
2605
                    attrs: {
2606
                        fill: "#0088db",
2607
                        stroke: "#fff",
2608
                        "stroke-width": 0,
2609
                        "stroke-linejoin": "round"
2610
                    },
2611
                    attrsHover: {
2612
                        "stroke-width": 3,
2613
                        animDuration: 300
2614
                    },
2615
                    text: {
2616
                        position: "right",
2617
                        margin: 10,
2618
                        attrs: {
2619
                            "font-size": 15,
2620
                            fill: "#c7c7c7"
2621
                        },
2622
                        attrsHover: {
2623
                            fill: "#eaeaea",
2624
                            animDuration: 300
2625
                        }
2626
                    },
2627
                    target: "_self",
2628
                    cssClass: "plot"
2629
                },
2630
                defaultLink: {
2631
                    factor: 0.5,
2632
                    attrs: {
2633
                        stroke: "#0088db",
2634
                        "stroke-width": 2
2635
                    },
2636
                    attrsHover: {
2637
                        animDuration: 300
2638
                    },
2639
                    text: {
2640
                        position: "inner",
2641
                        margin: 10,
2642
                        attrs: {
2643
                            "font-size": 15,
2644
                            fill: "#c7c7c7"
2645
                        },
2646
                        attrsHover: {
2647
                            fill: "#eaeaea",
2648
                            animDuration: 300
2649
                        }
2650
                    },
2651
                    target: "_self",
2652
                    cssClass: "link"
2653
                },
2654
                zoom: {
2655
                    enabled: false,
2656
                    minLevel: 0,
2657
                    maxLevel: 10,
2658
                    step: 0.25,
2659
                    mousewheel: true,
2660
                    touch: true,
2661
                    animDuration: 200,
2662
                    animEasing: "linear",
2663
                    buttons: {
2664
                        "reset": {
2665
                            cssClass: "zoomButton zoomReset",
2666
                            content: "&#8226;", // bullet sign
2667
                            title: "Reset zoom"
2668
                        },
2669
                        "in": {
2670
                            cssClass: "zoomButton zoomIn",
2671
                            content: "+",
2672
                            title: "Zoom in"
2673
                        },
2674
                        "out": {
2675
                            cssClass: "zoomButton zoomOut",
2676
                            content: "&#8722;", // minus sign
2677
                            title: "Zoom out"
2678
                        }
2679
                    }
2680
                }
2681
            },
2682
            legend: {
2683
                redrawOnResize: true,
2684
                area: [],
2685
                plot: []
2686
            },
2687
            areas: {},
2688
            plots: {},
2689
            links: {}
2690
        },
2691

2692
        // Default legends option
2693
        legendDefaultOptions: {
2694
            area: {
2695
                cssClass: "areaLegend",
2696
                display: true,
2697
                marginLeft: 10,
2698
                marginLeftTitle: 5,
2699
                marginBottomTitle: 10,
2700
                marginLeftLabel: 10,
2701
                marginBottom: 10,
2702
                titleAttrs: {
2703
                    "font-size": 16,
2704
                    fill: "#343434",
2705
                    "text-anchor": "start"
2706
                },
2707
                labelAttrs: {
2708
                    "font-size": 12,
2709
                    fill: "#343434",
2710
                    "text-anchor": "start"
2711
                },
2712
                labelAttrsHover: {
2713
                    fill: "#787878",
2714
                    animDuration: 300
2715
                },
2716
                hideElemsOnClick: {
2717
                    enabled: true,
2718
                    opacity: 0.2,
2719
                    animDuration: 300
2720
                },
2721
                slices: [],
2722
                mode: "vertical"
2723
            },
2724
            plot: {
2725
                cssClass: "plotLegend",
2726
                display: true,
2727
                marginLeft: 10,
2728
                marginLeftTitle: 5,
2729
                marginBottomTitle: 10,
2730
                marginLeftLabel: 10,
2731
                marginBottom: 10,
2732
                titleAttrs: {
2733
                    "font-size": 16,
2734
                    fill: "#343434",
2735
                    "text-anchor": "start"
2736
                },
2737
                labelAttrs: {
2738
                    "font-size": 12,
2739
                    fill: "#343434",
2740
                    "text-anchor": "start"
2741
                },
2742
                labelAttrsHover: {
2743
                    fill: "#787878",
2744
                    animDuration: 300
2745
                },
2746
                hideElemsOnClick: {
2747
                    enabled: true,
2748
                    opacity: 0.2,
2749
                    animDuration: 300
2750
                },
2751
                slices: [],
2752
                mode: "vertical"
2753
            }
2754
        }
2755

2756
    };
2757

2758
    // Mapael version number
2759
    // Accessible as $.mapael.version
2760
    Mapael.version = version;
2761

2762
    // Extend jQuery with Mapael
2763
    if ($[pluginName] === undefined) $[pluginName] = Mapael;
2764

2765
    // Add jQuery DOM function
2766
    $.fn[pluginName] = function (options) {
2767
        // Call Mapael on each element
2768
        return this.each(function () {
2769
            // Avoid leaking problem on multiple instanciation by removing an old mapael object on a container
2770
            if ($.data(this, pluginName)) {
2771
                $.data(this, pluginName).destroy();
2772
            }
2773
            // Create Mapael and save it as jQuery data
2774
            // This allow external access to Mapael using $(".mapcontainer").data("mapael")
2775
            $.data(this, pluginName, new Mapael(this, options));
2776
        });
2777
    };
2778

2779
    return Mapael;
2780

2781
}));
2782

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.