Дьявол в деталях:  интерактивная карта на OpenLayers. Часть 1.

Аватарка пользователя Алевтина Морозова

Поделились опытом создания интерактивной карты на JS-библиотеке OpenLayers. Рассказали, как создать карту, настроить её и сделать маркеры.

Обложка поста Дьявол в деталях:  интерактивная карта на OpenLayers. Часть 1.

Антон Ефременков, Senior web developer ITentika и спикер HolyJS, делится своим опытом создания интерактивной карты на основе библиотеки OpenLayers.

В конце прошлого года в нашу компанию обратился заказчик, которому необходимо было разработать систему для управления транспортной сетью города (автобусы, троллейбусы, трамваи, более экзотический транспорт – всё вместе). Это вполне себе энтерпрайз-проект, с командой разработки более 30 человек, в рамках которого надо было реализовать действительно большое количество задач. Их всех сейчас я перечислять, конечно же, не буду (это слишком долго и уныло). Остановлюсь на одной группе задач – той, что связана с отображением карты города и разными режимами работы с ней.

Что нам нужно было реализовать:

  • отобразить карту города (сам город назвать не смогу – NDA).
  • добавлять на карту остановки ТС (и отображать остановки по заданным координатам)
  • отображать ТС в режиме реального времени
  • прокладывать на карте маршрут, по которому будет идти ТС
  • рисовать на карте геометрические фигуры и искать объекты, попавшие внутрь фигуры
  • выделять на карте объект и видеть сопутствующую информацию

Описание проблемы

На тот момент у нас была более чем хорошая экспертиза, связанная с использованием библиотеки Leaflet для отрисовки различных карт и взаимодействия с ними. И всё было бы хорошо, но…

Наш заказчик – это одна из гос. компаний, и им довольно принципиально, какие именно сторонние программные продукты мы используем. Так получилось, что нпм-пакет лифлета можно спокойно установить и сейчас (берёте и устанавливаете, проблем нет), но если вы захотите почитать документацию по этой библиотеке, вы увидите, что жителям нашей страны этот сайт не доступен.

Дьявол в деталях:  интерактивная карта на OpenLayers. Часть 1. 1

В ходе нашего ресёрча мы отобрали всего 2 кандидата – Mapbox и OpenLayers.

Первый кандидат обладает шикарным функционалом, но, к сожалению, платный – и это, почему-то, очень не понравилось нашему заказчику. Второй кандидат был, своего рода, тёмной лошадкой – ни у кого в компании не было опыта использования этой библиотеки, да к тому же и статистика использования этой библиотеки вызывает один большой вопрос “а что с ними не так?..”

Дьявол в деталях:  интерактивная карта на OpenLayers. Часть 1. 2

Мы изучили примеры, размещённые на сайте OpenLayers, и они показались нам вполне убедительными. В общем, мы поверили в возможности этой библиотеки и в то, что она реально умеет работать с картой, поэтому мы решили сделать PoC с основными фичами именно на OpenLayers.

PoC

Пара слов о том стеке, что мы выбрали. На бэке у нас был .NET и PostgreSQL вместе с PostGIS и pgRouting (для хранения информации об объектах на карте). Карту города нам выдавал MapServer, саму карту импортировали с OpenStreetMap. Ну а для браузерной части мы выбрали Angular и библиотеку OpenLayers (к сожалению, никакой ангулярной обёртки для неё нет).

1. Создание и настройка карты

Если сравнивать OpenLayers с Leaflet, то сразу могу сказать, что в случае работы с OpenLayers кода приходится писать несколько больше, но не криминально.

Как видно на коде ниже, мы можем задать начальную координату для центровки карты и начальный уровень зума. Также мы можем создать любое количество доступных котролов и передать их создаваемой карте (к примеру, мы для всех карт создавали шкалу с масштабом, с помощью всего одной строчки кода).

Для отображения карты города в качестве некой “подложки” мы используем TileLayer, где прописываем url нашего MapServer’a.

Также мы можем использовать дефолтные элементы интерактивности (к примеру, перемещение по карте и зум), дополнив их некоторыми другими, при необходимости.

			const view = new View({
    center: options.mapCenter,
    zoom: options.initialZoomLevel,
});

useGeographic();
const scaleControl = new ScaleLine({ units: 'metric' });

const tileLayer = new TileLayer({
    source: new TileWMS({
        url: environment.mapServerUrl,
        params: {LAYERS: 'default', FORMAT: 'image/png'}
    }),
});

return new Map({
    interactions: defaultInteractions()
                     .extend(options.interactions || []),
    view: view,
    layers: ([tileLayer] as Array)
                .concat(options.vectorLayers || []),
    controls: [scaleControl],
    target: 'ol-map',
});
		

2. Работа с векторными слоями

Мы должны уметь отображать на карте объекты разного типа, и для этого мы решили использовать векторные слои, отображаемые поверх нашей карты-подложки.

Для каждого типа объектов мы используем свой векторный слой – для того, чтобы можно было довольно легко скрыть/отобразить все объекты нужного нам типа. При необходимости, мы можем прописать дженерик-тип для того, чтобы максимально явно указать, для чего именно предназначен слой.

			this.labelVectorSource =
    new VectorSource({ wrapX: false });
const labelLayer =
    new VectorLayer>({
    	source: this.labelVectorSource,
    	style: this.labelStyle,
});


this.roadVectorSource =
    new VectorSource({ wrapX: false });
const roadLayer =
new VectorLayer>({
    source: this.roadVectorSource,
});


this.drawVectorSource = new VectorSource({ wrapX: false });
const drawLayer = new VectorLayer({
    source: this.drawVectorSource,
});
		

3. Стилизация маркеров

Маркер – это точка на карте, имеющая вид какой-либо геометрической фигуры, или же определённой иконки. В OpenLayers у нас есть два базовых варианта стилизации таких маркеров. Для создания маркеров первого типа мы используем Style с такими примитивами как Fill и Stroke, ну а для отображения иконки мы используем Style с объектом Icon.

			new Style({
    image: new Icon({
        height: isBig ? 32 : 20,
        src: `/assets/markers_stop.svg`,
    }),
    zIndex: 5,
});


new Style({
    fill: new Fill({
        color: [64, 169, 255, 0.35],
    }),

    stroke: new Stroke({
        color: 'green',
        width: 2,
        lineDash: [8, 12],
    }),
});
		

4. Рисуем фигуры

Зная координаты, мы можем создать объект фигуры и поместить его на нужный векторный слой. Но есть и более интересный вариант: с помощью Draw – объекта из семейства interactions – мы можем перевести карту в режим рисования геометрических фигур, задав тип нужной нам фигуры. В нашем арсенале есть как геометрические примитивы, так и более сложные фигуры (точка, прямая, ломаная линия, круг, …). А ещё мы можем рисовать правильные многоугольники, передав соответствующую геометрическую функцию.

			this.draw = new Draw({
    source: this.drawVectorSource,
    type: 'LineString',
});


this.map.addInteraction(this.draw);


this.draw = new Draw({
    source: this.drawVectorSource,
    type: 'Polygon',
    geometryFunction: createRegularPolygon(6),
});
		

5. Изменяем фигуры

Отдельно хочу остановиться на одной интерактивности OpenLayers, которая меня действительно приятно удивила. По техническому заданию, мы должны уметь не только рисовать геометрические фигуры, но ещё и изменять их (и сохранять новые координаты, конечно же). В библиотеке Leaflet такой функциональности “из коробки” нет, для этого приходится или использовать дополнительные плагины, или писать свою собственную реализацию. В OpenLayers же это поведение настраивается буквально тремя строчками кода – с помощью объекта Modify.

			this.modifyShapeInteraction = new Modify({
    source: this.drawVectorSource,
    pixelTolerance: 10,
});


return new Map({
    interactions:
        defaultInteractions()
            .extend([this.modifyShapeInteraction]),
    view: view,
    layers: ([...]),
    controls: [scaleControl],
    target: 'ol-map',
});
		

Спустя некоторое время, наш PoC был готов, и результат более чем убедил нас в том, что OpenLayers подходит для наших целей, и все поставленные задачи мы сможем реализовать.

Заранее оговорюсь, что и дизайнер, и большая часть разработчиков нашей команды из Питера, поэтому наши макеты и тестовые координаты представлены на карте именно этого славного города =)

Что ж, мы решили перейти к следующему этапу. И, конечно же, встретили первые проблемы.

Проблемы лейблов

Согласно ТЗ, мы должны уметь отображать маркеры транспортных средств, и они должны были выглядеть так, как показано на картинке слева. Но после первой итерации мы получили совсем другую картинку. Ту, что справа.

Дьявол в деталях:  интерактивная карта на OpenLayers. Часть 1. 3

Нам не удалось ни скруглить рамку, ни наложить тень, ни выставить нормальные отступы… Почему? Потому что OpenLayers рисуют все объектны на canvas’e, а там наши возможности очень и очень ограничены, поскольку большинство CSS-свойств попросту отсутствует.

Что же мы, в итоге, можем задать? Список невелик:

  • Fill и Stroke (примитивы для заливки цветом и отображения рамки)
  • Image (непосредственно, иконка маркера на карте)
  • Text (подпись маркера, которой, в том числе, можно навесить свои Fill и Stroke)

С самого начала мы и планировали построить наши маркеры именно вокруг объекта Text. Что ж, не вышло =)

Дизайн этой части системы был уже согласован с заказчиком, поэтому нам пришлось искать решение задачи, без каких-либо надежд обойтись “облегчённой” версией маркеров.

В OpenLayers есть возможность “собрать” визуальное оформление точки на карте из нескольких частей – просто задав массив стилей, применяемых к этой точке. За это мы и зацепились. Теперь у нас несколько стилей, и в совокупности они образуют маркер транспортного средства:

			const textStyle = new Style({
    text: new Text({
        text: `${coordinates.boardNumber}`,
        textAlign: 'left',
        fill: new Fill({ color: '#40A9FF' }),
        font: labelsFont,
        padding: [6, 20, 0, 24],
        offsetX: 24,
        offsetY: 1,
    }),
    zIndex: coordinates.vehicleId,
});
		
			const vehicleMarkerStyle = new Style({
    image: new Icon({
        height: 46,
        width: 46,
        src: '/assets/marker.svg',
        anchor: [0.5, 0.5],
        rotation: coordinates.direction * Math.PI / 180,
    }),
    zIndex: coordinates.vehicleId,
});
		
			const backWidth = textWidth + shadowGap + radiusGap + leftPadding;
const backStyle = new Style({
    image: new Icon({
        height: 56,
        width: backWidth,
        src: '/assets/vehicle-label-back.svg',
        anchor: [0.1, 0.35],
    }),
    zIndex: coordinates.vehicleId,
});
		

Конечно, я не могу сказать, что это самое элегантное решение, и, исходя из способа получения backWidth (ширины нашей подложки, с фоном и тенью), решение выглядит довольно хрупким. Но у него есть два существенных плюса:

  1. Оно работает! (что уже не маловажно)
  2. Нам понятен порядок слоёв, и мы можем им управлять, если это понадобится.

Также к минусам можно отнести некоторые “тормозюльки” при довольно большом количестве ТС на карте. Но об этом чуть позже…)

Так или иначе, мы решили поставленную задачу и добились намеченной цели.

А теперь, дорогой читатель, вопрос к тебе: как думаешь, что могло пойти не так? Да-да, всё верно. У нас случился Его Величество Редизайн. Да, даже на этой стадии проекта. Войдя во вкус, наш заказчик захотел внести “несколько“ изменений в макет. Как видно на картинке ниже, сами маркеры теперь разноцветные (в зависимости от типа ТС), а также внутри такого маркера отображается сопутствующая информация (о статусе ТС, к примеру):

Дьявол в деталях:  интерактивная карта на OpenLayers. Часть 1. 4

Это вылилось в ещё более сложный и хрупкий способ собрать все стили. На картинке ниже можно увидеть, что у нас добавилось логики, ну и увеличилось количество слоёв: :

			const shadowGap = 28;
const radiusGap = 8;
const leftPadding = 24;
const bnsoGap = coords.hasEquipFault ? 22 : 0;
const emergencyGap = 
  coords.vehicleStatus === VehicleStatus.Emergency ? 20 : 0;
const extraGap = bnsoGap && emergencyGap ? 6 : 0;

const backStyle = new Style({
  image: new Icon({
    height: 56,
    width: textWidth + shadowGap + radiusGap + leftPadding 
        + bnsoGap + emergencyGap + extraGap,
    src: '/assets/vehicle-label-back.svg',
    anchor: [0.1, 0.35],
  }),
  zIndex: coords.vehicleId,
});

const styles = [backStyle, textStyle, vehicleMarkerStyle, 
    vehicleIconStyle, bnsoStyle, emergencyStyle, statusStyle];
		

Тем не менее, задача была решена, и мы достигли очень важного этапа жизни нашего проекта: мы окончательно убедились в том, что выбранная библиотека нам подходит, и все поставленные задачи могут быть реализованы с её помощью. Ну а первые результаты можно увидеть на этом видео:

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

Но об этом – во второй части.

Вторая часть https://tproger.ru/articles/dyavol-v-detalyah-interaktivnaya-karta-na-openlayers-chast-2 

JavaScript
Веб-разработка
552