MySQL 8. Поиск по полигональным картам

MySQL 8 Поиск по полигональным картам

В этой статье я расскажу вам, как сделать очень простой локатор (locator map). Такие карты обычно используют для поиска магазинов, объектов недвижимости, домов AirBNB или геокэшига. Конечно, эта карта будет не такой полнофункциональной, как в вышеприведенных примерах, но она реализует свои базовые функции гораздо проще, чем мы обычно видим.

Я сделаю несколько предположений относительно ваших знаний, и если вы понимаете, что не подходите под этот образ, то вам, возможно, стоит сначала изучить всё это:

  • Вы можете с нуля создать новое приложение Laravel + Vue.
  • Вы достаточно разбираетесь в Docker, чтобы запустить его, или можете настроить собственную среду для запуска этого приложения.
  • Вы хорошо понимаете MySQL.
  • Вы можете создать и запустить миграцию базы данных через Laravel.
  • Вы уже работали с JavaScript API Google карт или имеете базовые знания об этом.
  • У вас есть представление о том, что я подразумеваю под геолокацией и что такое координата.
  • (необязательно) Вы читали мою статью о MySQL 8 и геолокации

Кроме того, поскольку это всего лишь техническая демка, я расскажу о best practises, таких как TDD или тесты в целом. В некоторых местах код будет не такой, как я пишу обычно, чтобы не отвлекаться на дополнительные абстракции.

Не предназначено для копирования в реальные проекты без каких-либо существенных изменений!

Без лишних слов, давайте начнем!

Шаг 1: Что нам нужно?

Для демонстрации создадим приложение Laravel + Vue. С этого момента я буду предполагать, что у вас есть хотя бы небольшой уровень понимания этих инструментов. Кроме того, я работаю с Docker, и демку, которое я предоставлю в конце, поставляется с настройкой Docker, но вы можете проигнорировать все это, если вы будете запускать приложение любым другим способом.

Необходимые инструменты:

  • Laravel 5.8
  • Docker (если вы не настроили собственную среду)
  • VueJS 2
  • Google Maps JavaScript API

Шаг 2: База данных

Для приложения нам реально нужна будет только одна таблица. Давайте назовем её markers и настроим миграцию.

php artisan make:migration create_markers

Сама миграция очень маленькая. Просто первичный ключ и колонки координат.

Schema::create('markers', function (Blueprint $table) {
    $srid = env('SPATIAL_REF_ID', 4326);

    $table->bigIncrements('id');    
    
    $table->point('coordinate', $srid);   
    
    $table->string('lat')->virtualAs('ST_X(coordinate)');
    $table->string('lon')->virtualAs('ST_Y(coordinate)');

    $table->timestamps();
});

Поскольку координата всегда состоит из двух значений (широта и долгота), нам нужна колонка point.
Этот тип колонки является пространственным полем (spatial field) со значениями x и y. Внимательные читатели, возможно, заметили первую строчку:

$srid = env(‘SPATIAL_REF_ID’, 4326);

Если вы читали другие мои статьи, то узнаёте термин SRID, а если нет, то я сейчас всё кратко объясню.

Система пространственной привязки (SRS — spatial reference system) или система координат ( CRS — coordinate reference system) — это локальная, региональная или глобальная система координат, используемая для определения местоположения географических объектов.

MySQL 8 позволяет определить «идентификатор пространственной ссылки» (Spatial Reference ID) для каждой колонки. В случаях использования, например, в локаторах магазинов, вы почти всегда захотите использовать WGS 84, который идентифицируется как 4326. Используя правильную систему отсчета, убедитесь, что координаты в вашей базе данных, при проецировании их на карту, соответствует тому же положению, которое и предполагалось.

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

$table->point('coordinate', $srid);

$table->string('lat')->virtualAs('ST_X(coordinate)');
$table->string('lon')->virtualAs('ST_Y(coordinate)');

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

Шаг 3: Структура данных и обработка

Координата является наиболее важным объектом данных в этом приложении, и нужно убедиться, что мы можем положиться на её корректность. Итак, начнем с создания простого DTO (Объект передачи данных, один из шаблонов проектирования):

namespace App;

use Illuminate\Contracts\Support\Arrayable;

class Coordinate implements Arrayable
{
    /**
     * @var float
     */
    private $lat;

    /**
     * @var float
     */
    private $lon;

    /**
     * @param float $lat
     * @param float $lon
     */
    public function __construct(float $lat, float $lon)
    {
        $this->lat = $lat;
        $this->lon = $lon;
    }

    /**
     * @return float
     */
    public function getLat(): float
    {
        return $this->lat;
    }

    /**
     * @return float
     */
    public function getLon(): float
    {
        return $this->lon;
    }

    /**
     * Get the instance as an array.
     *
     * @return array
     */
    public function toArray()
    {
        return [
            'lat' => $this->getLat(),
            'lon' => $this->getLon(),
        ];
    }
}

Как вы могли заметить, я всегда использую числа с плавающей точкой для двух частей координаты. Это может показаться очевидным, но я видел как часто используют обычные строки (и, следовательно, varchars в базе данных), чтобы не указать на это.

Модель маркеров

Далее мы создадим модель для нашей таблицы markers.

php artisan make:model Marker

Мы будем использовать Coordinate DTO для назначения и получения координаты из модели.

namespace App;

use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model;

class Marker extends Model
{
    /**
     * @var array
     */
    protected $fillable = [
        'coordinate',
    ];

    /**
     * @param Coordinates $coordinates
     *
     * @return $this
     */
    public function setCoordinate(Coordinate $coordinates): self
    {
        $srid = env('SPATIAL_REF_ID', 4326);
        $lat  = $coordinates->getLat();
        $lon  = $coordinates->getLon();

        $this->attributes['coordinate'] = DB::raw("ST_GeomFromText('POINT($lat $lon)', $srid)");

        return $this;
    }

    /**
     * @return Coordinate
     */
    public function getCoordinate(): Coordinate
    {
        return new Coordinate($this->lat, $this->lon);
    }
}

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

Обратите внимание на использование ST_GeomFromText в сеттере. Это необходимо для того, чтобы иметь возможность установить координату. Поскольку Laravel и Eloquent офигительны, то довольно легко подключиться к событию модели saving и выполнить эту логику там. Если у вас есть несколько моделей, которые используют координаты, вы можете даже создать для этого абстрактный класс, трейт, фабрику или какой-то другой класс более высокого порядка.

А пока мы будем придерживаться простой реализации.

Создание Маркера

Используя два приведенных выше класса мы можем создать наш первый маркер:

$coordinate = new Coordinate(50.8815228, 5.7469309);

$marker     = (new Marker)
                ->setCoordinate($coordinate)
                ->save();

В демке, которую я выложил на Github, я еще добавляю поле имени и сеттер для этого поля. Но это необязательно.

Шаг 3b: Создание списка случайных координат

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

Почему генератор, а не сидер (seeder)? Потому что я хочу, чтобы вы имели возможность создавать список маркеров, актуальных для области вокруг вас, а не только для моего офиса. Да и потому, что это просто весело!

Для начала давайте сгенерируем консольную команду:

php artisan make:command GenerateRandomLocations

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

Посмотрите на описание, чтобы узнать, что умеет команда:

/**
 * The name and signature of the console command.
 *
 * @var string
 */
protected $signature = 'make:locations 
        {startLat=50.884408: Starting latitude} 
        {startLon=5.756073: Starting longitude} 
        {radius=50: Limiting radius from starting point in Km} 
        {amount=100: Amount of locations to generate}';

Мы используем знакомый префикс make. Есть несколько аргументов:

  • startLat: широта координаты, которая будет в центре вновь созданного списка координат.
  • startLon: долгота той же центральной координаты.
  • radius: определяет максимальное расстояние сгенерированных координат, в километрах.
  • amount: количество локаций для генерации.

По дефолту, без аргументов, команда сгенерирует 100 точек в радиусе 5 км от моего офиса.

«Магия» происходит в методе handle:

/**
 * @param GeoCalculationService $service
 */
public function handle(GeoCalculationService $service)
{
    $faker         = Factory::create();
    $startingPoint = $this->getStartingPoint();

    for($i = 0; $i <= $this->getAmount(); $i++) {
        $newCoordinate = $service->randomCoordinate($startingPoint, $this->getRadius());

        (new Marker)
            ->setName($faker->name)
            ->setCoordinate($newCoordinate)
            ->save();
    }
}

Мы используем фабрику Faker для генерации имен (помните, я говорил, что это необязательно? Если вам они не нужны, пропустите эту часть).

Внутри цикла for (на основе выбранного значения amount ) мы будем использовать сервисный класс для генерации случайной координаты на основе предоставленных аргументов.

Сервисный класс

Полный сервисный класс можно посмотреть в демке на Github. А для статьи я хотел бы показать эту небольшую часть:

/**
 * @param Coordinate $coordinates
 * @param int         $maxDistance
 *
 * @return Coordinate
 */
public function randomCoordinate(Coordinate $coordinates, int $maxDistance): Coordinate
{
    $heading  = mt_rand(0, 359);
    $distance = mt_rand(0, $maxDistance);

    return $this->project($coordinates, $distance, $heading);
}

Чтобы создать случайную координату, мы сначала определяем случайный курс (в пределах 360 градусов по компасу). Затем мы определяем случайное расстояние в пределах заданного лимита.

Функция проекта, которую я не буду здесь объяснять (так как она сама по себе заслуживает отдельной статьи), будет использовать (на удивление) технику, называемую проекцией, для вычисления координаты на курсе и расстоянии от заданной начальной точки. Тем из вас, кто увлекался геокешингом, это знакомо. Это можно сделать с помощью обычного компаса и пешего хода, но у нас нет на это времени.

Я говорил о формуле haversine в одной из моих статей, и формула для проецирования новой координаты тесно связана с ней. Она учитывает такие вещи, как радиус Земли, а затем выполняет множество расчетов sin/cos/tan, которые я сам почти не понимаю. Поэтому я не буду расстраивать вас своими объяснениями. Ссылка выше позволит вам протестировать сделанные нами проекции, если вы хотите убедиться, что они верны.

Шаг 4: Найти локации

Итак, теперь мы можем создавать случайные координаты в пределах фиксированного радиуса, но как нам искать точки?

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

/**
 * @param array $mapBounds
 * @param array|null $markerIds
 *
 * @return Builder
 */
public function findByBounds(array $mapBounds)
{
    ...

    [   'south' => $south,
        'west'  => $west,
        'north' => $north,
        'east'  => $east
    ] = $mapBounds;

   ...
}

Методы findByBounds ожидают массив точек, которые представляют два ребра прямоугольника. Мы можем получить эту информацию из Google Maps, и позже я покажу вам как это сделать. А пока давайте предположим, что массив всегда будет содержать следующие данные и в этом порядке:

  • Самая южная долгота прямоугольника.
  • Самая западная широта прямоугольника
  • Самая северная долгота пересечения
  • Самая восточная широта прямоугольника.

То, что я имею ввиду прекрасно проиллюстрирует эта картинку, которую я любезно свистнул со StackOverflow:

Карта точек

Точка C представляет юго-западный угол прямоугольника, а B — северо-восточный угол. Рисуя минимальный ограничивающий прямоугольник  из этих двух точек, вы получите этот красный прямоугольник. Таким образом, всё, что нам нужно для получения прямоугольника, это всего лишь два его угла, и, к счастью, Google Maps легко нам их предоставляет.

Поэтому, если посмотреть на полный метод findByBounds, мы можно понять, как они могут вписаться в нужный нам запрос:

/**
 * @param array $mapBounds
 *
 * @return Builder
 */
public function findByBounds(array $mapBounds)
{
    $srid = env('SPATIAL_REF_ID', 4326);

    [   'south' => $south,
        'west'  => $west,
        'north' => $north,
        'east'  => $east
    ] = $mapBounds;

    $query = Marker::query()
        ->whereRaw("
        ST_Contains(
            ST_PolygonFromText('POLYGON(
                   ($north $west, $north $east, $south $east, $south $west, $north $west)
                )', {$srid}), 
            `coordinate` 
        )");

    return $query;
}

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

Сам запрос требует небольшого объяснения, поэтому позвольте мне его разобрать:

POLYGON(($north $west, $north $east, $south $east, $south $west, $north $west))

Во-первых, в самой внутренней части запроса мы используем значения $mapBounds, которые мы извлекли, чтобы создать наш Polygon. Если вам интересно, если вы ошиблись при подсчете, то да, мы используем 4 различных значения для заполнения 10 значений в запросе.Из двух координат мы можем легко определить другие 2 угла прямоугольника. Таким образом, остаются 2 значения, которые в основном повторяются первые 2 значения. Всегда нужно «закрыть» полигон, чтобы начальная точка всегда совпадала с конечной точкой.

Если мы продвинемся на один шаг вверх, то получим ST_PolygonFromText, который просто преобразует наше текстовое представление многоугольника в геометрическое.

Еще выше, и мы получаем абсолютно блестящую функцию ST_Contains. Единственное, что требует эта функция: полигон, в котором она будет искать, и координата для поиска. Координата в этом случае — просто колонка нашей таблицы маркеров. Полигон — тот, который мы только что создали.

И это всё, что нам нужно на самом деле. Эти объединенные методы будут использовать индексированную пространственную колонку (spatial column) для поиска в пределах сгенерированной геометрии. Сама геометрия генерируется для каждой записи, но влияние её незначительно. Однако есть способы сгенерировать его один раз, а затем использовать для вычисления опорных точек.

Демо-код также содержит возможность добавления в список идентификаторов маркеров для исключения из поиска, поскольку нет необходимости извлекать маркеры, которые мы уже выбирали ранее. Мы кратко вернемся к этому, когда я буду объяснять фронтенд код.

Последняя часть для получения данных — метод контроллера, который, понятно дело, небольшой:

public function getLocations(MapRequest $request, LocationFinderService $finderService)
{
    if(!$request->validated()) {
        throw new InvalidArgumentException('Invalid map bounds');
    }

    $markers = $finderService->findByBounds(
        $request->getMapBounds()
    );

    return MarkerResource::collection(
        $markers->paginate(100)
    );
}

Класс MapRequest просто гарантирует массив $mapBounds, который мы обсуждали ранее. Всё, что нам нужно сделать, это предоставить эти данные в LocationFinderService. Если вы обратили внимание, то заметили, что сервис возвращает экземпляр Builder, который позволяет нам выполнять такие вещи, как пагинация из контроллера.

Шаг 5: Покажите мне карту!

Большая часть кода бэкенда готова, поэтому давайте перейдем к фронтенду.

Я немного ленив и, вместо того, чтобы создавать новый шаблон, я просто изменил дефолтный welcome.blade.php:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" content="{{ csrf_token() }}">
        <title>Демонстрация поиска по полигональной карте</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    </head>
    <body class="bg-light">
        <div class="container">
            <div class="py-5 text-center">
                <h1>Демонстрация поиска по полигональной карте</h1>
                <p class="lead">
                    Демонстрация возможностей MySQL 8 поиска точек в Полигоне для картографирования.
                </p>
            </div>
            <div class="d-flex justify-content-center" id="app">
                <demo-map :lat="50.884408" :lon="5.756073" :zoom="16"></demo-map>
            </div>
        </div>
        <script src="{{ mix('js/app.js') }}" type="text/javascript"></script>
    </body>
</html>

Здесь не происходит ничего особенного, за исключением загрузки Bootstrap через CDN для облегчения запуска без необходимости написания какого-либо CSS. Конечно, Tailwind тоже бы справился с этим. Просто работайте с тем, что знаете или любите больше всего.

Если знакомы с VueJS, то заметите, что основным компонентом здесь является demo-map. Мы передаем два параметра компоненту, определяющие центр карты. Для своих целей вы, скорей всего, захотите изменить это на собственное местоположение.

Примечание: Если вам интересно, почему я здесь использую динамический параметры (используя сокращение v-bind ), то причина в том, что мы работаем с числами с плавающей точкой. Если бы я этого не сделал, то результирующие элементы в компоненте стали бы строками.

Остальная часть шаблона это основа и подключение JavaScript.

Компонент DemoMap

Что подводит нас к самой демо-карте:

<template>
    <div>
        <div class="google-map" id="map"></div>
    </div>
</template>

Он начинается с очень простого шаблона, который представляет собой не что иное, как div с Google Maps. Мы используем немного пользовательских стилей, так как Google Maps не любит динамическое изменение размеров:

<style>
    #map {
        margin: 0 auto;
        height: 600px;
        width: 800px;
    }
</style>

Но это, в основном, просто оформление витрины. Настоящее веселье происходит здесь:

import GoogleMaps from "../GoogleMaps";
import debounce from "tiny-debounce";
import axios from "axios";

export default {
    props: {
        zoom: {
            type: Number,
            default: 8
        },
        lat: {
            type: Number
        },
        lon: {
            type: Number
        }
    },
    data() {
        return {
            googleMapsKey: process.env.MIX_GOOGLE_MAPS_KEY,
            loaded: false,
            map: null,
            markers: {},
            markerIds: {},
            page: 1,
            search: null
        }
    },
    mounted() {
        this.load();
    },
    methods: {
        fetchMarkers(postData) {
            return axios.post('find-markers');
        },
        fetchMarkersInBounds(mapBounds) {
            let that        = this;
            this.page       = 1;
            this.properties = [];

            this.fetchMarkers({
                'north' : mapBounds.north,
                'south' : mapBounds.south,
                'east'  : mapBounds.east,
                'west'  : mapBounds.west,
            })
            .then(response => {
                let {data: {data: markers, links}} = response;

                if (links.prev === null) {
                    that.page = 1;
                }

                if (markers.length > 0) {
                    that.page += 1;
                    that.loadMarkers(markers);
                }

                that.links = links;

                if (typeof that.links.next !== 'undefined' && that.links.next !== null) {
                    that.fetchMarkersInBounds(mapBounds);
                }
            });
        },
        async load() {
            this.loaded = true;

            await GoogleMaps.load(this.googleMapsKey, "EN", 'en');

            this.initMap();
        },
        loadMarkers(markers) {
            for (let i = 0; i < markers.length; i++) {
                let marker = markers[i];

                this.markerIds[marker.id] = true;

                if(marker.hasOwnProperty('coordinates') && !this.markers.hasOwnProperty(marker.id)) {
                    this.createMarker(marker);
                }
            }
        },
        createMarker(item) {
            let position = new google.maps.LatLng(item.coordinates.lat, item.coordinates.lon);

            this.markers[item.id] = new google.maps.Marker({
                position: position,
                map: this.map
            });
        },
        initMap() {
            this.map = new google.maps.Map(document.getElementById('map'), {
                zoom  : this.zoom,
                center: new google.maps.LatLng(this.lat, this.lon),
                streetViewControl: false,
                mapTypeControl: false,
                fullscreenControl: false
            });

            this.map.addListener('bounds_changed', () => {
                this.boundsChanged();
            });
        },
        boundsChanged: debounce(function() {
            this.fetchMarkersInBounds(this.map.getBounds().toJSON());
        }, 500),
    }
};

Я разобью всё это на составляющие компоненты.

async load() {
    await GoogleMaps.load(this.googleMapsKey, "EN", 'en');this.loaded = true;
    this.initMap();
},

У меня есть небольшой хелпер GoogleMaps, написанный Патриком Брауэрсом. Он загружает библиотеку GoogleMaps в промис (promise), и мы сможем, при необходимости, её подгрузить. Это включено в демо код, но я не буду вдаваться в подробности.

Функция загрузки вызывается один раз через хук mounted. Затем initMap загружает фактическую карту.

initMap() {
    this.map = new google.maps.Map(document.getElementById('map'), {
        zoom  : this.zoom,
        center: new google.maps.LatLng(this.lat, this.lon),
        streetViewControl: false,
        mapTypeControl: false,
        fullscreenControl: false
    });    
    
    this.map.addListener('bounds_changed', () => {
        this.boundsChanged();
    });
}

Большая часть этого кода — ванильный JavaScript, потому что так работает библиотека карт. Но если вы часто используете Карты, вы можете использовать или создать специальный компонент для этого. Несколько есть на Github.

Мы инициализируем экземпляр google.maps.Map и привязываем его к нашему экземпляру компонента. Широта и долгота, которые мы получаем из шаблона, используются для центрирования карты. Масштаб также может быть установлен через параметры, но по умолчанию он равен 16.

Затем мы добавляем слушателя к событию bounds_changed. Google Maps будут запускать это событие каждый раз, когда видимые границы будут изменены. Это происходит, когда карта загружена, изменяется размер или когда вы прокручиваете или масштабируете карту. Когда это происходит, мы запускаем наш метод boundsChanged:

boundsChanged: debounce(function() {
    this.fetchMarkersInBounds(this.map.getBounds().toJSON());
}, 500),

Он извлечет границы из экземпляра карты и преобразует их в JSON для использования в методе fetchMarkersInBounds.

fetchMarkersInBounds(mapBounds) {
    let that        = this;
    this.page       = 1;
    this.properties = [];    this.fetchMarkers({
        'north' : mapBounds.north,
        'south' : mapBounds.south,
        'east'  : mapBounds.east,
        'west'  : mapBounds.west,
    })
    .then(response => {
        let {data: {data: markers, links}} = response;        
        
        if (links.prev === null) {
            that.page = 1;
        }        
        
        if (markers.length > 0) {
            that.page += 1;
            that.loadMarkers(markers);
        }        that.links = links;        
        
        if (typeof that.links.next !== 'undefined' && that.links.next !== null) {
            that.fetchMarkersInBounds(mapBounds);
        }
    });
},

Он возьмет объект mapBounds, который мы только что извлекли из экземпляра карты, и передаст его через метод fetchMarkers. Затем, когда промис этого метода завершится, он добавит маркеры в экземпляр компонента и установит ссылки и страницу соответственно (как вы видели ранее, мы используем пагинацию).

fetchMarkers(postData) {
    if(this.markerIds) {
        postData.markerIds = Object.keys(this.markerIds);
    }    return axios.post('find-markers', postData);
},

Метод fetchMarkers просто добавляет уже существующие маркеры в postData и использует axios для запроса нового списка маркеров из API endpoint, которое мы создали ранее.

loadMarkers(markers) {
    for (let i = 0; i < markers.length; i++) {
        let marker = markers[i];        
        
        this.markerIds[marker.id] = true;        
        
        if(marker.hasOwnProperty('coordinates') && !this.markers.hasOwnProperty(marker.id)) {
            this.createMarker(marker);
        }
    }
},

Метод loadMarkers перебирает маркеры, полученные от API, затем добавляет идентификаторы в список (который мы используем в fetchMarkers чтобы убедиться, что не получаем один и тот же маркер дважды). Затем он проверяет, есть у маркера координаты, не использованы ли они уже, и если оба эти утверждения верны, то создает маркер на карте.

createMarker(item) {
    let position = new google.maps.LatLng(item.coordinates.lat, item.coordinates.lon);    
    
    this.markers[item.id] = new google.maps.Marker({
        position: position,
        map: this.map
    });
},

Наконец мы достигли момента, когда маркер будет отображен на самой карте. Мы привязываем созданный маркер к this.markers чтобы сохранить ссылку на все маркеры на случай, если вам понадобится доступ к ним позже, например, если понадобиться удалить их с карты.

Несколько примечаний:

  • Фактический демо код также содержит метод для выполнения поиска по местоположению, который будет центрировать карту по найденному местоположению, и, поскольку это изменяет границы карты, он инициирует вызов API для поиска маркеров в этой области.
  • Я использовал tiny-debounce в методе boundsChanged, чтобы минимизировать количество обращений к API. Вы также можете использовать версию lodash или любой другой подобный метод. Или даже написать небольшую реализацию setTimeout.
  • Я предполагаю, что вы понимаете, как работает API Google Maps, поэтому я не объяснил такие вещи, как new google.maps.Marker или new google.maps.LatLng
  • Мы передаем ключ API карт из файла .env через process.env.MIX_GOOGLE_MAPS_KEY. Это базовая магия Laravel + Vue + Mix. Я не имею к этому никакого отношения.

Конечный результат

Пройдя все шаги мы получим:

Поиск по полигональным картам

Посмотрите демо проект на Github. Не стесняйтесь обращаться ко мне за вопросами по любым деталям.

Автор: Jochem Fuchs
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.