@ -169,6 +169,8 @@ const map = new maplibregl.Map({
>
>
> Страница HTML является корневой. Ей необходимо дать информацию о том, какие внешние библиотеки и файлы будут использоваться. Например, `style.css` и `main.js` являются внешними файлами, а MapLibre является внешней библиотекой. Находящиеся на сервере файлы необходимо подключать по URL.
> Страница HTML является корневой. Ей необходимо дать информацию о том, какие внешние библиотеки и файлы будут использоваться. Например, `style.css` и `main.js` являются внешними файлами, а MapLibre является внешней библиотекой. Находящиеся на сервере файлы необходимо подключать по URL.
### Запуск локального сервера
После этого запустим Live Server, перейдём по адресу локального сервера и увидим карту.
После этого запустим Live Server, перейдём по адресу локального сервера и увидим карту.
> Live Server обычно запускается по адресу `127.0.0.1:5500`. `127.0.0.1` или `localhost` -- это внутренний адрес сервера на нашем компьютере. Он будет одним и тем же у всех компьютеров. И он недоступен для запросов снаружи. На одном веб-сервере может быть запущено несколько приложений. Для их разграничения используется порт, в нашем случае `5500`.
> Live Server обычно запускается по адресу `127.0.0.1:5500`. `127.0.0.1` или `localhost` -- это внутренний адрес сервера на нашем компьютере. Он будет одним и тем же у всех компьютеров. И он недоступен для запросов снаружи. На одном веб-сервере может быть запущено несколько приложений. Для их разграничения используется порт, в нашем случае `5500`.
@ -410,12 +412,12 @@ map.on('load', () => {
})
})
```
```
## Что мы получили
У нас получилась отличная карта!
У нас получилась отличная карта!
При желании посмотрите [полный код](https://github.com/gtitov/geojson-maplibre-map) и [возможный результат](https://gtitov.github.io/geojson-maplibre-map/).
При желании посмотрите [полный код](https://github.com/gtitov/geojson-maplibre-map) и [возможный результат](https://gtitov.github.io/geojson-maplibre-map/).
## Что мы получили
Откроем вкладку Сеть в инструментах разработчика и ещё разок проследим поток данных
Откроем вкладку Сеть в инструментах разработчика и ещё разок проследим поток данных
import { Card, FileTree, LinkCard, TabItem, Tabs } from '@astrojs/starlight/components';
import { Card, FileTree, LinkCard, TabItem, Tabs } from '@astrojs/starlight/components';
@ -16,26 +15,39 @@ import Option from '../../components/Option.astro';
- определение API
- определение API
- асинхронность JavaScript
- асинхронность JavaScript
- запрос `fetch...then`
- запрос `fetch...then`
- кластеризацию точек на карте
- связку элементов веб-страницы с картой
В рамках практической части создадим карту вакансий на основе изменяющегося содержания онлайн-таблицы. При желании посмотрите [полный код](https://github.com/gtitov/sheet-maplibre-map) и [возможный результат](https://gtitov.github.io/sheet-maplibre-map/).
В рамках практической части создадим карту вакансий на основе изменяющегося содержания Google-таблицы.
## API
## API
API обычно переводят как прикладной программный интерфейс или программный интерфейс приложений. На практике чаще говорят просто "апи".
API обычно переводят как прикладной программный интерфейс или программный интерфейс приложений. На практике чаще говорят просто "апи".
Помните формулировку
Помните формулировку из предыдущего упражнения
> ...клиент (браузер) обращается к серверу с запросом, сервер возвращает клиенту ответ
> ...клиент (браузер) обращается к серверу с запросом, сервер возвращает клиенту ответ


Откуда браузер знает, куда обращаться? Мы написали. А откуда мы знаем? Мы знаем, потому что ожидаем, что при обращении к определённому *адресу* (URL) нам вернётся определённый ответ. Например, при обращении за файлом `style.css` мы получим стили.
Откуда браузер знает, куда обращаться?
Мы написали, куда ему нужно обращаться.
А откуда мы знаем?
Мы знаем, потому что ожидаем, что при обращении к определённому *адресу* (URL) нам вернётся определённый ответ. Например, при обращении за файлом `style.css` мы получим стили, потому что сами и создали этот файл.
<Card title="API">Совокупность *адресов* (URL) и правил обращения к ним называется API. Отдельные адреса называются методами API или, в обиходе, ручками API.</Card>
<Card title="API">Совокупность *адресов* (URL) и правил обращения к ним называется API. Отдельные адреса называются методами API или, в обиходе, ручками API.</Card>
В идеале API будет иметь интуитивно понятное назначение и описание каждого метода, но это не всегда так.
В идеале API будет иметь интуитивно понятное назначение и описание каждого метода.
> Про то, как этого добиться написана не одна книга. Но помните, сложно понять, что такое хорошо и что такое плохо, без практики, поэтому для начала надо дерзать и пробовать.
Существуют стандарты API. Обращаясь к API, который следует стандарту пользователь знает, чего ему ожидать.
> Для пространственных и картографических данных существует ряд стандартов для API. Обращаясь к API, который следует стандарту пользователь знает, чего ему ожидать. Мы уже использовали стандртный формат обмена GeoJSON и видели, что MapLibre принимает его без необходимости предварительной обработки. К другим популярным стандартам следует отнести протоколы WMS, WFS и векторные тайлы. Ряд стандартов курирует Open Geospatial Consortium, некоторые приняты как стандарты ISO и ГОСТ.
> Ряд стандартов API существует для пространственных и картографических данных. Мы уже использовали стандартный формат обмена GeoJSON и видели, что MapLibre принимает его без необходимости предварительной обработки. К другим популярным стандартам следует отнести протоколы WMS, WFS, векторные тайлы. Ряд стандартов курирует Открытый Геопространственный Консорциум (Open Geospatial Consortium), некоторые приняты как стандарты ISO и ГОСТ.
Подключая источники данных, мы тоже использовали API, просто вызовом необходимых методов занималась библиотека MapLibre.
Подключая источники данных, мы тоже использовали API, просто вызовом необходимых методов занималась библиотека MapLibre.
@ -43,8 +55,7 @@ API обычно переводят как прикладной программ
Попробуем обратиться к публично доступным методам API Google-таблиц, а именно загрузить данные таблицы в формате CSV.
Попробуем обратиться к публично доступным методам API Google-таблиц, а именно загрузить данные таблицы в формате CSV.
По аналогии с первым упражнением создадим заготовку для карты из файлов `index.html`, `style.css`, `main.js`.
Для начала по аналогии с первым упражнением создадим заготовку для карты из файлов `index.html`, `style.css`, `main.js`.
<Tabs>
<Tabs>
<TabItem label="HTML">
<TabItem label="HTML">
@ -83,19 +94,15 @@ API обычно переводят как прикладной программ
В консоль вывелся *Promise* -- обещание того, что браузер уже занимается нашим запросом. Ответ от сервера не вывелся, хотя во вкладке Сеть мы видим, что данные загружены. Дело в том, что вывод в консоль был выполнен раньше, чем мы получили ответ от сервера.
> Сюжет с асинхронностью может показать контринтуитивным. Это нормально. Почитайте теорию, попрактикуйтесь, вернитесь к теории, можно поглядеть материалы из раздела [Чтение](#чтение). И всё получится! Проверено.
> Браузер начал исполнять наш код, увидел запрос к внешему ресурсу и подумал: "Здесь можно завязнуть. Ещё неизвестно, сколько этот внешний ресурс будет отвечать. Я верну пока обещание, что когда будет ответ, я его предоставлю, и отправлю запрос. А пока жду ответа буду дальше код выполнять."
### Асинхронность
Запрос к внешнему ресурсу выполняется асинхронно, то есть изымается из последовательного выполнения программного кода и выполняется отдельно. Поэтому вывод в консоль выполняется раньше того, как данные получены.
В консоль браузера вывелся *Promise* -- обещание того, что браузер уже занимается нашим запросом. Ответ от сервера не вывелся, хотя во вкладке "Сеть" в инструментах разработчика мы видим, что данные браузер запросил и получил. Дело в том, что вывод в консоль был выполнен раньше, чем мы получили ответ от сервера.
Чтобы этого не происходило, мы должны в явном виде указать, что код, использующий ответ на запрос, должен выполняться после выполнения запроса. Для этого используем конструкцию `fetch...then`
Браузер начал исполнять наш код, увидел запрос к внешнему ресурсу и подумал: "Здесь можно завязнуть. Ещё неизвестно, сколько этот внешний ресурс будет отвечать. Я сейчас отправлю запрос, а пока верну обещание, что когда будет ответ, я его предоставлю. А пока жду ответа буду дальше код выполнять."
```js title="main.js"
Другими ~более умными~ словами, запрос к внешнему ресурсу выполняется асинхронно, то есть изымается из последовательного выполнения программного кода и выполняется отдельно. Поэтому вывод в консоль выполняется раньше того, как данные получены.
### Работа с асинхронностью
Для асинхронных запросов мы должны в явном виде указать, что код, использующий запрашиваемые данные, должен выполняться после получения ответа на запрос. Для этого используем конструкцию `fetch...then`
> Можно запрашивать эту таблицу, но лучше скопировать её себе. Тогда можно будет менять данные, а этом и есть интерес этого упражнения. Чтобы открыть таблицу уберите из URL часть `/export?format=csv` или воспользуйтесь этой [ссылкой](https://docs.google.com/spreadsheets/d/1f0waZduz5CXdNig_WWcJDWWntF-p5gN2-P-CNTLxEa0). Чтобы потому запросить свою таблицу в формате CSV, добавьте в конец `/export?format=csv`, чтобы URL был похож на тот, что в коде выше.
<details>
`fetch` выполняет запрос и возвращает Promise (1) -- обещание, что дождётся ответа от внешнего ресурсас csv-данными о вакансиях.
<summary>В первой карте тоже был асинхронный код</summary>
Сама карта создаётся асинхронно, поэтому все действия по добавлению слоёв мы выполняем после загрузки карты `map.on('load', () => {})`. Функция, которая вызывается после успешного завершения события называется callback-функцией. Это ещё один вариант работы с асинхронностью. А ещё асинхронно выполняется добавление источников данных `map.addSource`, они же тоже фактически загружаются с сервера. В этом случае библиотека MapLibre сама отслеживает, что код по добавлению источника должен завершиться, прежде чем мы сможем создавать картографические слои `map.addLayer` из этого источника.
`then` получает Promise (1) и сразу возвращает новый Promise (2) -- обещание, что обработает ответ от Promise (1) с помощью заданной функции `(response) => response.text()`. Эта функция извлечёт текст из ответа от Promise (1).
</details>
Следующий `then` получает Promise (2) и сразу возвращает Promise (3). Этот Promise (3) в нашем случае никуда не идёт. Заключительный `then` дождётся исполнения предыдущих Promise (1) и (2) и обработает итоговый ответ функцией `(csv) => console.log(csv)`, то есть выведет полученный от Promise (2) текст в консоль.
> Попробуйте перечитать это, заменив слово Promise на слово Обещание. Иногда становится понятнее.
### Асинхронность внутри карты
В первой карте тоже был асинхронный код!
Сама карта создаётся асинхронно, поэтому все действия по добавлению слоёв мы выполняем после загрузки карты `map.on('load', () => {})`. Функция, которая вызывается после успешного завершения события, в данном случае `'load'` называется колбэк (callback) функцией. Колбэк -- это ещё один вариант работы с асинхронностью.
А ещё асинхронно выполняется добавление источников данных `map.addSource`, они же тоже фактически загружаются с сервера. В этом случае библиотека MapLibre сама отслеживает, что код по добавлению источника должен завершиться, прежде чем мы сможем создавать картографические слои `map.addLayer` из этого источника. Спасибо ей за это!
## Преобразование данных
## Преобразование данных
MapLibre не может работать с форматом CSV. Мы должны преобразовать данные в знакомый формат GeoJSON. Сделаем это!
MapLibre не может работать с форматом CSV. Мы преобразуем данные в знакомый формат GeoJSON.
Подключим библиотеку для чтения CSV данных в JS-объект.
Подключим библиотеку для чтения CSV данных в JS-объект.
@ -171,7 +192,8 @@ MapLibre не может работать с форматом CSV. Мы долж
Выполним чтение CSV данных c использованием подключенной библиотеки и сконструируем GeoJSON-объект.
Выполним чтение CSV данных c использованием подключенной библиотеки и сконструируем GeoJSON-объект.
@ -193,15 +215,15 @@ MapLibre не может работать с форматом CSV. Мы долж
})
})
```
```
GeoJSON уже можно использовать в качестве источника для MapLibre.
Полученный GeoJSON используем в качестве источника данных для нашей карты.
## Работа над картой
## Добавление и кластеризация точек
У нас есть заготовка, есть данные, самое время заняться картой!
У нас есть заготовка, есть данные, самое время заняться картой!
```js title="main.js"
```js title="main.js"
.then((csv) => {
.then((csv) => {
...
// ...
const geojson = {
const geojson = {
type: "FeatureCollection",
type: "FeatureCollection",
features: geojsonFeatures
features: geojsonFeatures
@ -245,10 +267,12 @@ GeoJSON уже можно использовать в качестве исто
})
})
```
```
## Сопутствующие элементы
## Связка карты и веб-страницы
Чаще всего карту сопровождают дополнительные элементы веб-страницы. Для этой карты мы приведём список всех вакансий и список вакансий, которые пользователь видит на карте при текущем охвате.
Чаще всего карту сопровождают дополнительные элементы веб-страницы. Для этой карты мы приведём список всех вакансий и список вакансий, которые пользователь видит на карте при текущем охвате.
### Разметка списков
Разметим этим спискам место на веб-странице.
Разметим этим спискам место на веб-странице.
```html title="index.html"
```html title="index.html"
@ -300,11 +324,13 @@ h2 {
Теперь на нашей веб-странице выделено место под списки. Плюс мы добавили оформление для заголовков второго уровня `h2` и создали класс `.list-item` для будущих элементов списка.
Теперь на нашей веб-странице выделено место под списки. Плюс мы добавили оформление для заголовков второго уровня `h2` и создали класс `.list-item` для будущих элементов списка.
### Список всех вакансий
Сначала наполним список всех вакансий. Это нужно сделать единожды.
Сначала наполним список всех вакансий. Это нужно сделать единожды.
```js title="main.js"
```js title="main.js"
.then((csv) => {
.then((csv) => {
...
// ...
geojson.features.map((f) => {
geojson.features.map((f) => {
document.getElementById(
document.getElementById(
"list-all"
"list-all"
@ -316,24 +342,28 @@ h2 {
})
})
```
```
### Список видимых вакансий
А список вакансий, которые видит пользователь при заданном охвате карты, надо будет обновлять при каждом перемещении по карте. Мы будем реагировать на окончание перемещения. Ещё одной сложностью является необходимость извлечь из каждого кластера сведения о том, какие объекты в него входят. Со всем этим мы прекрасно справимся.
А список вакансий, которые видит пользователь при заданном охвате карты, надо будет обновлять при каждом перемещении по карте. Мы будем реагировать на окончание перемещения. Ещё одной сложностью является необходимость извлечь из каждого кластера сведения о том, какие объекты в него входят. Со всем этим мы прекрасно справимся.
```js title="main.js"
```js title="main.js"
.then((csv) => {
.then((csv) => {
...
// ...
map.on('moveend', () => {
map.on('moveend', () => {
const features = map.queryRenderedFeatures({
const features = map.queryRenderedFeatures({
layers: ["clusters"]
layers: ["clusters"]
});
})
document.getElementById("list-selected").innerHTML = "<h2>Сейчас на карте</h2>"
document.getElementById("list-selected").innerHTML = "<h2>Сейчас на карте</h2>"
// Эти события мы назначаем, пока ждём ответа от внешнего ресурса.
// Не теряем ни секунды.
map.on("click", "clusters", function (e) {
map.on("click", "clusters", function (e) {
map.flyTo({ center: e.lngLat, zoom: 8 });
map.flyTo({ center: e.lngLat, zoom: 8 });
})
})
@ -374,28 +410,54 @@ map.on("load", () => {
})
})
```
```
На протяжении упражнения мы использовали события `load`, `moveend`, `click`, `mouseenter`, `mouseleave`. Полный список доступных событий можно узнать в [документации](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/MapEventType/).
## Что мы получили
## Что мы получили
Проделана прекрасная работа!
Проделана прекрасная работа!
При желании посмотрите [полный код](https://github.com/gtitov/sheet-maplibre-map) и [возможный результат](https://gtitov.github.io/sheet-maplibre-map/).
При желании посмотрите [полный код](https://github.com/gtitov/sheet-maplibre-map) и [возможный результат](https://gtitov.github.io/sheet-maplibre-map/).
После первичной загрузки карты мы делаем следующее:
1. выполняем запрос к внешнему ресурсу -- по API получаем CSV-файл из Гугл таблицы,
1. дожидаемся ответа, используя конструкцию `fetch...then`,
1. обрабатываем его -- конвертируем CSV в GeoJSON,
1. добавляем полученный GeoJSON как источник данных на карту,
1. создаём на основе этого GeoJSON кластеризованный слой
1. используем исходный GeoJSON для формирования списка всех объектов с возможностью поиска по карте
1. используем слой вакансий на карте для формирования списка видимых объектов
Такая карта хороша, когда нужно организовать совместную работу с обновляемыми пространственными данными. Например, на этой основе мне приходилось делать карту для выставки. Специалисты заполняли таблицу в своём темпе, а на основе содержания этой таблицы генерировалась карта, прямо как в этом упражнении. Главное заранее договориться о структуре таблицы. Такую карту можно разметить в Интернете практически бесплатно, потому что динамическая составляющая сайта обеспечивается бесплатным сервисом таблиц.
## Упражнения
## Упражнения
1. Поменяйте местами списки
1. Поменяйте местами списки
2. Сделайте так, чтобы цвет кластера зависел от количества элементов внутри него
2. Сделайте так, чтобы цвет кластера зависел от количества элементов внутри него
3. Сделайте, чтобы до первого перемещения карты список вакансий "Сейчас на карте" тоже был заполнен
3. Сделайте, чтобы до первого перемещения карты список вакансий "Сейчас на карте" тоже был заполнен
<details>
<details>
<summary>Подсказка для третьего 🧙♂️</summary>
<summary>Маленькая подсказка для третьего 🧙♀️</summary>
Нужно найти подходящее [событие карты](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/MapEventType/) вместо <code>map.on("moveend", () => \{\})</code>
</details>
<details>
<summary>Большой подсказ для третьего 🧙♂️</summary>
Можно использовать метод <code>map.on("idle", () => \{\})</code>
Можно использовать метод <code>map.on("idle", () => \{\})</code>
</details>
</details>
## Контрольные вопросы
1. Как называется функция, выполняемая после `load` в конструкции `map.on('load', () => {})`?
1. Что возвращает функция `fetch`?
1. Как называется функция, которую мы применяем для обработки ответа от `fetch`?
1. В каком методе мы указываем параметры кластеризации точек на карте?
1. Какой метод мы используем, чтобы получить объекты, отрисованные на карте?
## Чтение
## Чтение
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/)