Намалюйте карту. Ковзна карта з плитками.

Намалюйте карту. Ковзна карта з плитками.

У цій статті ми хочемо показати, як за допомогою бібліотеки Aspose.GIS та загальнодоступних даних можна побудувати ковзну карту, яка генеруватиметься в реальному часі. Завдяки новій функції бібліотеки ми тепер можемо запитувати геоінформаційні дані з бази даних через SQL-запит. Ось що ми повинні отримати в результаті:

Результат

Репозиторій вихідного коду тут.

Підготовка даних.

Насамперед нам знадобиться геопросторова інформація, яку ми можемо завантажити в базу даних. Одним із популярних джерел такої інформації є OpenStreetMap, тож давайте використаємо його. Найзручніший спосіб, на мою думку, — це витягувати дані у форматі pbf з загальнодоступного ресурсу https://download.geofabrik.de/ . Наприклад, завантажимо Угорщину.

На наступному етапі нам потрібен робочий екземпляр PostGIS. Звичайно, ви можете використовувати локально встановлену версію PostgreSQL, але я вважаю дуже зручним використання Docker-контейнерів. Давайте встановимо PostGIS за допомогою файлу docker compose:

services:
  postgis:
    image: postgis/postgis
    environment:
      - POSTGRES_DB=gis
      - POSTGRES_USER=gis
      - POSTGRES_PASSWORD=password
    ports:
      - 5432:5432
    volumes:
      - d:\\local_folder:/usr/share/gisdata

Том d:\local_folder:/usr/share/gisdata потрібен для завантаження геоінформаційних даних з локальної машини.

Далі давайте запустимо наш контейнер:

docker compose up

Підключіться до екземпляра бази даних, використовуючи pgAdmin, і створіть там базу даних Угорщини:

Створити БД

або через SQL-команду:

CREATE DATABASE Hungary;

Додайте необхідні розширення до цієї бази даних:

Розширення

Це будуть розширення postgis та hstore. hstore — це розширення, яке дозволяє використовувати тип даних ключ-значення. OpenStreetMap широко використовує цей тип для опису атрибутів, які не входять до категорії основних, тому для них не створюються окремі поля, але вони зберігаються в полі tags.

Також є версія команд, подібна до SQL:

CREATE EXTENSION IF NOT EXISTS hstore;
CREATE EXTENSION IF NOT EXISTS postgis;

Тепер давайте підключимося до контейнера, у моєму випадку це local_folder-postgis-1:

docker exec -it local_folder-postgis-1 sh

І встановіть програму, яка імпортуватиме дані з файлу pbf в базу даних:

apt-get update && apt-get install -y osm2pgsql

Переконайтеся, що файл hungary-latest.osm.pbf знаходиться у вашій папці local_folder, а потім запустіть команду імпорту:

osm2pgsql --create --database=Hungary --user=gis --password --host=localhost --port=5432 --hstore /usr/share/gisdata/hungary-latest.osm.pbf

У випадку Угорщини на завершення цієї команди у мене пішло півтори хвилини. Опція --create означає простий режим створення нової бази даних. До речі, крім усього іншого, існує також режим --append, який дозволяє оновлювати дані, якщо вони змінилися:

osm2pgsql --append --slim OSMFILE

Опція --hstore повідомляє програмі додатково створити поле tags типу hstore для зберігання додаткової інформації про об’єкти та геометрії.

Бек-енд

Отже, наші дані готові до використання. Наступним кроком на шляху до створення карти є створення бек-енду. Мета нашого бек-енду — генерувати спеціальні tiles, зазвичай розміром 256*256, з яких, як мозаїка, карта буде зібрана в браузері. Кожна плитка унікально ідентифікується комбінацією таких параметрів, як Z, що є ступенем збільшення/зменшення масштабу карти, X — це рядок у масиві плиток, а Y — це стовпець. Більше про природу плиток можна прочитати тут.

Наш бек-енд буде на ASP.NET Core відповідно, тож давайте почнемо зі створення проєкту. Отже, давайте створимо проєкт на основі попередньо встановленого шаблону ASP.NET Core MVC у Visual Studio.

Далі встановіть NuGet пакет Aspose.GIS у проект, який генеруватиме плитки:

dotnet add package Aspose.GIS --version 24.6.0

Очистіть проєкт від непотрібних файлів. Щоб структура виглядала приблизно так, як показано нижче:

Провідник рішень

Потім видаліть вміст папки wwwroot/lib, оскільки ми встановлюватимемо наші залежності через libman. Нижче наведена структура файлу libman.json:

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "leaflet@1.9.4",
      "destination": "wwwroot/lib/leaflet/"
    },
    {
      "library": "bootstrap@5.3.3",
      "destination": "wwwroot/lib/bootstrap/",
      "files": [
        "css/bootstrap-reboot.min.css"
      ]
    }
  ]
}

Додано залежності клієнта у файл _Layout.cshtml:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Aspose.GIS.TilesTest</title>
    <link href="~/lib/bootstrap/css/bootstrap-reboot.min.css" rel="stylesheet" />
    @await RenderSectionAsync("Styles", required: false)
</head>
<body>
    @RenderBody()
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

І також відредагуйте Index.cshtml:

@{
    ViewData["Title"] = "Home Page";
}

<div id="map"></div>

@section Styles {
    <link href="~/lib/leaflet/leaflet.min.css" rel="stylesheet" />
    <link href="~/css/map.css" rel="stylesheet" asp-append-version="true"/>
}

@section Scripts {
    <script src="~/lib/leaflet/leaflet.min.js"></script>
    <script src="~/js/map.js" asp-append-version="true"></script>
}

У цьому випадку bootstrap-reboot.min.css скидає налаштування стилю за замовчуванням, а leaflet.min.js відповідає за рендеринг карти, тобто збирання частин із плиток у карту. Давайте встановимо висоту блоку карти на повну висоту видимої області у файлі map.css:

#map {
    min-height: 100vh;
}

Вміст файлу map.js також досить простий, але трохи цікавіший:

var map = L.map('map').setView([47.59995, 19.36623], 13);
const tiles = L.tileLayer('/tiles/{z}/{x}/{y}.png', {
    maxZoom: 19,
    minZoom: 11
}).addTo(map);

Тут ми використовуємо API бібліотеки leaflet, де вказується ідентифікатор блоку карти 'map', потім у методі setView встановлюються координати місця, з якого почнеться початкове завантаження карти, а також масштаб, наприклад, 13. Зверніть увагу на метод tileLayer, він приймає рядковий шаблон для запиту плитки до сервера. Ця адреса може бути абсолютною для отримання доступу до сторонніх серверів плиток або відносною, як у нашому випадку.

Щоб реалізувати обробник запитів для генерації плиток, давайте спочатку визначимо окремий маршрут у файлі Program.cs:

app.MapControllerRoute(name: "tiles",
  pattern: "tiles/{z}/{x}/{y}.png",
  defaults: new { controller = "Tiles", action = "Index" });

app.MapControllerRoute(
  name: "default",
  pattern: "{controller=Home}/{action=Index}/{id?}");

Ми вказали, що передані координати відповідають системі координат 3857 (Web Mercator).

Важливий момент полягає в тому, що на поточному етапі інтеграції бази даних бібліотека може читати геометрії у форматі WKB, але краще використовувати EWKB, тобто Extended Well-Known Binary, тоді не потрібно буде передавати інформацію про просторову систему координат як окреме поле, оскільки вона вже буде вбудована в інформацію про геометрію. Для цих цілей ми використовуємо функцію ST_AsEWKB.

Функція ST_ClipByBox2D використовується для обрізання геометрій, які виходять за межі кордонів обмежувального прямокутника.

Добре, тепер нам просто потрібно відрендерити все це в PNG-плитку та повернути її клієнту. Це приклад коду:

using var map = new Map(256, 256);
var pngStream = new MemoryStream();
var labeling = new RuleBasedLabeling
{
  { x => x.GetValue<string>("source") == "polygon",  new SimpleLabeling("addr:housenumber") },
  LabelingRule.CreateElseRule(new SimpleLabeling("name"))
};

map.SpatialReferenceSystem = SpatialReferenceSystem.WebMercator;
map.Extent = new Extent(min_x, min_y, max_x, max_y, SpatialReferenceSystem.WebMercator);            
map.Add(citiesLayer, new SimpleFill { FillColor = Color.PeachPuff }, labeling);
map.Add(forestLayer, new SimpleFill { FillColor = Color.PaleGreen }, labeling);
map.Add(waterLayer, new SimpleFill { FillColor = Color.SkyBlue }, labeling);
map.Add(buildingsLayer, new SimpleFill { FillColor = Color.SandyBrown }, labeling);
map.Render(AbstractPath.FromStream(pngStream), Renderers.Png);

pngStream.Seek(0, SeekOrigin.Begin);

return File(pngStream, "image/png");

Сподіваємося, нам вдалося передати вам основні ідеї та методи побудови карти. Бажаємо успіхів у ваших експериментах.