---
title: Веб-карта
---

import { Card, FileTree, LinkCard, CardGrid } from '@astrojs/starlight/components';
import Question from '../../components/Question.astro';
import MultipleChoice from '../../components/MultipleChoice.astro';
import Option from '../../components/Option.astro';

В этой главе мы рассмотрим

- понятие веб-карты
- типы веб-карт
- клиент-серверную архитектуру
- HTML, CSS, JavaScript

В рамках практической части создадим карту мира на основе GeoJSON-файлов с использованием открытой библиотеки MapLibre GL JS. При желании посмотрите [полный код](https://github.com/gtitov/geojson-maplibre-map) и [возможный результат](https://gtitov.github.io/geojson-maplibre-map/).

<LinkCard title='Руки чешутся' href='#создание-первой-веб-карты' description='Сразу к практике'/>

## Карты в Интернете

Любую ли карту в Интернете можно назвать веб-картой? 

Яндекс Карты — да!

Скан карты России, который я отправил по почте — пожалуй, нет.

И между ними ещё огромное множество различных вариантов карт в Интернете.

### Определение веб-карты

Веб-карту определяет цель размещения карты в сети. Если карту разместили в Интернете для того, чтобы пользователи могли работать с ней по сети, то это веб-карта. А файлы карт, передаваемые по сети или размещаемые на сайтах, изображения карт, используемые для оформления веб-страницы, веб-картами не являются.

<Card title="Веб-карта">Карта, предназначенная для использования в сети, называется веб-картой</Card>

### Типы веб-карт

Веб-карты можно разделить на интерактивные / неинтерактивные и статические / динамические

| |статические|динамические|
|-|-|-|
|неинтерактивные|карты-картинки [[↗]](https://platonium238.github.io)|генераторы карт-картинок [[↗]](https://www.worldclim.org/cmip6maps.html)|
|интерактивные|"простые" веб-карты|картографические приложения|

Интерактивность подразумевает возможность пользователя перемещаться по карте, менять масштаб, получать подробную информацию по клику, скрывать слои, менять цвета и так далее.

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

Статические веб-карты получают данные на клиент точно в том виде, в котором они хранятся на сервере, без какой-либо обработки. Из программного обеспечения требуется только веб-сервер, который будет принимать запросы от клиентов и возвращать в ответ файлы данных с сервера клиенту.

Динамические веб-карты подразумевают серверную обработку данных и позволяют запрашивать данные более гибко. Из программного обеспечения обычно используются база данных, программа для обработки данных и веб-сервер. В общем случае веб-сервер примет запрос и обратится к программе для обработки данных, она извлечёт данные из базы, обработает их и передаст веб-серверу, который отправит подготовленные данные клиенту.

Интерактивность касается интерфейса и клиентской части, а деление на статические и динамические веб-карты связано с обработкой данных в серверной части.

<LinkCard title='Другие продукты веб-картографии' href='/extra/web-map-et-al' description='Геопорталы, картографические веб-сервисы, веб-атласы, веб-ГИС'/>

## Клиент-серверная архитектура

Рассмотрим пример работы веб-ресурса.

![alt text](../../assets/image-15.png)

Пользователь вводит адрес запрашиваемого веб-ресурса в адресную строку браузера и нажимает клавишу Enter (1). Браузер инициирует и выполняет запрос к серверу на получение данных для создания веб-страницы (2). Сервер возвращает ответ (3). Из этого ответа браузер формирует веб-страницу, которую видит пользователь (4).

<Card title="Короче">
    Браузер -- это клиент. Сервер -- это сервер. Клиент обращается к серверу с запросом, сервер возвращает клиенту ответ. Вот суть клиент-серверной архитектуры.
</Card>

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

:::tip
Откройте инструменты разработчика, нажав на клавишу F12 или сочетание клавиш Ctrl+Shift+i, перейдите во вкладку Сеть (Network) и обновите страницу, чтобы увидеть, сколько запросов выполняет браузер, чтобы показать вашу любимую веб-страницу.
:::

Первый запрос пользователь выполняет путём ввода адреса. Дальнейшие действия пользователя на веб-странице могут инициировать последующие запросы. Запрашиваться могут как веб-страницы целиком, так и отдельные данные для обновления содержания текущей веб-страницы. Если мы обновляем только часть содержания веб-страницы данными с сервера, то речь идёт об асинхронном запросе.

![*Запросы, выполняемые с веб-страницы*](../../assets/network-tab.png)

*Запросы, выполняемые с веб-страницы*

Веб-карты тоже строятся на основе клиент-серверной архитектуры. Рассмотрим интерактивную статическую карту (пока воображаемую). 

Разметка, стили, логика (программный код) веб-карты, а также наборы пространственных данных веб-карты хранятся на сервере. Пользователь вводит адрес карты. Браузер запрашивает с сервера разметку веб-карты (html). Разметка приходит в браузер. Разметка содержит запросы к стилям и логике веб-карты. Логика веб-карты приходит в браузер и запрашивает наборы пространственных данных для создания веб-карты. Пространственные данные приходят [Взаимодействие с сервером закончено!] и кодом веб-карты превращаются в картографические слои.

<Card title='В клиент-серверной архитектуре браузер это'>
    <MultipleChoice>
      <Option>
        сервер
      </Option>
      <Option isCorrect>
        клиент
      </Option>
      <Option >
        пользователь
      </Option>
    </MultipleChoice>

    <details>
      <summary>Узнать ответ</summary>
      Пользователь использует браузер (клиент), чтобы выполнить запрос к серверу
    </details>
</Card>

## Создание первой веб-карты

Мы рассмотрели интерактивную статическую карту, а сейчас мы её сделаем своими руками!

Выделим нашей первой веб-карте отдельную папку.

> В качестве среды разработки можно использовать [VS Code](https://code.visualstudio.com/), а чтобы запустить сервер -- расширение [Live Server](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer).

### Инициализация карты

Создадим файл разметки `index.html`.

```html title="index.html"
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Население мира</title>
    <!-- Запрашиваем стили 👇 -->
    <link rel="stylesheet" href="style.css">
    <!-- Запрашиваем библиотеку MapLibre 👇 -->
    <script src="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js"></script> 
    <link href="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css" rel="stylesheet" />
</head>

<body>
    <!-- Размечаем контейнер для карты 👇 -->
    <div id="map"></div>
    <!-- Запрашиваем логику карты 👇 -->
    <script src="main.js"></script>
</body>

</html>
```

Библиотеку MapLibre мы запрашиваем из внешнего ресурса, а вот стили и логику карты нам нужно создать.

Создадим файл стилей `style.css`.

```css title="style.css"
/* Объявляем, что контейнер карты должен занимать всю страницу */
#map {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
}
```

И файл с логикой карты `main.js`.

```js title="main.js"
// Инициализируем карту
const map = new maplibregl.Map({
  container: 'map',
  style: "https://raw.githubusercontent.com/gtitov/basemaps/refs/heads/master/positron-nolabels.json",
  center: [51, 0],
  zoom: 4
});
```

> Наименование файла `index.html` важно тем, что именно страница `index.html` загружается при обращении к корневому URL. Наименования файлов CSS и JavaScript особой роли не играют.
>    
> Страница HTML является корневой. Ей необходимо дать информацию о том, какие внешние библиотеки и файлы будут использоваться. Например, `style.css` и `main.js` являются внешними файлами, а MapLibre является внешней библиотекой. Находящиеся на сервере файлы необходимо подключать по URL.

### Запуск локального сервера

После этого запустим Live Server, перейдём по адресу локального сервера и увидим карту.
  
> Live Server обычно запускается по адресу `127.0.0.1:5500`. `127.0.0.1` или `localhost` -- это внутренний адрес сервера на нашем компьютере. Он будет одним и тем же у всех компьютеров. И он недоступен для запросов снаружи. На одном веб-сервере может быть запущено несколько приложений. Для их разграничения используется порт, в нашем случае `5500`.

### Добавление слоёв

Создадим подпапку `data` и загрузим в неё данные о [странах](https://raw.githubusercontent.com/gtitov/geojson-maplibre-map/refs/heads/master/data/countries.geojson), [городах](https://raw.githubusercontent.com/gtitov/geojson-maplibre-map/refs/heads/master/data/cities.geojson), [реках](https://raw.githubusercontent.com/gtitov/geojson-maplibre-map/refs/heads/master/data/rivers.geojson) и [озёрах](https://raw.githubusercontent.com/gtitov/geojson-maplibre-map/refs/heads/master/data/lakes.geojson).

Получится такая структура. HTML отвечает за структуру веб-страницы, CSS за оформление веб-страницы, JavaScript за логику работы веб-страницы. GeoJSON-файлы хранят пространственные данные.

<FileTree>
    - data/               # данные
        - cities.geojson    # города
        - countries.geojson # страны
        - lakes.geojson     # озёра
        - rivers.geojson    # реки
    - index.html          # разметка
    - style.css           # стили
    - main.js             # логика
</FileTree>

Все действия с картой выполняются после первичной загрузки исходной карты.

Добавление картографических слоёв включает два шага: добавление источника данных `addSource` и добавление слоя `addLayer`. На первом шаге указываем, откуда мы будем брать данные, а на втором -- как их оформить. Из одного источника можно создать несколько слоёв.

```js title="main.js"
// Инициализируем карту
// ... этим многоточием отмечен код, который мы уже написали
// в случае сомнений всегда можно свериться с полным кодом веб-карты
// ссылка на него есть в конце упражнения

map.on('load', () => {
    // Выполняется после загрузки карты
    // Добавление источника данных
     map.addSource('countries', {
        type: 'geojson',
        data: './data/countries.geojson',
        attribution: 'Natural Earth'
    })

    // Добавление слоя
    map.addLayer({
        id: 'countries-layer',
        type: 'fill',
        source: 'countries',
        paint: {
            'fill-color': 'lightgray',
        }
    })
})
```

Мы добавили полигональный слой (`type: 'fill'`). Аналогично добавляем слой линий и слой точек.


```js title="main.js"
// Инициализируем карту
...
map.on('load', () => {
    // Выполняется после загрузки карты
    ...
    map.addSource('rivers', {
        type: 'geojson',
        data: './data/rivers.geojson'
    })

    map.addLayer({
        id: 'rivers-layer',
        type: 'line',
        source: 'rivers',
        paint: {
            'line-color': '#00BFFF'
        }
    })

    map.addSource('lakes', {
        type: 'geojson',
        data: './data/lakes.geojson'
    })

    map.addLayer({
        id: 'lakes-layer',
        type: 'fill',
        source: 'lakes',
        paint: {
            'fill-color': 'lightblue',
            'fill-outline-color': '#00BFFF'
        }
    })

    map.addSource('cities', {
        type: 'geojson',
        data: './data/cities.geojson'
    })

    map.addLayer({
        id: 'cities-layer',
        type: 'circle',
        source: 'cities',
        paint: {
            'circle-color': 'rgb(123, 12, 234)',
            'circle-radius': 3
        }
    })
})
```

:::tip
Порядок отрисовки слоёв соответствует порядку их объявления в коде. Последующие слои перекрывают предыдущие.
:::



В MapLibre слои можно фильтровать и оформлять на основе атрибутов с помощью [выражений](https://maplibre.org/maplibre-style-spec/expressions/).

Например, оставим только города с численностью населения больше 1 000 000.

```diff lang="js" title="main.js"
// Инициализируем карту
...
map.on('load', () => {
    // Выполняется после загрузки карты
    ...
    map.addLayer({
        id: 'cities-layer',
        type: 'circle',
        source: 'cities',
        paint: {
            'circle-color': 'rgb(123, 12, 234)',
            'circle-radius': 3
        },
+       filter: ['>', ['get', 'POP_MAX'], 1000000]
    })
})
```

Изобразим красным (`red`) цветом страны, у которых атрибут `MAPCOLOR7` равен 1, а остальные изобразим светло-серым (`lightgray`).

```diff lang="js" title="main.js"
// Инициализируем карту
...
map.on('load', () => {
    // Выполняется после загрузки карты
    ...
    map.addLayer({
        id: 'countries-layer',
        type: 'fill',
        source: 'countries',
        paint: {
-           'fill-color': 'lightgray',
+           'fill-color': ['match', ['get', 'MAPCOLOR7'], 1, 'red', 'lightgray']
        }
    })
    ...
})
```

### Расширение интерактивности

Созданная нами карта сразу даёт пользователю возможности перемещения, зума и даже наклона (попробуйте зажать правую кнопку мыши). Однако чтобы, например, выводить атрибутивные сведения о слое по клику, надо указать это в коде.

Отследим событие клика по слою `cities-layer`. Назовём событие клика переменной `e`. Посмотрим в консоли браузера, что собой представляет это событие. Если мы отслеживаем событие клика по конкретному слою, а не по всей карте, то мы можем обратиться к набору объектов, по которым был выполнен клик `e.features`.

```js title="main.js"
// Инициализируем карту
...
map.on('load', () => {
    // Выполняется после загрузки карты
    ...
    map.on('click', ['cities-layer'], (e) => {
        console.log(e)
        console.log(e.features)
    })
})
```

Закомментируем вывод в консоль и выведем по клику на слой попап.

```js title="main.js"
// Инициализируем карту
...
map.on('load', () => {
    // Выполняется после загрузки карты
    ...
    map.on('click', ['cities-layer'], (e) => {
        // console.log(e)
        // console.log(e.features)
        new maplibregl.Popup() // создадим попап
            .setLngLat(e.features[0].geometry.coordinates) // установим на координатах объекта
            .setHTML(e.features[0].properties.NAME) // заполним  текстом из атрибута с именем объекта
            .addTo(map); // добавим на карту
    })
})
```

Попап отображается, но надо показать пользователю, что на объект можно кликать. При попадании мыши на слой `cities-layer` поменяем курсор на `pointer`, а при покидании слоя `cities-layer` вернём значение по умолчанию.

```js title="main.js"
// Инициализируем карту
...
map.on('load', () => {
    // Выполняется после загрузки карты
    ...
    map.on('mouseenter', 'cities-layer', () => {
        map.getCanvas().style.cursor = 'pointer'
    })
    map.on('mouseleave', 'cities-layer', () => {
        map.getCanvas().style.cursor = ''
    })
})
```

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

```diff lang="js" title="main.js"
// Инициализируем карту
const map = new maplibregl.Map({
  container: 'map',
- style: "https://raw.githubusercontent.com/gtitov/basemaps/refs/heads/master/positron-nolabels.json",
+ style: {
+   "version": 8,
+   "sources": {},
+   "layers": []
+ },
  center: [51, 0],
  zoom: 4
});

map.on('load', () => {
    // Выполняется после загрузки карты
+   map.addLayer({
+       id: 'background',
+       type: 'background',
+       paint: {
+       'background-color': 'lightblue'
+       }
+   })
    ...
})
```

## Что мы получили

У нас получилась отличная карта!

При желании посмотрите [полный код](https://github.com/gtitov/geojson-maplibre-map) и [возможный результат](https://gtitov.github.io/geojson-maplibre-map/).

Откроем вкладку Сеть в инструментах разработчика и ещё разок проследим поток данных

![geojson-network-tab](../../assets/geojson-network-tab.png)

1. Пользователь вводит адрес карты в браузере (в клиенте)
1. Клиент выполняет запрос к серверу по введённому адресу
1. Сервер обрабатывает запрос и возвращает разметку (HTML) (1)
1. В разметке содержатся запросы к оформлению (CSS), картографической библиотеке (MapLibre) и программной логике работы (JavaScript) веб-страницы (2)
1. Клиент (браузер), получив все необходимые сведения, отображает веб-страницу
1. Программная логика работы полученной веб-страницы выполняется и в соответствии с кодом инициирует запросы к данным (GeoJSON) для составления карты (3)
1. Полученные данные оформляются на веб-карте в рамках логики, описанной разработчиком на языке JavaScript, с использованием функций библиотеки MapLibre
1. Пользователь получает веб-карту
1. Веб-карта обогащается дополнительной интерактивностью в рамках описанной разработчиком логики

Такая карта удобна, когда мы показываем небольшое количество объектов. Мы отправляем пользователю данные как они есть. Это требует минимальных серверных мощностей. Подобные веб-ресурсы называются статическими. Их легко разместить в Интернете, есть варианты бесплатной публикации.

*Перед тем как броситься на выполнение упражнений, полезно сделать паузу, выпить чаю и немного почитать*

{/* <CardGrid> */}
    <LinkCard title='Теоретическая база' href='/extra/web-mapping' description='Веб-картографирование и веб-картография'/>
    {/* <LinkCard title='Исторический экскурс' href='/extra/brief-history' description='Краткая история веб-картографии'/> */}
{/* </CardGrid> */}

## Упражнения

1. Покрасьте Москву в красный цвет
2. Выведите в попап один из атрибутов стран
3. Добавьте слой с границами озёр, установите им толщину в 2 пикселя
4. Замените курсор на перекрестие (`crosshair`) при расположении поверх стран
5. ⁎ Внешний вид карты оставляет желать лучшего, оформите карту в соответствии с собственными представлениями о прекрасном

> ⁎ Это упражнение полезно делать для всех карт, которые мы создадим в рамках пособия. Самостоятельная работа над оформлением веб-карты даст ощутимый толчок в понимании процесса веб-картографирования.

## Контрольные вопросы

1. В каком файле описывается разметка веб-страницы?
1. Какой тег содержит ссылку на стили веб-страницы?
1. Что означает `container: 'map'` в коде `new maplibregl.Map({ container: 'map', ... })`?
1. Какой метод библиотеки MapLibre позволяет добавить картографический слой на карту?
1. Какой тип слоя используется для отображения полигонов озёр?
1. Какой текстовый формат пространственных данных может использоваться в качестве источника пространственных данных для библиотеки MapLibre? 