Как показать на Google Maps объекты, находящиеся в заданной области

Владимир | | HTML, JavaScript, Web разработка.

google maps

Показать пример решения такой задачи меня попросили в комментариях к одной из предыдущих статей о Google Maps. С выполнением этой просьбы я довольно сильно затянул, но, надеюсь, эта статья кому-нибудь пригодится 🙂

Начнём с постановки задачи

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

Будем считать, что заданная область представляет собой окружность, т.е. мы знаем координаты её центра и радиус. А координаты объектов находятся в KML файле (это XML формат, разработанный для описания объектов на Google maps).

Таким образом, алгоритм расстановки объектов получается довольно простым.

1) Читаем координаты объекта из KML файла.
2) Рассчитываем расстоянием между центром окружности и текущим объектом.
3) Если это расстояние меньше радиуса окружности, ставим маркер, если больше – пропускаем объект.

Переходим к решению.

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

Source

Серверная часть

Начнем с серверного скрипта, который формирует страницу с картой (index.php).

<?php
$organizations = simplexml_load_file('doc.kml');

$organizations_data = array();

foreach ($organizations->Folder->Placemark as $organisation) {
    $organizations_data[] = array(
        'name' => (string)$organisation->name,
        'coordinates' => (string)$organisation->Point->coordinates,
    );
}
?>

<!DOCTYPE html>

<html> <head> <meta charset="UTF-8" /> <title>Объекты на Google Maps</title> <script> <?php echo 'var organizations = '.json_encode($organizations_data).';'; ?> </script> </head> <body> <h1>Google Maps: отображение объектов, которые находятся в заданной области</h1> <div id="my_map" style="width:600px;height:400px"></div> <script src="main.js"></script> </body> </html>

Прежде всего, загружаем данные из KML файла (строка 2). Т.к. это обычный XML файл, то мы можем использовать любую библиотеку для работы с этим форматом. В данном случае – SimpleXML.

Затем, необходимо обеспечить доступ к этим данным на клиентской стороне, т.к. настройка Google Maps выполняется с помощью JavaScript.

Для этого мы формируем массив с необходимыми данными (строки 6-11), преобразовываем его в JSON формат (строка 22) и присваиваем переменной organizations (строка 22) в теге script. Таким образом, мы получаем JavaScript массив с координатами всех объектов из KML файла.

Тут я хочу сделать небольшое отступление. В принципе, работать с KML файлом можно и на стороне браузера, это несложно. Но в общем случае, этот файл не обязательно будет находиться на вашем сервере, а кросс-доменные (XSS) запросы запрещены политикой безопасности браузеров. Т.е. придётся искать какой-нибудь обходной путь. Подробнее на эту тему можно почитать здесь, здесь, здесь и здесь.

Подробно разбирать работу с SimpleXML, думаю, смысла не имеет. Для данного примера достаточно понимать, что после загрузки файла вы получите объект, имеющий такую же структуру как и xml-документ.

Т.е. вызов $organizations->Folder->Placemark вернет вам содержимое тега <Placemark> в документе с такой структурой

<kml xmlns="…" xmlns:kml="…" xmlns:atom="…">
<Folder>
	<Placemark>
	…
	</Placemark>
</Folder>
</kml>

Обратите внимание! Библиотека чувствительна к регистру тегов в xml-файле.

На самой странице мы создаём блок для карты (my_map).

И подключаем файл main.js, который и будет выполнять настройку карты.

Клиентская часть

Сразу посмотрим на весь скрипт целиком.

var map, circle, circleOptions, setCenter, marker, organizations_markers;

function initialize() {
    var centerPoint = new google.maps.LatLng(48.67, 44.47); //Волгоград
    var myOptions = {
        zoom: 12,
        center: centerPoint,
        mapTypeId: google.maps.MapTypeId.HYBRID
    }
    map = new google.maps.Map(document.getElementById("my_map"), myOptions);
    
    //радиус окружности - 5 км
    var radius = 5;
    
    circleOptions = {
        center: centerPoint,
        fillColor: "#00AAFF",
        fillOpacity: 0.5,
        strokeColor: "#FFAA00",
        strokeOpacity: 0.8,
        strokeWeight: 2,
        clickable: false,
        radius: radius*1000
    }

    //рисуем окружность
    circle = new google.maps.Circle(circleOptions);
    circle.setMap(map);

    //устанавливаем маркеры организаций
    organizations_markers = [];
    for (var i = 0; i < organizations.length; i++) {
        var ll = organizations[i].coordinates.split(',');
        var latlng = new google.maps.LatLng(ll[1], ll[0]);
        if (distHaversine(latlng, centerPoint) < radius) {
            organizations_markers[i] = new google.maps.Marker({
                    position:latlng,
                    clickable:true
                });
            organizations_markers[i].setMap(map);
        }
    }
}
  
function loadScript() {
    var script = document.createElement("script");
    script.src = "http://maps.google.com/maps/api/js?sensor=false&callback=initialize";
    document.body.appendChild(script);
}

rad = function(x) {return x*Math.PI/180;}

//эта функция используются для определения расстояния между точками на
//поверхности Земли, заданных с помощью географических координат
//результат возвращается в км
distHaversine = function(p1, p2) {
    var R = 6371; // earth's mean radius in km
    var dLat  = rad(p2.lat() - p1.lat());
    var dLong = rad(p2.lng() - p1.lng());
    
    var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
            Math.cos(rad(p1.lat())) * Math.cos(rad(p2.lat())) * Math.sin(dLong/2) * Math.sin(dLong/2);
    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    var d = R * c;
    
    return d.toFixed(3);
}

window.onload = loadScript;

В первую очередь мы создаём карту и устанавливаем её основные параметры (координаты центра, приближение). Для этого, назначаем обработчик событию window.onload (строка 69). Этот обработчик добавляет на страницу тег script в параметре src которого передается адрес имя метода, который будет вызван после загрузки Google Maps (в данном случае — initialize). Таким образом, все наши настройки выполняются в этом методе.

Выполняем установку параметров карты (строки 4-10). Мы указали: центральную точку (centerPoint), приближение (zoom) и тип карты (mapTypeId).

Теперь рисуем окружность (строки 13-28). Её центр совпадает с центром карты, а радиус равен 5 км. В настройках окружности указываем толщину и цвет линии, цвет и прозрачность фона и другие параметры.

После этого расставляем маркеры объектов.

Напомню, что координаты этих объектов находятся в массиве organizations, который был сформирован серверным скриптом.

Т.е. нам нужно пройтись по всем элементам этого массива и рассчитать расстояние от центра окружности до текущего объекта (строки 31-42).

Расчет расстояний для точек на поверхности Земли выполняется с помощью функции гаверсинусов (distHaversine). Её реализацию на JavaScript я взял здесь.

Если это расстояние меньше заданного, создаём маркер и показываем его на карте (строки 36-40). В противном случае – переходим к следующему объекту.

Как видите, алгоритм достаточно простой. Если немного усовершенствовать скрипт, можно сделать окружность перемещаемой и с переменным радиусом. Таким образом, имея хорошую базу объектов для какого-нибудь города, можно получить довольно удобный инструмент определения ближайших к заданной точке объектов.

Если есть вопросы или замечания, пишите, постараюсь ответить в комментариях 😉

Успехов!

Интересно почитать

Хотите создать свой сайт? Template CMS — быстрая и маленькая CMS! Поможет решить эту задачу с минимальными затратами времени и ресурсов.

В раздумьях, что подарить на свадьбу друзьям? Попробуйте свадебную фотокнигу.

Заполним 3-ндфл за 2011 год за 1 день, от 500 рублей!

  • wordpresser

    Спасибо, по этой теме очень мало информации в сети.

    Не попадался ли вам северный вариант такого скрипта?
    У меня на http://www.blog-map.ru больше 5 тыс. маркеров и даже сама загрузка их из xml занимает немало времени, думаю определение какие маркеры находятся в зоне видимости на js будет работать не очень быстро при таком количестве маркеров.

    • Думаю, в вашем случае, будет лучше всего сгенерировать kml файл с нужными маркерами. И показать его на карте как KML Layer.

      Иначе браузер может «тормозить» при таком количестве маркеров.

    • wDevil

      http://api.2gis.ru/about/features/#maps — у нас в js api карт это из коробки идет.

  • Алексей

    Подскажите, а как сделать так, чтобы клиентская часть передавала серверной части (например через AJAX) только координаты пользователя. А уже серверная часть считала, какие объекты есть в округе и уже выдавала ответом информацию об объектах в радиусе ???

    • На данный момент 100% надежного способа сделать это нет. Дело в том, что браузеры передают информацию о размещении только с разрешения пользователя. Попробуйте выполнить этот код:
      navigator.geolocation.getCurrentPosition(function(position) {
      console.log(position.coords.latitude + '; ' + position.coords.longitude);
      });

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

      Подробнее здесь.

      • Алексей

        Спасибо большое за оперативный ответ, только я другое имел ввиду: после того как пользователь разрешит браузеру определить координаты, передавать через ajax position.coords серверу, а уже серверная часть чтобы фильтровала объекты в заданном радиусе. Ну то есть я просто не хочу каждый раз клиенту показывать базу объектов, это неправильно. Я пытался на сервере организовать реализацию distHaversine(latlng, centerPoint), но не могу понять, какие параметры передавать в функцию, т.е latlng и centerPoint

        • На сервере у вас больше возможностей с точки зрения работы с координатами. Т.е. вы можете реализовать функцию distHaversine немного иначе.

          1) отправляете координаты точки серверу

          $.ajax('my_script.php', {coords: point}, ….)

          2) формируете список объектов. Реализация этой части зависит от того где и в каком формате хранятся координаты объектов. Один из самых удобных вариантов — когда широта и долгота хранятся в базе данных, в отдельных полях типа float. В этом случае если вы зададите координаты центра и радиус, то сможете получить список всех объектов с помощью одного запроса к базе.

          SELECT id, ( 3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) * cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) * sin( radians( lat ) ) ) ) AS distance FROM markers HAVING distance < 25 ORDER BY distance

          Подробно читайте здесь.

        • Алексей

          Спасибо за наводку! Буду разбираться!

  • Андрей Витовтов

    Здравствуйте! Подскажите пожалуйста, при клике добавляется окружность заданного размера и на координаты попавшие в область ставится маркер. При следующему клику окружность перемещается, но маркер остается. Помогите убрать маркер при клике, если маркер не попадает в окружность.
    https://jsfiddle.net/Andrey_Vitovtov/h13q1yt7/

    • Получилось как-то так http://jsfiddle.net/vladimir_s/mnmo2atj/10/

      • Андрей Витовтов

        Огромное спасибо! Я вродибы тоже так делал но у меня почему-то не работало, значит что-то не так делал:)

        И еще вопрос если массив:
        objects_markers = [];
        infowindow = [];
        for (var i = 0; i < objects.length; i++) {
        var ll = objects[i].coordinates.split(',');
        var latlng = new google.maps.LatLng(ll[0], ll[1]);
        var address = objects[i].address;
        if (distHaversine(latlng, circleOptions.center) < radius) {
        infowindow[i] = new google.maps.InfoWindow({
        content: address
        });
        objects_markers[i] = new google.maps.Marker({
        position:latlng,
        clickable:true,
        map: map,
        title: 'title',
        animation: google.maps.Animation.DROP,
        visible: true,
        icon: 'images/beachflag.png'
        });
        objects_markers[i].addListener('click', function() {
        infowindow[i].open(map, objects_markers[i]);
        });
        }
        }
        Почему не работают infowindow и как теперь убрать добавленные маркеры

        • В обработчике клика проверьте состояние объектов infowindow[i] и objects_markers[i] (существуют или нет).

          А удаляются макреры точно так же:
          objects_markers[i].setMap(null);

        • Андрей Витовтов

          Все равно никуда не деваются:((
          https://jsfiddle.net/Andrey_Vitovtov/h13q1yt7/3/

        • Хорошо, только сначала один вопрос. js-фидлы, которые Вы создавали, у Вас работают? Когда я их открываю, то вижу такую картинку (прикрепил к комментарию). В прошлый раз я взял свою заготовку в js-фидл для карты и копировал в неё Ваш код, но это не удобно. И потом сложно сравнивать изменения.

        • Андрей Витовтов

          Не работают!:(
          А вообще у меня работает: http://test.xn--80akhmlofgv.net/google_maps/circle.php
          Информация о объектах хранится в MySQL
          Код тот же
          https://jsfiddle.net/Andrey_Vitovtov/h13q1yt7/4/

        • 1) На странице http://test.фрилансер.net/google_maps/circle.php есть как минимум одна ошибка в работе с базой данных (Warning: mysql_fetch_array() expects parameter 1 to be resource, boolean given). Это предупреждение, но оно означает, что перед этим mysql_query не смогла отправить запрос. Эту проблему нужно устранить перед тем как двигаться дальше.

          2) Попробуйте вставить Ваш код для работы с массивами маркеров в мой пример jsfiddle (который я отправил в прошлый раз). Есть хорошие шансы, что он заработает 🙂 Если нет — присылайте мне ссылку на этот jsfiddle.

        • Андрей Витовтов

          Ошибку исправил, переносил из локального, не успел импортировать базу.
          http://jsfiddle.net/Andrey_Vitovtov/mnmo2atj/11/

        • Сейчас работает http://jsfiddle.net/vladimir_s/eqL40fdd/1/

        • Андрей Витовтов

          Супер!
          Только infowindow не работает:((

        • Я думаю, Вы исправите 🙂

        • Андрей Витовтов

          Вы ошибаетесь:(

        • Там была типичная для JS ошибка — использование внешней переменной внутри обработчика события.
          http://jsfiddle.net/vladimir_s/eqL40fdd/2/

        • Андрей Витовтов

          Большое спасибо!!!