65.9K
CodeProject 正在变化。 阅读更多。
Home

SVG 世界地图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (60投票s)

2011 年 9 月 30 日

CPOL

7分钟阅读

viewsIcon

233307

downloadIcon

6642

使用 SVG 和 jQuery 实现的交互式世界地图。

目录

引言

鉴于技术领域的最新发展(亲爱的未来读者:现在是 2010 年 9 月),我们被大量新闻和事实所淹没,这些新闻和事实表明,Web 开发将从现在开始受到更多关注。越来越多的移动设备(智能手机/平板电脑)和具有可靠稳定宽带互联网访问的便携式设备(笔记本电脑/上网本)证明了这一趋势是合理的。

此外,一些重要的移动浏览器(如 iPad、iPhone 上的 Safari)和 Windows 8 Metro 上的 IE10(请注意,这里我指的是为平板电脑设计的 Metro 界面,而不是用于台式机/笔记本电脑的 Windows 8)不支持 Flash 和 Silverlight 等插件。这意味着,如果您是一名从事 Flash 或 Silverlight 开发的 Web 开发者,并且希望向这些设备交付 Web 应用程序,您将需要重新思考您的策略。Android 平台则没有这个问题,但我们(再次)到达了一个点,即您作为 Web 开发者必须确保您的应用程序能在所有预期的平台上运行。Flash(以及最近的 Silverlight)多年来一直无处不在,这对使用这些技术的开发人员来说是一件好事。在许多方面,这些插件解决了浏览器之间固有的差异问题,因此无论您的应用程序使用什么平台,结果都是一致的。

在花了一些时间学习 HTML5 的基础知识后,我一直在等待合适的时机来做一些与 SVG 相关的事情。虽然这篇文章只是一个将我学到的一些 SVG 理论付诸实践的机会,但幸运的是,我最终创建了一些(希望)对我们 Web 开发者来说可能很有用的东西。

系统要求

为了运行本文附带的 SVG 世界地图示例,您必须使用支持 HTML5 SVG 的浏览器。

  • Internet Explorer 8 或更高版本
  • Firefox
  • Safari
  • Chrome

SVG 的优势

由于我有一些 WPF(Windows Presentation Foundation)和 JavaScript 的背景,我必须承认,我对 SVG "语言"(它与 WPF 几何元素相似)以及操作 Web 元素的方式(当然是 jQuery)感到相当自在。当您能利用先前的知识学习新事物时,感觉是不是很棒?

在这个项目中,我使用了 jQuery SVG,这是 Keith Wood 创建的一个很棒的 jQuery 插件,它允许您以编程方式与 SVG 画布进行交互。

例如,下面的 CIA Factbook 网站就有一个非常漂亮的基于 Flash 的世界地图(免责声明:我与 CIA 没有任何关联)。当您将鼠标悬停在一个国家上时,该国的领土会被高亮显示,并显示一个带有国家名称的小框。这就是我想使用原生 HTML5/SVG/JavaScript 功能创建的应用程序类型,而无需依赖 Flash/Silverlight 等插件基础设施。

为了以编程方式创建一个新的 SVG,我首先从 Wikipedia 下载了一个 SVG 文件(http://upload.wikimedia.org/wikipedia/commons/a/ad/World_map_blank_with_blue_sea.svg)并对其进行了拆解。SVG 中可能有很多元素,但为了简洁起见,我们假设我们使用的特定 SVG 文件包含三个基本元素:SVGgpathSVG 是顶层元素,g 代表 "组"(group)的内部元素,而 path 是包含国家边界复杂几何形状的元素。

jQuery SVG 插件

jQuery SVG 是一个由 Keith Wood 创建的 很棒的插件。通常情况下,您无法使用标准 jQuery 框架对 SVG 进行太多操作。但幸运的是,jQuery SVG 让您的工作变得更加轻松。如果您熟悉 jQuery,那么您就知道如何使用选择器来访问 DOM 元素。

var myDiv = $('#myDiv');

现在,假设您想创建一个全新的 SVG 并将其附加到您的 div。您需要做的就是

var mySVG = $('#myDiv').svg();

从现在开始,您可能希望引用您新创建的 SVG。jQuery SVG 允许您使用类似这样的选择器

var svg = $('#myDiv').svg('get');

最后,您可能想绘制一个带有 3 像素粗细红色描边的黄色圆圈。无需深入 HTML,因为您可以以编程方式完成它。

svg.circle(100, 50, 50, {fill: 'yellow', stroke: 'red', strokeWidth: 3});

逆向工程原始世界地图 SVG 文件

这是项目中真正困难的部分:我将原始的 .svg 文件分解成多个字符串数组(后来存储在 JavaScript 文件中),每个国家一组。我称之为 "逆向工程 SVG"。我全部手动完成,并且没有包含一些国家,所以如果您的国家在地图上找不到,请原谅我(顺便说一句,我可以在文章的后续版本中包含它们)。

例如,下图显示了构成玻利维亚在原始 .svg 文件中的元素。您会注意到 g 元素和 path 元素包含了很多信息。

<g xmlns="http://www.w3.org/2000/svg" class="landxx bo" id="bo" 
  style="fill:#ffffff;fill-opacity:1;stroke:#000000;
         stroke-opacity:1;stroke-width:0.64507740000000002;
         stroke-miterlimit:3.97999999999999998;stroke-dasharray:none">
  <path d="M 742.08629,854.27855 C 743.99329,854.40255 745.90929,854.56755 747.81129,
          854.71055 C 748.68329,854.77555 748.23629,854.92055 748.36629,855.44055 
          C 748.60129,856.38055 749.95729,855.41755 750.33829,855.25255 C 750.77829,
          855.06155 751.65329,854.87955 751.93629,854.43355 C 752.37129,853.74755 752.51029,
          852.77755 753.16229,852.24055 C 754.60629,851.05155 755.75629,852.96855 756.54129,
          851.05155 C 757.22429,849.38255 759.34629,849.30355...">
  <path d="M 750.24716,899.89622 C 750.26405,899.89762 749.7766,
           899.86692 749.13517,899.68022 C 748.83492,
           899.59283 749.36535,898.78795 749.64135,898.64095...>
</g>

上面的 SVG 代码被转换成 JavaScript,形式为一个字符串数组,如下所示:

p = [];
p[p.length] = 'M 742.08629,854.27855 C 743.99329,854.40255 745.90929,
      854.56755 747.81129,854.71055 C 748.68329,854.77555 748.23629,854.92055...
p[p.length] = 'M 750.24716,899.89622 C 750.26405,899.89762 749.7766,
      899.86692 749.13517,899.68022 C 748.83492,899.59283 749.36535,898.78795...

var bo = { pathCollection: p, id: 'bo', translate: [29.90172, 45.07447] };

其中 "bo" 是包含 "玻利维亚" 国家数据的匿名对象。稍后在 JavaScript 文件中,当我们把国家的数组传递给 drawCountries 函数时,就会创建南美洲大陆。

var countries = [ar, bo, br, cl, co, ec, gf, gu, py, pe, sr, uy, ve, gy];
drawCountries(svg, scale, countries);

当运行 drawCountries 函数时,"逆向工程"就完成了。请注意 path.movepath.curveCpath.line 方法,它们分别用于在 SVG 路径中移动、绘制曲线和绘制直线。

function drawCountries(svg, config, countries, translate) {
    //Let's draw one country at a time
    for (var c = 0; c < countries.length; c++) {
        var country = countries[c];
        var g;

        //code removed for sake of brevity

        //Each country has a collection of path. Let's draw them
        for (var i = 0; i < country.pathCollection.length; i++) {
            var splitted = country.pathCollection[i].split(' ');

            var path = svg.createPath();

            var index = 0;
            
            //Here we parse each path
            while (index < splitted.length) {
                var command = splitted[index];

                switch (command) {
                    //M x,y = "Move to point (x,y)", that is, the path
                    // will start at this absolute position (x,y)
                    case 'M':
                        var moveconfig1 = splitted[index + 1].split(',');
                        path.move(moveconfig1[0], moveconfig1[1]);
                        index += 2;
                        break;
                    //C x1,y1 x2,y2 x3,y3 = "Curve (x1,y1,x2,y2,x3,y3)",
                    // and draws a bézier curve using 3 control points (xn,yn)
                    case 'C':
                        var curveCconfig1 = splitted[index + 1].split(',');
                        var curveCconfig2 = splitted[index + 2].split(',');
                        var curveCconfig3 = splitted[index + 3].split(',');
                        path.curveC(curveCconfig1[0], curveCconfig1[1],
                                    curveCconfig2[0], curveCconfig2[1],
                                    curveCconfig3[0], curveCconfig3[1]
                                    );
                        index += 4;
                        break;
                    //L x,y = "Straight Line (x,y)", a line segment starting
                    //at the current position ans ending in (x,y) point
                    case 'L':
                        var lineconfig1 = splitted[index + 1].split(',');
                        path.line(lineconfig1[0], lineconfig1[1]);
                        index += 2;
                        break;
                }
            }

            svg.path(g, path, { id: country.id, countryId: country.id });
        }
        
        //code removed for sake of brevity
    }
}

至此,我们已经重新创建了 SVG。这是最难的部分。现在乐趣才刚刚开始,我们可以与 SVG 及其组件进行交互。

与 SVG 交互

为了监听 SVG 触发的鼠标事件,我们必须 **绑定** 适当的事件。在这个例子中,我们将绑定 mousemovemouseentermouseoutclick 事件。当然,还有更多事件我们可以使用,但这些足以满足我们项目所需的功能。

$(svg.root()).bind('mousemove',
function (path) {
        var offset = $(config.selector).position();
        $('#box').attr('transform', 
          'translate(' + path.pageX + ' ' + path.pageY + ')');
    });

$('#' + country.id, svg.root()).bind('mousemove',
    function (path) {
        var g = path.target.parentNode;

        if (countryBoxFadeOut) {

            if (config.showCountryBoxOnMouserEnter && 
                (lastPoint[0] != path.pageX &&
                lastPoint[1] != path.pageY)) {
                showCountryBox(svg);
            }

            timer = setTimeout(function () {
                if (lastCountryId == currentCountryId) {
                    clearTimeout(timer);
                    hideCountryBox(svg);
                }
            }, 1000);
        }

        lastPoint = [path.pageX, path.pageY];
    });

$('#' + country.id, svg.root()).bind('mouseenter',
    function (path) {

        $('#box').attr('transform', 'translate(' + path.pageX + ' ' + path.pageY + ')');

        var g = path.target.parentNode;

        $(g).attr('opacity', config.activeCountryOpacity);
        $(g).attr('fill', config.activeCountryFill);
        $(g).attr('stroke', config.activeCountryStroke);
        $(g).attr('strokeWidth', config.activeCountryStrokeWidth);

        config.countryId = path.target.id;
        config.pos = [path.pageX, path.pageY];

        lastCountryId = currentCountryId;

        currentCountryId = config.countryId;

        if (config.showCountryBoxOnMouserEnter) {
            showCountryBox(svg);
            var box = $('#box', svg.root());
            $(box).stop();
            $(box).animate({ svgOpacity: 1.0 }, 100);
            var txt = $('#txtBox', svg.root());
            var name = getCountryName(config.countryId).split(',')[0];
            txt[0].textContent = name.toUpperCase();
        }

        timer = setTimeout(function () {
            if (lastCountryId == currentCountryId) {
                clearTimeout(timer);

                if (config.showCountryBoxOnMouserEnter)
                    hideCountryBox(svg);
            }
        }, 1000);

        if (config.onmouseenter) {
            config.onmouseenter(config);
        }
    }
);

$('#' + country.id, svg.root()).bind('mouseout',
function (path) {
    var g = path.target.parentNode;
    $(g).attr('fill', config.inactiveCountryFill);
    $(g).attr('opacity', config.inactiveCountryOpacity);
    $(g).attr('stroke', config.inactiveCountryStroke);
    $(g).attr('strokeWidth', config.inactiveCountryStrokeWidth);
    $('#box').stop();
});

$('#' + country.id, svg.root()).bind('click',
function (path) {
    if (config.onclick) {
        var g = path.target.parentNode;
        
        config.onclick(getCountryName(g.id));
    }
});

一些示例代码

在继续示例代码之前,让我们看看 SVG 世界地图所需的一些配置。

配置选项

  • id: (string) 地图的编程 ID
  • selector: (string) 将附加地图 SVG 元素的 CSS 选择器
  • scale: (number) 用于渲染地图的缩放比例。
  • margin: (string) 地图 SVG 元素的 CSS margin
  • top: (string) 地图 SVG 元素的 CSS 顶部 margin
  • height: (number) 地图 SVG 元素的 CSS height
  • width: (number) 地图 SVG 元素的 CSS width
  • inactiveCountryFill: (string) 用于填充非活动国家的 CSS fill
  • inactiveCountryStroke: (string) 用于描绘非活动国家边框的 CSS stroke
  • inactiveCountryStrokeWidth,: (number) 用于描绘非活动国家边框的 stroke 厚度
  • activeCountryFill: (string) 用于填充活动国家的 CSS fill
  • activeCountryStroke: (string) 用于描绘活动国家边框的 CSS stroke
  • activeCountryStrokeWidth,: (number) 用于描绘活动国家边框的 stroke 厚度
  • showCountryBoxOnMouserEnter: (bool) 确定当用户将鼠标悬停在国家上时是否显示名称框
  • drawNorthAmerica: (bool) 确定是否绘制北美洲
  • drawCentralAmerica: (bool) 确定是否绘制中美洲
  • drawSouthAmerica: (bool) 确定是否绘制南美洲
  • drawEurope: (bool) 确定是否绘制欧洲
  • drawAfrica: (bool) 确定是否绘制非洲
  • drawAsia: (bool) 确定是否绘制亚洲
  • drawOceania: (bool) 确定是否绘制大洋洲
  • drawAntarctic: (bool) 确定是否绘制南极洲

事件

  • onCountryMouseEnter: 当鼠标进入某个国家时触发
  • onCountryMouseMove: 当鼠标在某个国家上移动时触发
  • onCountryMouseOut: 当鼠标离开某个国家时触发
  • onCountryMouseClick: 当鼠标单击某个国家时触发

var wm1 = WorldMap({
    id: 'map1',
    selector: '#svgWorldMap1',
    scale: 0.2,
    margin: '0',
    top: '50',
    height: '300',
    width: '550',
    inactiveCountryFill: '#4af',
    inactiveCountryStroke: '#fff',
    inactiveCountryStrokeWidth: 6,
    showCountryBoxOnMouserEnter: true,
    drawNorthAmerica: true,
    drawCentralAmerica: true,
    drawSouthAmerica: true,
    drawEurope: true,
    drawAfrica: true,
    drawAsia: true,
    drawOceania: true,
    drawAntarctic: true,
    onCountryMouseEnter: function (config) {
        var id = config.countryId;
    },
    onCountryMouseMove: function (config) {
        var id = config.countryId;
    },
    onCountryMouseOut: function (config) {
        var id = config.countryId;
    },
    onCountryMouseClick: function (countryId) {
        var id = countryId;
    }
});

var wm2 = WorldMap({
    id: 'map2',
    selector: '#svgWorldMap2',
    scale: 0.2,
    margin: '0',
    top: '50',
    height: '300',
    width: '550',
    inactiveCountryFill: 'transparent',
    inactiveCountryStroke: '#ccc',
    inactiveCountryStrokeWidth: 4,
    activeCountryFill: 'orange',
    activeCountryStroke: '#ccc',
    activeCountryStrokeWidth: 0,
    showCountryBoxOnMouserEnter: false,
    drawNorthAmerica: true,
    drawCentralAmerica: true,
    drawSouthAmerica: true,
    drawEurope: true,
    drawAfrica: true,
    drawAsia: true,
    drawOceania: true,
    drawAntarctic: true,
    onCountryMouseEnter: function (config) {
        var id = config.countryId;
    },
    onCountryMouseMove: function (config) {
        var id = config.countryId;
    },
    onCountryMouseOut: function (config) {
        var id = config.countryId;
    },
    onCountryMouseClick: function (countryId) {
        var id = countryId;
    }
});

var wm3 = WorldMap({
    id: 'map3',
    selector: '#svgWorldMap3',
    scale: 0.2,
    margin: '0',
    top: '50',
    height: '300',
    width: '550',
    inactiveCountryFill: '#ccc',
    inactiveCountryStroke: 'gray',
    inactiveCountryStrokeWidth: 6,
    activeCountryFill: 'orange',
    activeCountryStroke: 'gray',
    activeCountryStrokeWidth: 6,
    showCountryBoxOnMouserEnter: true,
    drawNorthAmerica: true,
    drawCentralAmerica: true,
    drawSouthAmerica: true,
    drawEurope: true,
    drawAfrica: true,
    drawAsia: true,
    drawOceania: true,
    drawAntarctic: true,
    onCountryMouseEnter: function (config) {
        var id = config.countryId;
    },
    onCountryMouseMove: function (config) {
        var id = config.countryId;
    },
    onCountryMouseOut: function (config) {
        var id = config.countryId;
    },
    onCountryMouseClick: function (countryId) {
        var id = countryId;
    }
});

var wm4 = WorldMap({
    id: 'map4',
    selector: '#svgWorldMap4',
    scale: 0.2,
    margin: '0',
    top: '50',
    height: '300',
    width: '550',
    inactiveCountryFill: '#686',
    inactiveCountryStroke: '#fff',
    inactiveCountryStrokeWidth: 6,
    showCountryBoxOnMouserEnter: false,
    drawNorthAmerica: true,
    drawCentralAmerica: true,
    drawSouthAmerica: true,
    drawEurope: true,
    drawAfrica: true,
    drawAsia: true,
    drawOceania: true,
    drawAntarctic: true,
    onCountryMouseEnter: function (config) {
        var id = config.countryId;
    },
    onCountryMouseMove: function (config) {
        var id = config.countryId;
    },
    onCountryMouseOut: function (config) {
        var id = config.countryId;
    },
    onCountryMouseClick: function (countryId) {
        var id = countryId;
    }
});

最终考虑

至此,我们以 HTML5 SVG 完成了我们的这个小项目。请记住,SVG 是你的朋友,它背后有巨大的潜力,而探索它的机会掌握在你的手中。

代码并非 100% 完善,当然也还有很多有趣的改进空间。您可以随意使用它,如果您这样做了,我会很高兴。

如果您看到了这里,那么非常感谢您的耐心。我希望这篇文章能以某种方式提供信息,或者至少是有趣的。

历史

  • 2011-09-30:初始版本。
© . All rights reserved.