Создание виджета jQuery UI + OpenLayers + OpenStreetMap

Создание виджета jQuery UI + OpenLayers + OpenStreetMap

Библиотека jQuery UI, выпускаемая под лицензиями MIT и GNU GPL, предназначена для создания динамических интерфейсов в веб-приложениях. Фактически, это расширение библиотеки jQuery, которое приносит в код понятие виджета, анимацию, эффекты и прекрасную инкапсуляцию.

Не меньшей популярностью у веб-разработчиков пользуется библиотека OpenLayers (лицензия типа BSD), которая позволяет очень быстро и легко создать web-интерфейс для отображения картографических материалов. В связке с картографической подложкой от OpenStreetMap (лицензия СС-by-SA 2.0) широко используется в различных проектах (например, карту результатов выборов 2011 в госдуму на территории Москвы подготовили участники проекта NextGIS на площадке Forbes: http://www.forbes.ru/sobytiya/vlast/77338-izbiratelnaya-geografiya-moskvy).

В этой статье я попытаюсь описать, как «обернуть» OpenLayers в виджет jQuery UI. Забегая вперед скажу, что в результате у Вас получится очень компактный javascript-код с минимальным кодом на странице (я вижу в этом больше плюсов, чем минусов; к тому же руководство yahoo подтверждает это http://developer.yahoo.com/performance/rules.html#external).

Результатом должен стать виджет jQuery UI, реализующий картографическое приложение на базе OpenLayers и OpenStreetMap.

Часть 1. Создание виджета при помощи jQuery UI и ASP.NET MVC.
Часть 2. Создание приложения при помощи jQuery без jQuery UI.

Подготовка

Нам понадобятся следующие инструменты:

vs2010.project

Создаем проект ASP.NET MVC 3 Internet Application, добавляем в него jQuery, jQuery UI и OpenLayers.

В качестве веб-сервера используем IIS Express (подробнее об этом в блоге Scott Guthrie).

Убираем из проекта все, что связано с авторизацией пользователя (нам это не понадобится), а так же добавляем контроллер MapController, представление для действия Index() и пункт меню с ссылкой на это действие. В мастер-страницу так же добавляем ContentPlaceHolder внутрь тэга HEAD.

Файл /Views/Shared/Site.Master

<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<!DOCTYPE html>
<html>
<head runat="server">
    <title><asp:ContentPlaceHolder ""ID"" ="TitleContent" runat="server" /></title>
    <link href="<%: Url.Content("~/Content/site.css") %>" rel="stylesheet" type="text/css" media="all" />
    <script src="<%: Url.Content("~/Scripts/jquery-1.7.1.min.js") %>" type="text/javascript"></script>
    <asp:ContentPlaceHolder ""ID"" ="HeaderContent" runat="server" />
</head>
<body>
    <div class="page">
        <div id="header">
            <div id="title">
                <h1>My MVC Application</h1>
            </div>
            <div id="menucontainer">
                <ul id="menu">
                    <li><%: Html.ActionLink("Home", "Index", "Home")%></li>
                    <li><%: Html.ActionLink("Map", "Index", "Map")%></li>
                    <li><%: Html.ActionLink("About", "About", "Home")%></li>
                </ul>
            </div>
            <div style="clear:both"></div>
        </div>
        <div id="main">
            <asp:ContentPlaceHolder ""ID"" ="MainContent" runat="server" />
            <div id="footer">
            </div>
        </div>
    </div>
</body>
</html>

Файл /Views/Map/Index.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
<asp:Content ""ID"" ="Content1" ""ContentPlaceHolderID"" ="TitleContent" runat="server">
    OpenLayers Map
</asp:Content>

<asp:Content ""ID"" ="Content2" ""ContentPlaceHolderID"" ="MainContent" runat="server">
    <h2>OpenLayers Map</h2>
    <div id="map" class="ui-map-wrapper"></div>
</asp:Content>

<asp:Content ""ID"" ="Content3" ""ContentPlaceHolderID"" ="HeaderContent" runat="server">
    <link href="<%: Url.Content("~/Content/themes/base/jquery-ui.css") %>" rel="stylesheet" type="text/css" media="all" />
    <script src="<%: Url.Content("~/Scripts/jquery-ui-1.8.16.min.js") %>" type="text/javascript"></script>
    <script src="<%: Url.Content("~/Scripts/OpenLayers-2.11.min.js") %>" type="text/javascript"></script>
    <style type="text/css">
        .ui-map-wrapper {
            height: 400px;
            background-color: #ccc
        }
    </style>
    <script type="text/javascript">
        jQuery(document).ready(function ($) {
            // todo: javascript here
            alert('Hello world!');
        });
    </script>
</asp:Content>

Комментария тут заслуживает разве что код jQuery — функция обратного вызова выполнится в тот момент, когда дерево DOM будет построено в браузере, в отличие от функции load(), которая запускается тогда, когда браузер не только построит дерево страницы, но и загрузит весь контент (изображения, etc). Синтаксис выражения позволяет избежать конфликтов с другими библиотеками из-за символа $. Внутри callback’a вы можете использовать $ как alias к jQuery (одно НО: Visual Studio 2010 SP1 пока не может распознать, что $ это синоним jQuery, а потому внутри функции не работает IntelliSense).

Самое время запустить проект и убедиться, что все работает.

Создание виджета jQuery UI

Файлы CSS и JavaScript в проекте я делю на 2 категории — исходные и минифицированные. Исходные файлы содержат в себе пробелы, отступы, комментарии, переходы на новую строку и прочие вещи, на которые браузер плевал с высокой колокольни — ему они не важны, а значит файл в среднем на 40% состоит из абсолютно бесполезного контента (для браузера). Минификацию файлов проводят специальными программами — хороший пример Microsoft Ajax Minifier и Yahoo YUI Compressor. Минификация — тема отдельного поста, поэтому тут буквально пара фраз, почему файлы поименованы именно так, а не иначе.

Исходные файлы имеют формат 00-name.src.js, а минифицированныеname.min.js.

Добавим в проект файл /Scripts/25-jquery.ui.openMap.src.js, делаем ссылку на него со страницы карты и добвляем в него следующий код.

/// <reference path="jquery-1.7.1-vsdoc.js"/>
/// <reference path="OpenLayers-2.11.min.js"/>

(function ($) {
    $.widget("ui.openMap", {
        // These options will be used as defaults
        options: {
            message: 'Hello world!'
        },
        // Set up the widget
        _create: function () {
            alert(this.options.message);
        },
        // Use the _setOption method to respond to changes to options
        _setOption: function (key, value) {
            this._super(key, value);
        },
        // Use the _destroy method to clean up any modifications your widget has made to the DOM
        _destroy: function () {
        }
    });
}(jQuery));

В javascript-файлах используем комментарий /// для поддержки IntelliSense, что очень удобно (опять же в IS нет поддержки scope, но надеюсь со временем она появится).

Важно знать, что методы виджета разделяются на открытые (public) и закрытые (private), последние начинаются со знака «_» и не вызываются извне. Метод _create() является конструктором виджета и всегда идет в паре со свойством options — опции задаются через параметр конструктора и заменяют установленные по-умолчанию, хотя и есть возможность изменить параметры уже после создания виджета. Документация по виджетам на официальном сайте http://wiki.jqueryui.com/w/page/12138135/Widget%20factory, а исходный код стандартных виджетов лежит на GitHub https://github.com/jquery/jquery-ui.

Вызываем наш самый простой виджет со страницы /Views/Map/Index.aspx так

<script type="text/javascript">
        jQuery(document).ready(function ($) {
            // todo: javascript here
            //alert('Hello world!');
            $('#map').openMap({
                message: 'Hello world2!'
            });
        });
    
</script>

Конструкция $(‘#map’) является селектором jQuery и находит элемент в DOM с идентификатором «map» (в нашем случае это div, см. код страницы Index.aspx). Таким образом виджет openMap связан с неким элементом в DOM-дереве (а иногда и с несколькими элементами, ведь селектор строго говоря возвращает массив).

Время обновить страницу карты, чтобы проверить, все ли работает.

Создание обертки над OpenLayers

Рассказывать об OpenLayers не цель этого поста. Ссылки на документацию можно найти на сайте OSGeo http://trac.osgeo.org/openlayers/wiki/Documentation. А на сайте OpenLayers есть коллекция замечательных примеров http://openlayers.org/dev/examples/.

Основываясь на примере с редактором OpenLayers http://openlayers.org/dev/examples/editingtoolbar.html, модифицируем наш виджет так, чтобы реализовать подобный функционал.

/// <reference path="jquery-1.7.1-vsdoc.js"/>
/// <reference path="OpenLayers-2.11.min.js"/>

(function ($) {
    $.widget("ui.openMap", {
        // These options will be used as defaults
        options: {
            name: 'openmap',
            imgPath: '/Content/images/',
            center: {
                lon: 37.62428, // y
                lat: 55.75304, // x
                zoom: 9
            }
        },
        // Private options
        _po: {
            div: null,
            map: null,
            mapnik: null,
            vlayer: null,
            controls: {
                toolbar: null,
                mouseposition: null
            }
        },
        // Set up the widget
        _create: function () {
            this._po.div = $('<div/>').attr('id', this.options.name + '-map-generic')
                .css({
                    width: '100%',
                    height: '100%'
                });
            this.element.append(this._po.div);

            this._initMap();
        },
        // Map initializing
        _initMap: function () {
            var self = this;
            OpenLayers.ImgPath = this.options.imgPath;
            var options = {
                theme: null,
                projection: new OpenLayers.Projection("EPSG:900913"), // Spherical Mercator Projection
                displayProjection: new OpenLayers.Projection("EPSG:4326"), // WGS 1984
                units: "m",
                numZoomLevels: 18,
                maxResolution: 156543.0339,
                maxExtent: new OpenLayers.Bounds(-20037508, -20037508, 20037508, 20037508)
            }
            this._po.map = new OpenLayers.Map(this._po.div.attr('id'), options);
            this._po.mapnik = new OpenLayers.Layer.OSM('OSM');
            this._po.vlayer = new OpenLayers.Layer.Vector("Vector");
            this._po.map.addLayers([this._po.mapnik, this._po.vlayer]);

            this._po.controls.toolbar = new OpenLayers.Control.EditingToolbar(this._po.vlayer);
            this._po.controls.mouseposition = new OpenLayers.Control.MousePosition();

            $.each(this._po.controls, function (key, value) {
                self._po.map.addControl(value);
            });

            this._po.map.setCenter(new OpenLayers.LonLat(this.options.center.lon, this.options.center.lat) // Center of the map
                .transform(this._po.map.displayProjection, this._po.map.projection),
                this.options.center.zoom); // zoom level
        },
        // Use the _setOption method to respond to changes to options
        _setOption: function (key, value) {
            this._super(key, value);
        },
        // Use the _destroy method to clean up any modifications your widget has made to the DOM
        _destroy: function () {
        }
    });
}(jQuery));

Обо всем по-порядку.

  1. OpenLayers требует id элемента DOM в виде текста, но у нас в виджете есть только сам элемент DOM this.element — мы могли бы получить его id так this.element.attr(‘id’), но нам никто не гарантирует, что у элемента есть такой атрибут. Поэтому приходится создавать дочерний элемент уже внутри виджета, присваивать ему id и добавлять в него OpenLayers.
  2. По-умолчанию OpenLayers ищет изображения для своих контролов в директории ./img/, а стили — в ./theme/{themename}/ (описание тут http://docs.openlayers.org/library/deploying.html). В нашем случае скрипт лежит в корневой директории /Scripts/, а там создавать папки img и theme не хорошо (можно, но не хорошо). Поэтому при создании объекта карты выставляем theme в null, а свойству OpenLayers.ImgPath присваем путь до изображений. На странице с картой делаем ссылку на css-файл с темой карты, как с обычным css — это подгрузит все необходимые стили.
  3. Проекции. С ними частенько полно проблем, OpenLayer и OpenStreetMap — не исключение. По-умолчанию, OpenLayers использует проекцию EPSG:4326 (http://trac.osgeo.org/openlayers/wiki/FrequentlyAskedQuestions#Projections), к которой привыкли все (WGS 1984, GPS и иже с ними), но OpenStreetMap, как и большинство коммерческих карт, использует проекцию Меркатора (Spherical Mercator Projection, EPSG:900913). Поэтому при создании карты необходимо указать, что сама карта будет в проекции Меркатора, а данные мы в нее будем загружать в проекции EPSG:4326 (например, данные из Google Earth) — за это отвечают свойства displayProjection и projection соответственно.

Вызываем виджет со страницы так

<script type="text/javascript">
        jQuery(document).ready(function ($) {
            $('#map').openMap({
                name: 'mymap',
                imgPath: '/Content/images/map-default/'
            });
        });
    </script>

Вот, что должно получиться
openlayers.default.controls

В принципе, выполненных действий достаточно, чтобы получить базовое картографическое приложение. Но один дополнительный штрих я все же сделаю — стандартные элементы управления OpenLayers не вызывают у меня симпатий.

Изменение темы

Как провести смену темы OpenLayers написано на сайте сервиса MapBox.com http://support.mapbox.com/kb/mapping-101/openlayers-themes.

Тему Dark Theme (двойная лицензия BSD и GNU GPL v2) можно скачать с GitHub https://github.com/developmentseed/openlayers_themes.

how.to.zip.from.github

Распаковываем папку dark в /Content/images/map-dark/ и меняем параметр imgPath нашего виджета на такое же значение, не меняя код самого виджета =).

<script type="text/javascript">
  jQuery(document).ready(function ($) {
    $('#map').openMap({
      name: 'mymap',
      imgPath: '/Content/images/map-dark/'
    });
  });
</script>

Получаем более привлекательные элементы управления
OpenLayers.with.dark.theme

Результат

Если у вас хватило терпения дочитать статью до конца, то, надеюсь, вы поняли, что она не о том, как написать плагин jQuery UI, и не о том, как сделать первое приложение на OpenLayers. Она охватывает много аспектов, которые просто необходимо знать, проектируя динамическое, легко расширяемое приложение (а хорошие ГИС именно такими и должны быть). В статье практически не использовался проект ASP.NET MVC, ведь полученный виджет можно использовать и на обычной html-странице. НО: мы получили прекрасный шаблон для картографического приложения, который позволит быстро создать серверную часть (асинхронную закачку данных при помощи jQuery $.ajax() или OpenLayers.Format.KML), а так же быструю клиентскую часть (при условии минификации javascript- и css-файлов).

Лицензия

Статья, а так же связанные с ней файлы и исходный код доступны по лицензии Apache License 2.0

Исходный код

Исходный код можно посмотреть на github https://github.com/towa48/jQuery.UI.OpenLayersWrapper/