master
gman 1 year ago
parent 78754d2ed1
commit d75642491e

@ -0,0 +1,8 @@
{
"markdown.copyFiles.destination": {
"src/content/docs/**/*": "${documentWorkspaceFolder}/src/assets/${fileName}"
},
"files.associations": {
// "*.mdx": "markdown"
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

@ -5,7 +5,7 @@ title: Веб-картографирование
import { Card, LinkCard } from '@astrojs/starlight/components'; import { Card, LinkCard } from '@astrojs/starlight/components';
import Question from '../../../components/Question.astro' import Question from '../../../components/Question.astro'
Этот раздел содержит краткий *теоретический обзор* сферы веб-картографии В этой главе мы совершим краткий *теоретический обзор* сферы веб-картографии
<LinkCard title='Руки чешутся' href='/chapters/2-webmap#создание-первой-веб-карты' description='Сразу перейти к практическому упражнению'/> <LinkCard title='Руки чешутся' href='/chapters/2-webmap#создание-первой-веб-карты' description='Сразу перейти к практическому упражнению'/>
@ -66,19 +66,3 @@ import Question from '../../../components/Question.astro'
Веб-картография формируется на пересечении сетевых технологий, картографии и геоинформатики изучает особенности оборота пространственных данных в сетевой среде, включая хранение, кодирование, передачу, обработку данных, разработку веб-приложений и другие аспекты. Веб-картография формируется на пересечении сетевых технологий, картографии и геоинформатики изучает особенности оборота пространственных данных в сетевой среде, включая хранение, кодирование, передачу, обработку данных, разработку веб-приложений и другие аспекты.
</Card> </Card>
---
## Литература
1. ГОСТ Р 58570-2019. Инфраструктура Пространственных Данных. Общие Требования. Стандартинформ, 2019. [ссылка](https://docs.cntd.ru/document/1200168445)
1. Абдуллин Р. К., Пономарчук А. И. Технологии интернет-картографирования: учебное пособие / Пермский государственный национальный исследовательский университет. - Пермь, 2020. 132 с.: ил. [ссылка](https://gis.psu.ru/publications/технологии-интернет-картографирован/)
1. Берлянт А. М. Геоинформационное Картографирование. М.: Моск. гос. Ун-т им. М. В. Ломоносова, Рос. акад. естеств. наук, 1997. [ссылка](https://rusneb.ru/catalog/000199_000009_000611203/)
1. Каргашин П. Е. Основы цифровой картографии: Учебное пособие для бакалавров. 5-е изд., перераб. — Москва: Издательско-торговая корпорация Дашков и К, 2023. — 106 с. [ссылка](https://istina.msu.ru/publications/book/557759518/)
1. Лурье И.К., Самсонов Т.Е. Структура и содержание базы пространственных данных для мультимасштабного картографирования // Геодезия и картография. 2010. № 11. С. 17-23. [ссылка](https://istina.msu.ru/publications/article/427465/)
1. Титов Г. С., Прасолова А. И., Каргашин П. Е. Веб-картографирование ресурсов солнечной энергии Якутии // ИнтерКарто. ИнтерГИС. — 2021. — Т. 27, № 3. — С. 210220. [ссылка](https://istina.msu.ru/publications/article/412375618/)
1. Титов Г. С. Текущие проблемы терминологического аппарата отечественной веб-картографии // Геодезия, картография, геоинформатика и кадастры. Производство и образование : Сб. материалов IV Всероссийской науч.-практ. конф. — СПб Политехника: 2021. — С. 317323. [ссылка](https://istina.msu.ru/publications/article/716105082/)
1. Kraak M. J. Web Cartography: Developments and Prospects. Edited by M. J. Kraak and Allan Brown. New York: Taylor & Francis, 2001. [ссылка](https://doi.org/10.1201/9781482289237)
1. Muehlenhaus I. Web Cartography: Map Design for Interactive and Mobile Devices. Boca Raton, FL: CRC Press, 2014. [ссылка](https://doi.org/10.1201/b16229)
1. Neumann A. Web Mapping and Web Cartography. In Springer Handbook of Geographic Information, edited by Wolfgang Kresse and David M. Danko, 27387. Berlin, Heidelberg: Springer Berlin Heidelberg, 2011. [ссылка](https://doi.org/10.1007/978-3-540-72680-7_14)
1. Wind waves web atlas of the russian seas / Myslenkov S., Samsonov T., Shurygina A. et al. // Water. — 2023. — Vol. 15, no. 11. — P. 2036. [ссылка](http://dx.doi.org/10.3390/w15112036)

@ -0,0 +1,320 @@
### Инициализация карты
Создадим файл разметки `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
});
```
После этого запустим Live Server, перейдём по адресу локального сервера и увидим карту.
> Наименование файла `index.html` важно тем, что именно страница `index.html` загружается при обращении к корневому URL. Наименования файлов CSS и JavaScript особой роли не играют.
>
> Страница HTML является ключевой. Ей необходимо дать информацию о том, какие внешние библиотеки и файлы будут использоваться. В частности `style.css` и `main.js` являются внешними файлами. Для локальных файлов мы можем ввести относительные адреса. Удалённые (находящиеся на внешнем сервере) файлы необходимо подключать по URL.
>
> В роли сервера может выступать компьютер, за которым вы работаете. Веб-сервер, запущенный на компьютере, достуен с этого комьютера по IP-адресу `127.0.0.1` или `localhost`. Это внутренний адрес. Он будет одним и тем же у всех компьютеров. И он недоступен для запросов снаружи.
### Добавление слоёв
Создадим подпапку `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
}
})
})
```
В 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. Веб-карта обогащается дополнительной интерактивностью в рамках описанной разработчиком логики
Такая карта удобна, когда немного данных, потому что мы всё переправляем пользователю данные как есть. Когда мы отправляем пользователю данные как есть, почти не требуется серверных мощностей, поэтому для таких карт есть варианты бесплатного размещения в Интернете.
## Упражнения
1. Покрасьте Москву в красный цвет
2. Выведите в попап один из атрибутов стран
3. Добавьте слой с границами озёр, установите им толщину в 2 пикселя
4. Замените курсор на перекрестие (`crosshair`) при расположении поверх стран

@ -5,11 +5,11 @@ title: Веб-карта
import { Card, FileTree, LinkCard } from '@astrojs/starlight/components'; import { Card, FileTree, LinkCard } from '@astrojs/starlight/components';
import Question from '../../../components/Question.astro' import Question from '../../../components/Question.astro'
В этой главе В этой главе мы рассмотрим
- понятие веб-карты - понятие веб-карты
- типы веб-карт - типы веб-карт
- клиент и сервер - клиент-серверную архитектуру
- HTML, CSS, JavaScript - HTML, CSS, JavaScript
В рамках практической части создадим карту мира на основе статических GeoJSON-файлов. В рамках практической части создадим карту мира на основе статических GeoJSON-файлов.

@ -0,0 +1,332 @@
## Заготовка для карты
Попробуем обратиться к публично доступным методам API Google-таблиц, а именно загрузить данные таблицы в формате CSV.
По аналогии с первым упражнением создадим заготовку для карты из файлов `index.html`, `style.css`, `main.js`.
<Tabs>
<TabItem label="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">
<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>
```
</TabItem>
<TabItem label="CSS">
```css title="style.css"
#map {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
```
</TabItem>
<TabItem label="JavaScript">
```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, 37],
zoom: 4
});
```
</TabItem>
</Tabs>
<Card title={'Строка <code>&lt;div id="map">&lt;/div></code> это'}>
<Question answer="контейнер для карты" ballast={['инициализация карты', 'стиль карты']} explanation="Это разметка контейнера карты"/>
</Card>
Удостоверимся, что карта отображается на локальном сервере.
## Обращение к API
Выполним запрос и выведем в консоль ответ
```js title="main.js"
map.on("load", () => {
...
const response = fetch("https://docs.google.com/spreadsheets/d/1f0waZduz5CXdNig_WWcJDWWntF-p5gN2-P-CNTLxEa0/export?format=csv")
console.log(response)
})
```
В консоль вывелся Promise -- обещание того, что браузер уже занимается нашим запросом. Ответ от сервера не вывелся, хотя во вкладке Сеть мы видим, что данные загружены. Дело в том, что вывод в консоль был выполнен раньше, чем мы получили ответ от сервера.
> Браузер начал исполнять наш код, увидел запрос к внешему ресурсу и подумал: "Здесь можно завязнуть. Ещё неизвестно, сколько этот внешний ресурс будет отвечать. Я верну пока обещание, что когда будет ответ, я его предоставлю, и отправлю запрос. А пока жду ответа буду дальше код выполнять."
Запрос к внешнему ресурсу выполняется асинхронно, то есть изымается из последовательного выполнения программного кода и выполняется отдельно. Поэтому вывод в консоль выполняется раньше того, как данные получены.
Чтобы этого не происходило, мы должны в явном виде указать, что код, использующий ответ на запрос, должен выполняться после выполнения запроса. Для этого используем конструкцию `fetch...then`
```js title="main.js"
map.on("load", () => {
...
fetch("https://docs.google.com/spreadsheets/d/1f0waZduz5CXdNig_WWcJDWWntF-p5gN2-P-CNTLxEa0/export?format=csv")
.then((response) => response.text())
.then((csv) => {
console.log(csv)
})
})
```
<details>
<summary>В первой карте тоже был асинхронный код</summary>
Сама карта создаётся асинхронно, поэтому все действия по добавлению слоёв мы выполняем после загрузки карты `map.on('load', () => {})`. Функция, которая вызывается после успешного завершения события называется callback-функцией. Это ещё один вариант работы с асинхронностью. А ещё асинхронно выполняется добавление источников данных `map.addSource`, они же тоже фактически загружаются с сервера. В этом случае библиотека MapLibre сама отслеживает, что код по добавлению источника должен завершиться, прежде чем мы сможем создавать картографические слои `map.addLayer` из этого источника.
</details>
## Преобразование данных
MapLibre не может работать с форматом CSV. Мы должны преобразовать данные в знакомый формат GeoJSON. Сделаем это!
Подключим библиотеку для чтения CSV данных в JS-объект.
```html title="index.html"
<head>
...
<script src="https://unpkg.com/papaparse@5.4.1/papaparse.min.js"></script>
</head>
```
Выполним чтение CSV данных c использованием подключенной библиотеки и сконструируем GeoJSON-объект.
```js title="main.js"
.then((csv) => {
const rows = Papa.parse(csv, { header: true }) // читаем CSV
// console.log(rows) // любуемся
// Формируем объекты GeoJSON
const geojsonFeatures = rows.data.map((row) => {
return {
type: "Feature",
properties: row,
geometry: {
type: "Point",
coordinates: [row.lon, row.lat],
}
}
})
const geojson = {
type: "FeatureCollection",
features: geojsonFeatures
}
})
```
GeoJSON уже можно использовать в качестве источника для MapLibre.
## Работа над картой
У нас есть заготовка, есть данные, самое время заняться картой!
```js title="main.js"
.then((csv) => {
...
const geojson = {
type: "FeatureCollection",
features: geojsonFeatures
}
map.addSource("vacancies", {
type: "geojson",
data: geojson,
cluster: true, // точки будем объединять в кластеры
clusterRadius: 20, // радиус поиска 20 пикселей
});
map.addLayer({
id: "clusters",
source: "vacancies",
type: "circle",
paint: {
"circle-color": "#7EC8E3",
"circle-stroke-width": 1,
"circle-stroke-color": "#FFFFFF",
"circle-radius": [
"step", ["get", "point_count"],
12, // до 3 точек в кластере
3, // --- первое граничное значение
20, // от 3 точек до 6
6, // --- второе граничное значение
30 // больше 6 точек в кластере
],
},
});
map.addLayer({
id: "clusters-labels",
type: "symbol",
source: "vacancies",
layout: {
"text-field": ["get", "point_count"],
"text-size": 10,
},
});
})
```
## Сопутствующие элементы
Чаще всего карту сопровождают дополнительные элементы веб-страницы. Для этой карты мы приведём список всех вакансий и список вакансий, которые пользователь видит на карте при текущем охвате.
Разметим этим спискам место на веб-странице.
```html title="index.html"
<body>
<div id="map"></div>
<div id="list-selected"><h2>Сейчас на карте</h2></div>
<div id="list-all"><h2>Все вакансии</h2></div>
<script src="main.js"></script>
</body>
```
И зададим оформление.
```css title="style.css"
h2 {
margin: 10px;
}
.list-item {
padding: 10px;
}
#map {
position: absolute;
top: 0;
bottom: 0;
right: 300px;
left: 300px;
}
#list-selected {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 300px;
overflow-y: auto;
}
#list-all {
position: absolute;
top: 0;
bottom: 0;
right: 0;
width: 300px;
overflow-y: auto;
}
```
Теперь на нашей веб-странице выделено место под списки. Плюс мы добавили оформление для заголовков второго уровня `h2` и создали класс `.list-item` для будущих элементов списка.
Сначала наполним список всех вакансий. Это нужно сделать единожды.
```js title="main.js"
.then((csv) => {
...
geojson.features.map((f) => {
document.getElementById(
"list-all"
).innerHTML += `<div class="list-item">
<h4>${f.properties["Вакансия"]}</h4>
<a href='#' onclick="map.flyTo({center: [${f.geometry.coordinates}], zoom: 10})">Найти на карте</a>
</div><hr>`;
});
})
```
А список вакансий, которые видит пользователь при заданном охвате карты, надо будет обновлять при каждом перемещении по карте. Мы будем реагировать на окончание перемещения. Ещё одной сложностью является необходимость извлечь из каждого кластера сведения о том, какие объекты в него входят. Со всем этим мы прекрасно справимся.
```js title="main.js"
.then((csv) => {
...
map.on('moveend', () => {
const features = map.queryRenderedFeatures({
layers: ["clusters"]
});
document.getElementById("list-selected").innerHTML = "<h2>Сейчас на карте</h2>"
features.map(f => {
if (f.properties.cluster) {
const clusterId = f.properties.cluster_id;
const pointCount = f.properties.point_count;
map.getSource("vacancies").getClusterLeaves(clusterId, pointCount, 0)
.then((clusterFeatures) => {
clusterFeatures.map((feature) => document.getElementById("list-selected")
.innerHTML += `<div class="list-item">
<h4>${feature.properties["Вакансия"]}</h4>
<a target="blank_" href='${feature.properties["Ссылка на сайте Картетики"]}'>Подробнее</a>
</div><hr>`)
});
} else {
document.getElementById("list-selected")
.innerHTML += `<div class="list-item">
<h4>${f.properties["Вакансия"]}</h4>
<a target="blank_" href='${f.properties["Ссылка на сайте Картетики"]}'>Подробнее</a>
</div><hr>`
}
})
})
})
```
## Пара UX-штрихов
Для удобства пользования картой добавим приближение к карте по клику на объект и изменение курсора при наведении на слой.
```js title="main.js"
map.on("load", () => {
...
map.on("click", "clusters", function (e) {
map.flyTo({ center: e.lngLat, zoom: 8 });
})
map.on("mouseenter", "clusters", function () {
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "clusters", function () {
map.getCanvas().style.cursor = "";
});
})
```
Проделана прекрасная работа!
При желании посмотрите [полный код](https://github.com/gtitov/sheet-maplibre-map) и [возможный результат](https://gtitov.github.io/sheet-maplibre-map/).
## Упражнения
1. Сделайте, чтобы до первого перемещения карты список вакансий "Сейчас на карте" тоже был заполнен
1. Поменяйте местами списки (CSS)
1. Сделайте так, чтобы цвет кластера зависел от количества элементов внутри него
## Чтение
1. Что такое API / Дока [ссылка](https://doka.guide/tools/api/)
1. Асинхронность в JavaScript / Дока [ссылка](https://doka.guide/js/async-in-js/)
1. fetch() / Дока [ссылка](https://doka.guide/js/fetch/)
1. Promise / Дока [ссылка](https://doka.guide/js/promise/)

@ -2,13 +2,13 @@
title: API title: API
--- ---
import { Card, FileTree, LinkCard, Tabs, TabItem } from '@astrojs/starlight/components'; import { Card, FileTree, LinkCard, TabItem, Tabs } from '@astrojs/starlight/components';
import Question from '../../../components/Question.astro' import Question from '../../../components/Question.astro';
В этой главе В этой главе мы рассмотрим
- определение API - определение API
- асинхронность JavaScript - асинхронность JavaScript
@ -362,7 +362,6 @@ map.on("load", () => {
## Чтение ## Чтение
1. ГОСТ Р ИСО/МЭК 24730-1-2017 Информационные технологии. Системы позиционирования в реальном времени (RTLS). Часть 1. Прикладной программный интерфейс (API) [ссылка](https://docs.cntd.ru/document/1200145773)
1. Что такое API / Дока [ссылка](https://doka.guide/tools/api/) 1. Что такое API / Дока [ссылка](https://doka.guide/tools/api/)
1. Асинхронность в JavaScript / Дока [ссылка](https://doka.guide/js/async-in-js/) 1. Асинхронность в JavaScript / Дока [ссылка](https://doka.guide/js/async-in-js/)
1. fetch() / Дока [ссылка](https://doka.guide/js/fetch/) 1. fetch() / Дока [ссылка](https://doka.guide/js/fetch/)

@ -0,0 +1,328 @@
## Подготовка
Создадим папку с заготовкой для нашей карты.
Добавим туда папку `backend`. Загрузим [отсюда](https://github.com/gtitov/flask-maplibre-map/raw/refs/heads/main/backend/cities_index.sqlite) и положим туда базу данных. И создадим в этой папке файл с нашим бэкендом `app.py`.
<FileTree>
- backend/
- app.py
- cities_index.sqlite
- index.html
- style.css
- main.js
</FileTree>
<details>
<summary>Что это за база данных такая -- SQLite</summary>
SQLite -- встраиваемая база данных, которая хранит все свои данные в одном файле. В одном файле хранятся все таблицы, индексы и другая информация о базе данных, что упрощает управление и резервное копирование. Благодаря этому, она не требует отдельного сервера и легко интегрируется в различные приложения.
</details>
## Разработка бэкенда
### Установка Flask
Установим Python и после установки через терминал загрузим в Python библиотеку Flask.
```sh title=Терминал
pip install Flask
```
Теперь откроем ранее созданный нами файл `backend/app.py`. Подключим необходимые библиотеки и создадим объект `app`, в рамках которого мы будем определять доступные методы API.
```py title="app.py"
from flask import Flask, Response
import sqlite3
import json
import time
app = Flask(__name__)
DB_LOCATION = "cities_index.sqlite"
```
### Список городов по году
Добавим первый метод API. Он возвращает пользователю все города за выбранный год. При обращении к этому методу бэкенд выполняет запрос к базе данных и формирует на основе ответа GeoJSON файл, который мы сможем сразу отправить на карту.
> Мы могли бы отправить на фронтенд и неподготовленный файл. Собрать GeoJSON на клиентской стороне, как в карте вакансий, когда Google возвращал нам CSV. Но то был чужой API. А этот наш. И в нашем мы можем сделать так, как будет удобнее нам!
```py title=app.py
...
@app.route("/cities/<year>") # путь API, к которому обращается пользователь
def cities_by_year(year): # функция, которая будет выполняться при обращении
# start_time = time.time()
db = sqlite3.connect(DB_LOCATION) # подключение к базе данных
db.row_factory = sqlite3.Row # указание, что в строках мы будем сохранять название колонки и значение
cursor = db.execute("SELECT * FROM cities WHERE year = ?", (year,)) # выполняем запрос к базе, подставляя год, введённый пользователем
cities = cursor.fetchall() # забираем результат запроса
cursor.close() # закрываем запрос
db.close() # закрываем подключение
geojson = { # приводим к формату GeoJSON
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [row["longitude"], row["latitude"]],
},
"properties": dict(row),
}
for row in cities
],
}
r = Response( # формируем ответ
json.dumps(geojson, ensure_ascii=False), # ensure_ascii=False, чтобы нормально отображалась кириллица
mimetype="application/json" # указываем тип данных
)
# print("--- %s seconds ---" % (time.time() - start_time))
return r
```
Запускаем бэкенд на локальном сервере для проверки. Открываем терминал в папке `backend` и выполняем команду
```sh title=Терминал
flask run --debug
```
После чего увидим что-то вроде
```sh title=Терминал
flask run --debug
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 106-994-678
```
Теперь мы можем обратиться к нашему бэкенду через API по адресу [`http://127.0.0.1:5000/cities/2020`](http://127.0.0.1:5000/cities/2020).
### Сведения о городе по идентификатору
Перейдём ко второму методу. Получим подробные сведения о городе по его идентификатору.
```py title=app.py
@app.route("/city/<id>")
def city_by_id(id):
start_time = time.time()
db = sqlite3.connect(DB_LOCATION)
db.row_factory = sqlite3.Row
cursor = db.execute("SELECT * FROM cities WHERE id = ?", (id,))
city = cursor.fetchone()
cursor.close()
db.close()
r = Response(
json.dumps(dict(city), ensure_ascii=False),
mimetype="application/json",
)
# print("--- %s seconds ---" % (time.time() - start_time))
return r
```
Проверим этот метод [`http://127.0.0.1:5000/city/1000`](http://127.0.0.1:5000/city/1000).
## Разработка фронтенда
### Подключение данных и CORS
Наш бэкенд возвращает данные в формате GeoJSON, поэтому мы можем сразу подключить их в нашу карту.
```js title=main.js
map.on("load", () => {
map.addSource('cities', {
type: 'geojson',
data: "http://localhost:5000/cities/2020" // бэкенд должен быть запущен
});
map.addLayer({
'id': 'cities-layer',
'source': 'cities',
'type': 'circle',
'paint': {
'circle-stroke-width': 1,
'circle-stroke-color': '#FFFFFF',
// SELECT MIN(total_points), MAX(total_points) FROM cities
'circle-color': [
'interpolate',
['linear'],
['get', 'total_points'],
50,
'#d7191c',
150,
'#ffffbf',
250,
'#1a9641'
],
'circle-opacity': 0.8,
// SELECT DISTINCT group_name FROM cities
'circle-radius': [
"match",
['get', 'group_name'],
'Малый город', 3,
'Средний город', 6,
'Большой город', 6,
'Крупный город', 8,
'Крупнейший город', 12,
0 // остальные
]
}
});
})
```
Однако на карте мы не увидим искомых городов. Чтобы узнать почему, проверим вкладку "Сеть" в инструментах разработчика. У запроса к списку городов мы увидим надпись **Ошибка CORS**.
Механизм CORS -- Cross-Origin Resource Sharing -- призван повысить безопасность веб-страницы. Нам, чтобы избежать ошибки CORS, надо указать, что API может отвечать на запросы любых веб-страниц.
> CORS -- это история про *веб-страницы*, поэтому выполняя запросы к API напрямую мы с ней не сталкивались.
```diff lang=py title=app.py
...
@app.route("/cities/<int:year>") # путь API, к которому обращается пользователь
def cities_by_year(year): # функция, которая будет выполняться при обращении
...
r = Response( # формируем ответ
json.dumps(geojson, ensure_ascii=False), # ensure_ascii=False, чтобы нормально отображалась кириллица
mimetype="application/json", # указываем тип данных
+ headers={"Access-Control-Allow-Origin": "*"}
)
# print("--- %s seconds ---" % (time.time() - start_time))
return r
@app.route("/city/<int:id>")
def city_by_id(id):
...
r = Response(
json.dumps(dict(city), ensure_ascii=False),
mimetype="application/json",
+ headers={"Access-Control-Allow-Origin": "*"}
)
# print("--- %s seconds ---" % (time.time() - start_time))
return r
```
После добавления заголовков о том, что API может обслуживать любые веб-страницы, в ответ мы получаем наш список городов в формате GeoJSON.
### Выбор года
Дадим пользователю возможность выбирать год, за который он хочет видеть индекс городов.
Разметим элемент с выпадающем списком.
```diff lang=html title=index.html
<body>
<div id='map'></div>
<dialog id="city-details-modal" onmousedown="this.close()"></dialog>
+ <div>
+ <p class="text-center">Год</p>
+ <select id="year-selector">
+ <option value="2020" selected>2020</option>
+ <option value="2019">2019</option>
+ <option value="2018">2018</option>
+ </select>
+ </div>
<script src="main.js"></script>
</body>
```
Сделаем так, чтобы он выводился поверх карты.
```diff lang=css title=style.css
#map {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
+ z-index: -1;
}
```
Запрограммируем реакцию карты на выбор года. Асинхронный запрос за нас выполнит картографическая библиотека.
```js title=main.js
map.on("load", () => {
...
document.getElementById("year-selector").addEventListener(
'change',
(e) => {
const year = e.target.value // фиксируем выбранный год
map.getSource('cities').setData(`http://localhost:5000/cities/${year}`) // меняем источник данных
}
)
})
```
### Сведения о городе
Выведем по клику на город подробные сведения о нём. Для отображения информации используем модальное окно, то есть диалоговое окно вспывающее поверх страницы.
```diff lang=html title=index.html
<body>
<div id='map'></div>
...
+ <dialog id="city-details-modal" onmousedown="this.close()"></dialog>
<script src="main.js"></script>
</body>
```
При клике будем выполнять запрос к методу API, который возвращает подробную информацию о городе. Здесь мы работаем с асинхронностью самостоятельно.
```js title=main.js
map.on("load", () => {
...
map.on('click', 'cities-layer', (e) => {
// console.log(e.features[0].properties.id)
fetch(`http://localhost:5000/city/${e.features[0].properties.id}`)
.then(response => response.json())
.then(cityProperties => {
// console.log(cityProperties)
document.getElementById("city-details-modal").innerHTML = `<h1>${cityProperties.name}</h1>
<img src="${cityProperties.emblem_url}" height="200">
<h3>Численность населения</h3><h2>${cityProperties.people_count} тыс. чел</h2>
<h3>Индекс качества городской среды</h3><h2>${cityProperties.total_points} / 360</h2>
<hr>
<h3>Жилье и прилегающие пространства</h3><h2>${cityProperties.house_points} / 60</h2>
<h3>Озелененные пространства</h3><h2>${cityProperties.park_points} / 60</h2>
<h3>Общественно-деловая инфраструктура</h3><h2>${cityProperties.business_points} / 60</h2>
<h3>Социально-досуговая инфраструктура</h3><h2>${cityProperties.social_points} / 60</h2>
<h3>Улично-дорожная</h3><h2>${cityProperties.street_points} / 60</h2>
<h3>Общегородское пространство</h3><h2>${cityProperties.common_points} / 60</h2>`
document.getElementById("city-details-modal").showModal() // showModal() -- встроенный метод элемента <dialog>
})
})
map.on('mouseenter', 'cities-layer', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'cities-layer', () => {
map.getCanvas().style.cursor = '';
});
})
```
> Если вы уже заметили кое-какую нестыковку в наших запросах, посмотрите упражнения в конце главы
## Упражнения
1. Создайте метод, который вернёт список доступных годов
1. Выведите модальное окно слева и выполните подлёт к точке клика
1. Вы могли заметить, что то, что мы получаем методом запроса деталей о городе уже содержится в полному списке городов: вам предлагается избавиться от этой избыточности
<details>
<summary>Есть два варианта -- подумайте над ними. Это тест на то, что вам ближе, бэкенд или фронтенд. Когда подумаете, можно посмотреть разгадку 👀</summary>
Бэкендер: можно убрать из метода для списка городов лишние атрибуты<br/>Фронтендер: на клик по объекту можно не обращаться к серверу, а использовать данные из атрибутов объекта
</details>
## Чтение
1. Что такое CORS / Дока [ссылка](https://doka.guide/tools/cors/)
1. Безопасность веб-приложений и распространённые атаки / Дока [ссылка](https://doka.guide/tools/web-security/)

@ -2,9 +2,9 @@
title: Бэкенд title: Бэкенд
--- ---
import { Card, FileTree, LinkCard, Tabs, TabItem } from '@astrojs/starlight/components'; import { Card, FileTree, LinkCard, TabItem, Tabs } from '@astrojs/starlight/components';
В этой главе В этой главе мы рассмотрим
- понятия бэкенда и фронтенда - понятия бэкенда и фронтенда
- SQL - SQL
@ -341,7 +341,7 @@ map.on("load", () => {
1. Выведите модальное окно слева и выполните подлёт к точке клика 1. Выведите модальное окно слева и выполните подлёт к точке клика
1. Вы могли заметить, что то, что мы получаем методом запроса деталей о городе уже содержится в полному списке городов: вам предлагается избавиться от этой избыточности 1. Вы могли заметить, что то, что мы получаем методом запроса деталей о городе уже содержится в полному списке городов: вам предлагается избавиться от этой избыточности
<details> <details>
<summary>Есть два варианта -- подумайте над ними. Это тест на то, что вам ближе, бэкенд или фронтент</summary> <summary>Есть два варианта -- подумайте над ними. Это тест на то, что вам ближе, бэкенд или фронтенд. Когда подумаете, можно посмотреть разгадку 👀</summary>
Бэкендер: можно убрать из метода для списка городов лишние атрибуты<br/>Фронтендер: на клик по объекту можно не обращаться к серверу, а использовать данные из атрибутов объекта Бэкендер: можно убрать из метода для списка городов лишние атрибуты<br/>Фронтендер: на клик по объекту можно не обращаться к серверу, а использовать данные из атрибутов объекта
</details> </details>

@ -0,0 +1,364 @@
---
title: Тайлы
---
import { Card, LinkCard } from '@astrojs/starlight/components';
В этой главе мы рассмотрим
- понятие тайлов
- PostgreSQL + PostGIS
- Martin
## Что такое тайлы
Пространственные данные могут быть большими по объёму. Если пользователь хочет посмотреть на веб-карту передавать ему гигабайты данных, мягко говоря, неоптимально. Это приведёт к длительной загрузке веб-страницы, избыточному трафику, медленной работе веб-карты или падению браузера.
Данные можно поделить на кусочки и передавать пользователю только *нужные кусочки* с *нужной детальностью*. Данные можно поделить на кусочки по-разному. Пространственные данные ожидаемо удобно делить на географические кусочки — тайлы. *Нужные кусочки* -- те, что попадают на экран. *Нужная детальность* -- та, что соответствует текущему масштабу карты.
Стандартная система тайлов делит планету на квадраты X/Y для каждого уровня зума Z. Каждый тайл имеет индекс Z/X/Y. По этому индексу и выполняются запросы тайлов, то есть формируется API сервиса векторных тайлов. Когда запрос выполняется, вызывается серверная функция, формирующая тайл, или возвращается заранее рассчитанный (кэшированный) тайл.
![zxy](../../../assets/zxy.png)
*[By AsPJT](https://commons.wikimedia.org/w/index.php?curid=149301346)*
Тайл привязан к глобальной системе координат одной точкой, геометрии внутри тайла храняться во внутренней системе координат тайла. Тайлы бывают векторными и растровыми. В векторных тайлах содержание одного тайла составляют точки, линии и полигоны, [особым образом](https://docs.mapbox.com/data/tilesets/guides/vector-tiles-standards/#encoding-attributes) кодируются атрибуты. В растровых тайлах содержание одного тайла составляют пиксели.
Растровые тайлы можно использовать как для растровых данных, например, снимков, ЦМР, индексных изображений, так и для векторных, когда на тайлы будет нарезаться подготовленное изображение карты. Векторные тайлы, в большинстве случаев, оказываются удачным решением для векторных наборов данных.
<LinkCard title='Тайлы растровые и векторные' href='/chapters/7-extra/#тайлы-векторные-и-растровые' description='Что лучше 🤨'/>
## Использование тайлов
Познакомимся к возможностями практического применения тайлов. Для этого загрузим наборы пространственных данных в базу пространственных данных. Подключим к ней сервер векторных тайлов. Получим векторные тайлы на клиентской стороне веб-приложения средствами картографической библиотеки.
### База пространственных данных
Используем сервер баз данных Postres с расширением для пространственных данных PostGIS.
#### Установка
Дистрибутив для сервера баз данных можно загрузим [здесь](https://www.postgresql.org/download/). При установке следует обратить внимание на порт, который будет занимать сервер баз данных, и пароль для пользователя `postgres`.
> Сервер баз данных запускается локально. Доступ к локальному серверу баз данных осуществляется по заданному при установке порту -- обычно 5432. Если установщик предлагает другой порт, возможно, что у вас уже установлен сервер баз данных, который этот порт занимает.
После установки сервера баз данных Postgres можно установить расширение для работы с пространственными данными PostGIS. Дистрибутив доступен [тут](https://postgis.net/documentation/getting_started/#installing-postgis).
> Наблюдаются проблемы с установкой PostGIS на Windows через StackBuilder. Рабочим вариантом является самостоятельная загрузка дистрибутива PostGIS [отсюда](https://download.osgeo.org/postgis/windows/). Найдите папку с версией Postgres, которую установили. Например, для Postgres 17 нужна папка `pg17/`, в папке находится дистрибутив `pg17/postgis-bundle-pg17x64-setup-3.5.0-2.exe`.
#### Создание базы данных
Вместе с сервером базы данных устанавливается графический интерфейс для работы с базами данных PgAdmin 4.
> При необходимости, его можно установить [отдельно](https://www.pgadmin.org/download/).
Через этот интерфейс мы создадим базу данных на нашем локальном сервере.
И добавим к созданной базе расширение PostGIS.
#### Загрузка данных
Грузить в базу будем данные об ойконимах Московского региона:
1. точки -- [скачать](https://disk.yandex.ru/d/nYXeBHLxJ0yv-w)
2. агрегирующие их шестиугольники -- [скачать](https://disk.yandex.ru/d/UZZy0xkSuO94-Q)
Сделаем это через [QGIS](https://www.qgis.org/download/).
Добавим слои на карту.
Выполним подключение к базе данных.
Перенесём слои с карты в базу данных.
### Векторные тайлы
Подготовим векторные тайлы на основе данных, загруженных в базу.
#### Сервер векторных тайлов
Для подготовки векторных тайлов используем сервер векторных тайлов Martin. Загрузим это приложение [отсюда](https://github.com/maplibre/martin/releases/tag/v0.14.2).
Распакуем архив и запустим сервер векторных тайлов, указав подключение к локальной базе данных, куда мы загрузили ойконимы.
Перейдём по адресу `localhost:3000/catalog`, чтобы увидеть доступные наборы векторных тайлов. Их должно быть два по количеству пространственных таблиц в базе данных.
По адресам `localhost:3000/grid` и `localhost:3000/oikonyms` доступны описания наборов векторных тайлов в формате [TileJSON](https://github.com/mapbox/tilejson-spec/tree/master/3.0.0). Наиболее существенным в нём является указание адреса, по которому доступны векторные тайлы -- `localhost:3000/grid/{z}/{x}/{y}`
> `/catalog`, `/grid`, `/grid/{z}/{x}/{y}` -- это всё эндпоинты API, которое для нас автоматически формирует Martin. Он же выполняет нужные серверные функции, за счёт которых мы получаем ответы, обращаясь к этим эндпоинтам. И ничего не пришлось писать самим, как в прошлом упражнении!
#### Векторные тайлы на карте
Остаётся принять эти векторные тайлы на карте. Заготовку для карты формируем как обычно.
Добавляем источник пространственных данных и картографический слой. При добавлении источника пространственных данных указываем тип `vector`, а не `geojson`, как в прошлых картах. При добавлении слоя указываем `source-layer` -- векторный тайл может содержать несколько слоёв. В нашем случае слой только один, посмотреть мы на него можем в TileJSON описании `localhost:3000/grid`, где идентификаторы слоёв указываются в обязательном списке `vector_layers`.
```js title=main.js {2, 8}
map.addSource("grid", {
type: "vector",
url: "http://localhost:3000/grid",
})
map.addLayer({
id: "grid-layer",
source: "grid",
"source-layer": "grid",
type: "fill",
paint: {}
})
```
При добавлении источника мы можем указать не только `url` TileJSON описания, но и `tiles` -- список адресов, по которому можно выполнять запросы к тайлам. Если мы указываем `url`, MapLibre самостоятельно находит этот список в TileJSON описании.
```js title=main.js {3}
map.addSource("oikonyms", {
type: "vector",
tiles: ["http://localhost:3000/oikonyms/{z}/{x}/{y}"],
})
map.addLayer({
id: "oikonyms-layer",
source: "oikonyms",
"source-layer": "oikonyms",
type: "circle",
paint: {}
})
```
### Веб-карта
#### Оформление слоёв
```js title=main.js
map.addLayer({
id: "grid-layer",
source: "grid",
"source-layer": "grid",
type: "fill",
paint: {
"fill-color": [
"interpolate",
["linear"],
['to-number', ["get", "sum_pop"]],
0,
"#440154",
100,
"#39568c",
1000,
'#1f968b',
10000,
'#fde725'
]
}
})
```
```js title=main.js
map.addLayer({
id: "oikonyms-layer",
source: "oikonyms",
"source-layer": "oikonyms",
type: "circle",
paint: {
"circle-color": "#1a9641",
"circle-radius": 6,
"circle-stroke-width": 1,
"circle-stroke-color": "#FFF",
"circle-opacity": 0.8
}
})
```
#### Подсветка при наведении
```diff title=main.js
map.addSource("grid", {
type: "vector",
url: "http://localhost:3000/grid",
+ promoteId: "id"
})
```
```js title=main.js
let hoveredFeatureId = null;
map.on("mousemove", "grid-layer", (e) => {
if (hoveredFeatureId !== null) {
map.setFeatureState(
{
source: "grid",
sourceLayer: "grid",
id: hoveredFeatureId
},
{ hover: false }
)
}
hoveredFeatureId = e.features[0].id
map.setFeatureState(
{
source: "grid",
sourceLayer: "grid",
id: hoveredFeatureId
},
{ hover: true }
)
})
map.on("mouseleave", "grid-layer", () => {
map.setFeatureState(
{
source: "grid",
sourceLayer: "grid",
id: hoveredFeatureId
},
{ hover: false }
)
})
```
```diff title=main.js
map.addLayer({
id: "grid-layer",
source: "grid",
"source-layer": "grid",
type: "fill",
paint: {
"fill-color": [
"interpolate",
["linear"],
['to-number', ["get", "sum_pop"]],
0,
"#440154",
100,
"#39568c",
1000,
'#1f968b',
10000,
'#fde725'
],
+ 'fill-outline-color': [
+ 'case',
+ ['boolean', ['feature-state', 'hover'], false],
+ "cyan",
+ "transparent"
+ ]
}
})
```
#### Подлёт при клике
```js title=main.js
map.on("click", "grid-layer", (e) => {
map.flyTo({
center: e.lngLat,
zoom: 10
})
})
map.on('mouseenter', 'grid-layer', () => {
map.getCanvas().style.cursor = 'pointer'
})
map.on('mouseleave', 'grid-layer', () => {
map.getCanvas().style.cursor = ''
})
```
#### Попап при наведении
```js title=main.js
const popup = new maplibregl.Popup({
closeButton: false,
closeOnClick: false
});
map.on('mouseenter', 'oikonyms-layer', (e) => {
popup
.setLngLat(e.features[0].geometry.coordinates)
.setHTML(e.features[0].properties.name)
.addTo(map);
});
map.on('mouseleave', 'oikonyms-layer', () => {
popup.remove();
});
```
#### Фиксированный охват карты
```diff title=main.js
const map = new maplibregl.Map({
container: "map",
style: "https://raw.githubusercontent.com/gtitov/basemaps/refs/heads/master/voyager-nolabels.json",
center: [37, 55],
zoom: 6,
maxZoom: 11,
+ maxBounds: [[25, 50], [50, 60]],
hash: true,
})
```
#### Интерактивная фильтрация
```diff title=index.html
<body>
<div id="map"></div>
+ <input type="number" id="filter" value="12000000"/>
+ <label for="filter">Фильтр по населению</label>
<script src="main.js"></script>
</body>
```
```diff title=style.css
#map {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
+ z-index: -1;
}
```
```js title=main.js
document.getElementById("filter").addEventListener("input", (e) => {
filterValue = parseInt(e.target.value)
map.setFilter("grid-layer", ["<", ["to-number", ["get", "sum_pop"]], filterValue])
})
```
### Растровые тайлы
Загрузим файл с каналом Ландсата на Москву
#### Подготовка набора тайлов
Через QGIS превратим в MBTiles.
В рамках инструмента в MBTiles в растровые тайлы превращается отрисованное изображение, которые мы видим в основном окне QGIS, с применёнными параметрами оформления, а не исходное изображение. Можно добавить к этому изображению и векторные слои.
#### Подключение в Martin
Запустим Martin, подключив его к MBTiles.
Объединим запуск подключения к Postgres и подключения к MBTiles.
Запустим подключение к Postgres с сохранением файла конфигурации.
Затем запустим подключение к MBTiles с сохранением файла конфигурации.
Объединим файлы.
Запустим Martin с применением объединённого файла конфигурации.
#### Растровые тайлы на карте
Для подключения растровых тайлов укажем тип источника `raster`
При добавлении слоя параметры оформления не указываем.
## Упраженения
1. Подсветка при клике

@ -0,0 +1,17 @@
---
title: Заключение
tableOfContents: false
---
<div style="text-align: right; font-style: italic; line-height: 0.5rem">
<p>Вы не устали? Я бы мог</p>
<p>Вести вас дальше тропкой строк</p>
<p>И много новых букв открыть.</p>
<p>Но погодим. Умерим прыть.</p>
<br/>
<p>Доктор Сьюз в переводe </p>
<p>Григория Кружкова</p>
</div>
Не хватало работы с сервером, но это навык, который лучше всего осваивать через самостоятельную практику

@ -14,4 +14,106 @@ title: Внеклассное чтение
Этими терминами злоупотребляют по отношению к любым веб-ресурсам, связанным с пространственными данными и картами. Картографические продукты, публикуемые в сети, правильнее объединить под названием *картографические веб-ресурсы*, так как они являются веб-ресурсами, основным назначением которых является предоставление доступа к картографической информации. Этими терминами злоупотребляют по отношению к любым веб-ресурсам, связанным с пространственными данными и картами. Картографические продукты, публикуемые в сети, правильнее объединить под названием *картографические веб-ресурсы*, так как они являются веб-ресурсами, основным назначением которых является предоставление доступа к картографической информации.
<!-- ## Стиль программирования карты --> ## Зум, детальность, масштаб
| Уровень зума | Детальность | Масштаб |
| ------------ | ----------- | ------------- |
| z0 | 10 000 м | 1:320 000 000 |
| z1 | 5000 м | 1:160 000 000 |
| z2 | 2500 м | 1:80 000 000 |
| z3 | 1250 м | 1:40 000 000 |
| z4 | 600 м | 1:20 000 000 |
| z5 | 300 м | 1:10 000 000 |
| z6 | 150 м | 1:5 000 000 |
| z7 | 80 м | 1:2 500 000 |
| z8 | 40 м | 1:1 250 000 |
| z9 | 20 м | 1:640 000 |
| z10 | 10 м | 1:320 000 |
| z11 | 5 м | 1:160 000 |
| z12 | 2 м | 1:80 000 |
| z13 | 1 м | 1:40 000 |
| z14 | 50 см | 1:20 000 |
| z15 | 25 см | 1:10 000 |
| z16 | 15 см | 1:5000 |
| z17 | 8 см | 1:2500 |
| z18 | 4 см | 1:1250 |
| z19 | 2 см | 1:600 |
| z20 | 1 см | 1:300 |
| ... | ... | ... |
<!-- ## Стиль программирования карты -->
## Тайлы векторные и растровые
Нельзя сказать, что векторные тайлы легче, быстрее, производительнее, тем более лучше, чем растровые тайлы. Хотя некоторые авторы этим и грешат. Например, векторные тайлы, отображающие множество объектов с богатой атрибутикой, могут весит значительно больше растровых тайлов, а сложная, тем более динамическая, стилизация векторных тайлов, может привести к замедлению отрисовки картографического изображения в браузере.
У векторных и растровых тайлов есть свои достоинства и недостатки. Выбор конкретного варианта зависит от отображаемых данных, назначения веб-карты, требований к безопасности данных.
![vector-raster](../../../assets/vector-raster.gif)
Векторные тайлы хорошо подходят для объектно-ориентированного картографирования: изображения точек интереса, дорог, границ. Растровые тайлы незаменимый вариант для отображения спутниковых снимков, непрерывных покрытий, данных без возможности прямого доступа к объектам.
Векторные тайлы нагружают клиентскую часть. Растровые тайлы нагружают серверную часть.
привязан к глобальной системе координат одной точкой, геометрии внутри тайла храняться во внутренней системе координат тайла. Для векторных тайлов это приводит к сложностям в объединении объектов, которые попадают в несколько тайлов сразу: их соединение требует использования вычислительно дорогих операций. Зато дальнейшая работа с векторными тайлами становится более удобной благодаря возможностям прямого доступа к объектам и их атрибутам для оформления картографических слоёв и организации интерактивной работы с объектами на карте.
Для векторных данных преобладает использование векторных тайлов. Снимки, ЦМР, индексы, в общем, продукты дистанционного зондирования -- растровые тайлы.
Раньше растровые тайлы были для всего
Изначально картографические тайловые системы или пирадимы тайлов применялись к растровым изображениям. Растровые тайлы делятся на стороне сервера, передаются в браузер, в браезере сшиваются в единое изображение той части карты, которую просматривает пользователь.
Однако для растровых тайлов сложно обеспечить непрерывное изменение масштаба, дополнетельных операций требует определение того, какому объекту соответствует тот или иной пиксель, обычно требуется серверная стилизация изображения. Неудобства такого рода привели к разработке векторных тайловых систем.
## Производительность векторных тайлов
Формирование тайла представляет собой запрос кусочка из полного набора пространственных данных, его перепроецирование и кодирование в векторных тайл.
Такие популярные серверы векторных тайлов, как Martin, Tegola, pg_tileserv, формируют тайл средствами базы данных PostGIS. Увидеть запросы, которые они используют можно в режиме отладки. Логично предположить, что на производительность формирования тайлов влияет
1. наличие пространственного индекса, так как есть этап запроса кусочка набора данных по границам тайла
2. проекция исходного набора данных, так как есть этап перепроецирования
Проверим это на запросах к линейному набору пространственных данных
```sql
-- EPSG:4326 без индекса
SELECT ((SELECT ST_AsMVT(q,'lines',4096,'geom','fid') AS data FROM (SELECT ST_AsMVTGeom(ST_Transform(geom, 3857), ST_TileEnvelope(6, 37, 20)) as geom, fid FROM lines WHERE geom && ST_TileEnvelope(6, 37, 20)) AS q)) AS data;
-- EPSG:4326 с индексом
SELECT ((SELECT ST_AsMVT(q,'lines_index',4096,'geom','fid') AS data FROM (SELECT ST_AsMVTGeom(ST_Transform(geom, 3857), ST_TileEnvelope(6, 37, 20)) as geom, fid FROM lines_index WHERE geom && ST_TileEnvelope(6, 37, 20)) AS q)) AS data;
-- EPSG:3857 без индекса
SELECT ((SELECT ST_AsMVT(q,'lines3857',4096,'geom','fid') AS data FROM (SELECT ST_AsMVTGeom(geom, ST_TileEnvelope(6, 37, 20)) as geom, fid FROM lines3857 WHERE geom && ST_TileEnvelope(6, 37, 20)) AS q)) AS data;
-- EPSG:3857 с индексом
SELECT ((SELECT ST_AsMVT(q,'lines3857_index',4096,'geom','fid') AS data FROM (SELECT ST_AsMVTGeom(geom, ST_TileEnvelope(6, 37, 20)) as geom, fid FROM lines3857_index WHERE geom && ST_TileEnvelope(6, 37, 20)) AS q)) AS data;
```
| | без индекса | с индексом |
| ------------------------- | ----------- | ---------- |
| другая проекция (4326) | 389,4 мс | 3,5 мс |
| проекция веб-карты (3857) | 13,5 мс | 0,7 мс |
При запросе тайла на весь мир (фактически всех данных) индекс становится менее существенным.
```sql
-- EPSG:4326 без индекса
SELECT ((SELECT ST_AsMVT(q,'lines',4096,'geom','fid') AS data FROM (SELECT ST_AsMVTGeom(ST_Transform(geom, 3857), ST_TileEnvelope(0, 0, 0)) as geom, fid FROM lines WHERE geom && ST_TileEnvelope(0, 0, 0)) AS q)) AS data;
-- EPSG:4326 с индексом
SELECT ((SELECT ST_AsMVT(q,'lines_index',4096,'geom','fid') AS data FROM (SELECT ST_AsMVTGeom(ST_Transform(geom, 3857), ST_TileEnvelope(0, 0, 0)) as geom, fid FROM lines_index WHERE geom && ST_TileEnvelope(0, 0, 0)) AS q)) AS data;
-- EPSG:3857 без индекса
SELECT ((SELECT ST_AsMVT(q,'lines3857',4096,'geom','fid') AS data FROM (SELECT ST_AsMVTGeom(geom, ST_TileEnvelope(0, 0, 0)) as geom, fid FROM lines3857 WHERE geom && ST_TileEnvelope(0, 0, 0)) AS q)) AS data;
-- EPSG:3857 с индексом
SELECT ((SELECT ST_AsMVT(q,'lines3857_index',4096,'geom','fid') AS data FROM (SELECT ST_AsMVTGeom(geom, ST_TileEnvelope(0, 0, 0)) as geom, fid FROM lines3857_index WHERE geom && ST_TileEnvelope(0, 0, 0)) AS q)) AS data;
```
| | без индекса | с индексом |
| ------------------------- | ----------- | ---------- |
| другая проекция (4326) | 544,7 мс | 519,3 мс |
| проекция веб-карты (3857) | 44,1 мс | 41,2 мс |
Проекция и пространственный индекс позволяют ускорить формирование тайлов, однако наибольший прирост производительности даёт *кэширование*, то есть сохранение результатов запросов. Если данные меняются редко можно предварительно рассчитать тайлы. Для хранения используются форматы MBTiles и PMTiles.

@ -0,0 +1,17 @@
---
title: Литература
tableOfContents: false
---
1. ГОСТ Р 58570-2019. Инфраструктура Пространственных Данных. Общие Требования. Стандартинформ, 2019. [ссылка](https://docs.cntd.ru/document/1200168445)
2. Абдуллин Р. К., Пономарчук А. И. Технологии интернет-картографирования: учебное пособие / Пермский государственный национальный исследовательский университет. - Пермь, 2020. 132 с.: ил. [ссылка](https://gis.psu.ru/publications/технологии-интернет-картографирован/)
3. Аляутдинов А. Р., Лурье И. К., Ушакова Л. А. Основные принципы функционирования геоинформационных ресурсов // Известия высших учебных заведений. Геодезия и аэрофотосъемка. — 2016. — Т. 60, № 5. — С. 123128 [ссылка](https://elibrary.ru/item.asp?id=27224121)
4. Берлянт А. М. Геоинформационное Картографирование. М.: Моск. гос. Ун-т им. М. В. Ломоносова, Рос. акад. естеств. наук, 1997. [ссылка](https://rusneb.ru/catalog/000199_000009_000611203/)
5. Каргашин П. Е. Основы цифровой картографии: Учебное пособие для бакалавров. 5-е изд., перераб. — Москва: Издательско-торговая корпорация Дашков и К, 2023. — 106 с. [ссылка](https://istina.msu.ru/publications/book/557759518/)
6. Лурье И.К., Самсонов Т.Е. Структура и содержание базы пространственных данных для мультимасштабного картографирования // Геодезия и картография. 2010. № 11. С. 17-23. [ссылка](https://istina.msu.ru/publications/article/427465/)
7. Титов Г. С., Прасолова А. И., Каргашин П. Е. Веб-картографирование ресурсов солнечной энергии Якутии // ИнтерКарто. ИнтерГИС. — 2021. — Т. 27, № 3. — С. 210220. [ссылка](https://istina.msu.ru/publications/article/412375618/)
8. Титов Г. С. Текущие проблемы терминологического аппарата отечественной веб-картографии // Геодезия, картография, геоинформатика и кадастры. Производство и образование : Сб. материалов IV Всероссийской науч.-практ. конф. — СПб Политехника: 2021. — С. 317323. [ссылка](https://istina.msu.ru/publications/article/716105082/)
9. Kraak M. J. Web Cartography: Developments and Prospects. Edited by M. J. Kraak and Allan Brown. New York: Taylor & Francis, 2001. [ссылка](https://doi.org/10.1201/9781482289237)
10. Muehlenhaus I. Web Cartography: Map Design for Interactive and Mobile Devices. Boca Raton, FL: CRC Press, 2014. [ссылка](https://doi.org/10.1201/b16229)
11. Neumann A. Web Mapping and Web Cartography. In Springer Handbook of Geographic Information, edited by Wolfgang Kresse and David M. Danko, 27387. Berlin, Heidelberg: Springer Berlin Heidelberg, 2011. [ссылка](https://doi.org/10.1007/978-3-540-72680-7_14)
12. Wind waves web atlas of the russian seas / Myslenkov S., Samsonov T., Shurygina A. et al. // Water. — 2023. — Vol. 15, no. 11. — P. 2036. [ссылка](http://dx.doi.org/10.3390/w15112036)

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

@ -1,5 +1,5 @@
--- ---
title: Обзор содержания title: Введение к введению
tableOfContents: false tableOfContents: false
--- ---

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 MiB

Loading…
Cancel
Save