diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..db5e29e
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,8 @@
+{
+ "markdown.copyFiles.destination": {
+ "src/content/docs/**/*": "${documentWorkspaceFolder}/src/assets/${fileName}"
+ },
+ "files.associations": {
+ // "*.mdx": "markdown"
+ },
+}
\ No newline at end of file
diff --git a/src/assets/vector-raster.gif b/src/assets/vector-raster.gif
new file mode 100644
index 0000000..3ffb9aa
Binary files /dev/null and b/src/assets/vector-raster.gif differ
diff --git a/src/assets/zxy.png b/src/assets/zxy.png
new file mode 100644
index 0000000..2f21355
Binary files /dev/null and b/src/assets/zxy.png differ
diff --git a/src/content/docs/chapters/1-webmapping.mdx b/src/content/docs/chapters/1-webmapping.mdx
index 13d1433..c948480 100644
--- a/src/content/docs/chapters/1-webmapping.mdx
+++ b/src/content/docs/chapters/1-webmapping.mdx
@@ -5,7 +5,7 @@ title: Веб-картографирование
import { Card, LinkCard } from '@astrojs/starlight/components';
import Question from '../../../components/Question.astro'
-Этот раздел содержит краткий *теоретический обзор* сферы веб-картографии
+В этой главе мы совершим краткий *теоретический обзор* сферы веб-картографии
@@ -66,19 +66,3 @@ import Question from '../../../components/Question.astro'
Веб-картография формируется на пересечении сетевых технологий, картографии и геоинформатики изучает особенности оборота пространственных данных в сетевой среде, включая хранение, кодирование, передачу, обработку данных, разработку веб-приложений и другие аспекты.
-
----
-
-## Литература
-
-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. — С. 210–220. [ссылка](https://istina.msu.ru/publications/article/412375618/)
-1. Титов Г. С. Текущие проблемы терминологического аппарата отечественной веб-картографии // Геодезия, картография, геоинформатика и кадастры. Производство и образование : Сб. материалов IV Всероссийской науч.-практ. конф. — СПб Политехника: 2021. — С. 317–323. [ссылка](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, 273–87. 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)
diff --git a/src/content/docs/chapters/2-webmap.md b/src/content/docs/chapters/2-webmap.md
new file mode 100644
index 0000000..1b105c6
--- /dev/null
+++ b/src/content/docs/chapters/2-webmap.md
@@ -0,0 +1,320 @@
+### Инициализация карты
+
+Создадим файл разметки `index.html`.
+
+```html title="index.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 файлы хранят пространственные данные.
+
+
+- data/ # данные
+ - cities.geojson # города
+ - countries.geojson # страны
+ - lakes.geojson # озёра
+ - rivers.geojson # реки
+- index.html # разметка
+- style.css # стили
+- main.js # логика
+
+
+Все действия с картой выполняются после первичной загрузки исходной карты.
+
+Добавление картографических слоёв включает два шага: добавление источника данных `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/).
+
+## Что мы получили
+
+Откроем вкладку Сеть в инструментах разработчика и ещё разок проследим поток данных
+
+
+
+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`) при расположении поверх стран
\ No newline at end of file
diff --git a/src/content/docs/chapters/2-webmap.mdx b/src/content/docs/chapters/2-webmap.mdx
index 0d3f3f8..d01065b 100644
--- a/src/content/docs/chapters/2-webmap.mdx
+++ b/src/content/docs/chapters/2-webmap.mdx
@@ -5,11 +5,11 @@ title: Веб-карта
import { Card, FileTree, LinkCard } from '@astrojs/starlight/components';
import Question from '../../../components/Question.astro'
-В этой главе
+В этой главе мы рассмотрим
- понятие веб-карты
- типы веб-карт
-- клиент и сервер
+- клиент-серверную архитектуру
- HTML, CSS, JavaScript
В рамках практической части создадим карту мира на основе статических GeoJSON-файлов.
diff --git a/src/content/docs/chapters/3-api.md b/src/content/docs/chapters/3-api.md
new file mode 100644
index 0000000..3d4a4b3
--- /dev/null
+++ b/src/content/docs/chapters/3-api.md
@@ -0,0 +1,332 @@
+## Заготовка для карты
+
+Попробуем обратиться к публично доступным методам API Google-таблиц, а именно загрузить данные таблицы в формате CSV.
+
+По аналогии с первым упражнением создадим заготовку для карты из файлов `index.html`, `style.css`, `main.js`.
+
+
+
+
+ ```html title="index.html"
+
+
+
+
+
+
+ Карта вакансий
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+
+ ```css title="style.css"
+ #map {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+ ```
+
+
+ ```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
+ });
+ ```
+
+
+
+<div id="map"></div> это'}>
+
+
+
+Удостоверимся, что карта отображается на локальном сервере.
+
+## Обращение к 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)
+ })
+})
+```
+
+
+
+В первой карте тоже был асинхронный код
+
+Сама карта создаётся асинхронно, поэтому все действия по добавлению слоёв мы выполняем после загрузки карты `map.on('load', () => {})`. Функция, которая вызывается после успешного завершения события называется callback-функцией. Это ещё один вариант работы с асинхронностью. А ещё асинхронно выполняется добавление источников данных `map.addSource`, они же тоже фактически загружаются с сервера. В этом случае библиотека MapLibre сама отслеживает, что код по добавлению источника должен завершиться, прежде чем мы сможем создавать картографические слои `map.addLayer` из этого источника.
+
+
+
+## Преобразование данных
+
+MapLibre не может работать с форматом CSV. Мы должны преобразовать данные в знакомый формат GeoJSON. Сделаем это!
+
+Подключим библиотеку для чтения CSV данных в JS-объект.
+
+```html title="index.html"
+
+ ...
+
+
+```
+
+Выполним чтение 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"
+
+
+
Сейчас на карте
+
Все вакансии
+
+
+```
+
+И зададим оформление.
+
+```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 += `
`;
+ });
+})
+```
+
+А список вакансий, которые видит пользователь при заданном охвате карты, надо будет обновлять при каждом перемещении по карте. Мы будем реагировать на окончание перемещения. Ещё одной сложностью является необходимость извлечь из каждого кластера сведения о том, какие объекты в него входят. Со всем этим мы прекрасно справимся.
+
+```js title="main.js"
+.then((csv) => {
+ ...
+ map.on('moveend', () => {
+ const features = map.queryRenderedFeatures({
+ layers: ["clusters"]
+ });
+
+ document.getElementById("list-selected").innerHTML = "
`
+ }
+ })
+ })
+})
+```
+
+## Пара 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/)
diff --git a/src/content/docs/chapters/3-api.mdx b/src/content/docs/chapters/3-api.mdx
index a0f61e2..bff2cca 100644
--- a/src/content/docs/chapters/3-api.mdx
+++ b/src/content/docs/chapters/3-api.mdx
@@ -2,13 +2,13 @@
title: API
---
-import { Card, FileTree, LinkCard, Tabs, TabItem } from '@astrojs/starlight/components';
-import Question from '../../../components/Question.astro'
+import { Card, FileTree, LinkCard, TabItem, Tabs } from '@astrojs/starlight/components';
+import Question from '../../../components/Question.astro';
-В этой главе
+В этой главе мы рассмотрим
- определение API
- асинхронность 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. Асинхронность в JavaScript / Дока [ссылка](https://doka.guide/js/async-in-js/)
1. fetch() / Дока [ссылка](https://doka.guide/js/fetch/)
diff --git a/src/content/docs/chapters/4-backend.md b/src/content/docs/chapters/4-backend.md
new file mode 100644
index 0000000..90bd4d9
--- /dev/null
+++ b/src/content/docs/chapters/4-backend.md
@@ -0,0 +1,328 @@
+## Подготовка
+
+Создадим папку с заготовкой для нашей карты.
+
+Добавим туда папку `backend`. Загрузим [отсюда](https://github.com/gtitov/flask-maplibre-map/raw/refs/heads/main/backend/cities_index.sqlite) и положим туда базу данных. И создадим в этой папке файл с нашим бэкендом `app.py`.
+
+
+- backend/
+ - app.py
+ - cities_index.sqlite
+- index.html
+- style.css
+- main.js
+
+
+
+Что это за база данных такая -- SQLite
+
+SQLite -- встраиваемая база данных, которая хранит все свои данные в одном файле. В одном файле хранятся все таблицы, индексы и другая информация о базе данных, что упрощает управление и резервное копирование. Благодаря этому, она не требует отдельного сервера и легко интегрируется в различные приложения.
+
+
+
+## Разработка бэкенда
+
+### Установка 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/") # путь 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/")
+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/") # путь 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/")
+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
+
+
+
++
++
Год
++
++
+
+
+```
+
+Сделаем так, чтобы он выводился поверх карты.
+
+```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
+
+
+ ...
++
+
+
+```
+
+При клике будем выполнять запрос к методу 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 = `
${cityProperties.name}
+
+
Численность населения
${cityProperties.people_count} тыс. чел
+
Индекс качества городской среды
${cityProperties.total_points} / 360
+
+
Жилье и прилегающие пространства
${cityProperties.house_points} / 60
+
Озелененные пространства
${cityProperties.park_points} / 60
+
Общественно-деловая инфраструктура
${cityProperties.business_points} / 60
+
Социально-досуговая инфраструктура
${cityProperties.social_points} / 60
+
Улично-дорожная
${cityProperties.street_points} / 60
+
Общегородское пространство
${cityProperties.common_points} / 60
`
+ document.getElementById("city-details-modal").showModal() // showModal() -- встроенный метод элемента