Как стать автором
Обновить

Что делать, если ваша карта выглядит так?

Время на прочтение 7 мин
Количество просмотров 33K
Ezem.ru - Земельный участки


Приветствую, меня зовут Влад. Я ведущий разработчик проекта Ezem.ru. На картинке сверху вы видите все участки размещенные нашими пользователи в Московской области (8000+ участков). Я хочу вам рассказать как мы решили эту задачу и какие подводные камни были на нашем пути.

Главная проблема в том, что когда у нас на карте более 500 объектов — среднестатистический офисный компьютер начинает жутко тормозить и картой пользоваться становится трудновато. Помимо основной проблемы, если выводить все участки, невозможно кликнуть на какой-то конкретный маркер, а в Москве образовалась просто какая-то черная дыра :) Мы полезли смотреть, как эту задачу решили другие пользователи Google Maps API. GPS-Club: POI: Камеры скорости, Где этот дом, МирТесен, Пушкино.org (первое что пришло на ум), либо не решили проблему с «тормозами», либо просто ограничивают кол-во объектов на экране. И проблема тут скорее не в google maps api, а в самой DOM модели, которая в убийце интернета (IE 6) уж очень тормозная.

Костыли 1. Использовать стандартные средства.
Взгляд пал на GMarkerManager. Принцип его работы прост. Подгружать в DOM только те маркеры, которые мы сейчас видим на экране, тем самым уменьшая кол-во объектов DOMа и облегчая, и без того нелегкую, жизнь браузеру…
Но посмотрев более детально на документацию был немного confused:
This class is deprecated; developers are recommended to use the open sourced MarkerManager instead.



Костыли 2. Использовать сторонние средства.
Побродив по интернету и поспрашивав у прохожих, я наткнулся на замечательную библиотеку с не менее замечательным названием "MarkerManager". Принцип ее работы такой же как и у GMarkerManager, но с небольшим отличием. Там есть функции удаления всех маркеров на экране и удалении конкретного маркера. Данный костыль вменяемо работал, но только до тех пор, пока кол-во маркеров не перевалило за 3 000. Очередной камень заключался в том, что маркеры загружались при первом заходе на страницу, и на их обработку (200кб XML), на стороне клиента, уходило довольно-таки много времени. В общем, было решено жонглировать AJAX запросами и вытаскивать ТОЛЬКО маркеры видимой области и ТОЛЬКО того зума, на котором мы сейчас находимся. Данный вариант, с небольшими поправками и работает на нашем сайте.


Костыли 3. Группировка маркеров.
Как я говорил выше, чем больше у нас маркеров сконцентрировано в одной точке, — тем сложнее пользоваться картой. Действительно, попасть по какому-то конкретному маркеру, иногда, стало просто невозможно. Было принято решение уменьшить кол-во маркеров на экране и внедрить группировку маркеров на стороне сервера. Решение довольно-таки простое, но не слишком правильное. На каждый из зумов карты были подобраны правильные размеры прямоугольников (см. картинку справа) и маркеры, попадающие в эту область, помечались как дочерние и на этом зуме показывался только 1 из них. И так для каждого негруппированного маркера и для каждого зума. Плюсы этого подхода в том, что теперь стало возможно пользоваться картой. Но в этом подходе есть крупные недостатки: обсчет занимал около 12 минут и делался РАЗ в день. Мы побороли медлительность карты, но у нас встала другая проблема: так как обсчет маркеров делался раз в день, мы не могли внедрить группировку в наши фильтры. И браузер можно было легко поставить на колени, выбрав тип: «Индивидуальное жилищное строительство (только красные маркеры)» и размер участка от 0 до 50 соток.

Костыль 4. Динамическая группировка маркеров.
Мысль о том, что браузер можно поставить на колени 2-мя кликами мыши не давала нам спокойно спать и мы решили сделать более продвинутую версию группировки:

Вариант 1. Статичные квадраты
Идею подал Олег Волчков. Заранее просчитать ВСЕ возможные квадраты карты на ВСЕ возможные зумы. Сразу же оговорюсь, что коэффициент прямоугольника для каждого из зумов разный и мы считали ТОЛЬКО Россию. У нас получилось более 23 000 000 записей и база «поправилась» на 200 МБ. Преимущество этого подхода в том, что группировка маркеров проводилась более аккуратно, чем во втором варианте. Минусы в том, что данные надо было просчитать (заняло где-то одну ночь на девелоперской тачке) и уже просчитанные данные необходимо было выделить в отдельную БД, а это потянуло бы за собой некий рефакторинг.

Вариант 2. Группировка только по видимой области
Автор этой идеи Сергей Колчин. Каждый раз, когда мы двигаем карту, мы передаем на сервер координаты видимой нам области, — так почему-же не делить только этот прямоугольник и группировать маркеры в реальном времени? Минусом в этом случае является то, что при смещении на небольшие расстояния маркеры будут немного смещаться. Это мы посчитали некритичной проблемой потому-что, во-первых: маркеры не настолько сильно смещаются, во-вторых: пользователь обычно сразу приближает интересующий его участок, а не путешествует по карте. Этот вариант, ввиду того что он проще и работает быстрее 1-го и используется у нас на сайте по сей день.

Немного концептуального кода:

Пример запроса: http://ezem.ru/gmap/getmarkers/?appoi..

Подхаченный markermanager
/* Copyright (c) 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Version: 1.0
 * Author: Doug Ricket, others
 *
 * Marker manager is an interface between the map and the user, designed
 * to manage adding and removing many points when the viewport changes.
 *
 *
 * Algorithm: The MM places its markers onto a grid, similar to the map tiles.
 * When the user moves the viewport, the MM computes which grid cells have
 * entered or left the viewport, and shows or hides all the markers in those
 * cells.
 * (If the users scrolls the viewport beyond the markers that are loaded,
 * no markers will be visible until the EVENT_moveend triggers an update.)
 *
 * In practical consequences, this allows 10,000 markers to be distributed over
 * a large area, and as long as only 100-200 are visible in any given viewport,
 * the user will see good performance corresponding to the 100 visible markers,
 * rather than poor performance corresponding to the total 10,000 markers.
 *
 * Note that some code is optimized for speed over space,
 * with the goal of accommodating thousands of markers.
 *
 */

Файл большой, и я не стал его выкладывать. Вы можете его посмотреть по ссылке http://ezem.ru/js/gmap/markermanager.js


Функция обработчик

    public function getmarkersAction()
    {
        $params = array();
        foreach (array('zoom', 'area_min', 'area_max', 'units') as $k) {
            $params[$k] = (int)$this->_getParam($k, 0);
        }
        foreach (array('lat1', 'lat2', 'lng1', 'lng2') as $k) {
            $params[$k] = round((float)$this->_getParam($k, 0.0), 4);
        }
        $params['no_groups'] = ('true' == $this->_getParam('no_groups', null));
        $params['deal_type'] = ('sell' == $this->_getParam('deal_type', null)) ? 'sell' : 'buy';
        $params['appointment'] = explode(',', trim($this->_getParam('appointment', '')));
        $params['except_objects'] = explode(',', trim($this->_getParam('except_objects', '')));

        $dbWhere = Medialab_Items::getActiveItemsLimits();
        if (@$this->user->id) {
            $dbWhere = array('(('.implode(' AND ', $dbWhere).') OR o.uid = '.$this->user->id.')');
        }
        $dbWhere[] = "`deal`.`type` = '".$params['deal_type']."'";
        $dbWhere[] = '`marker`.`is_polygon` = 0';

        $markers = Medialab_Gmap_Marker::getMarkersDynamic($params, $dbWhere);

        $qty = 0;
        $s = "<m>\n";
        foreach ($markers as $marker) {
            $isGroup = (1 < $marker['qty']);
            $s .= '<m i="'.$marker['id'].'" '
                .'o="'.$marker['object_id'].'" ' <BR />
                .'t="'.$marker['lat'].'" ' 
                .'g="'.$marker['lng'].'" ' 
                .'q="'.$marker['qty'].'" ' 
                .'a="'.($isGroup ? 'group' : $marker['appointment_type']).'"/>'."\n";
            $qty += $marker['qty'];
        }
        $s .= '<info count="'.$qty."\"/>\n";
        $s .= '</m>';

        header('Content-Type: text/xml; charset=windows-1251');
        exit($s);
    }


УБЕРИТЕ BR из кода. Пришлось его вставить, иначе хабрапарсер глючит. Не знаю почему, но без него он вытягивает кусок кода в одну строку и появляется скроллинг.

Класс, который занимается всем грязным :)
class Medialab_Gmap_Marker
{
    /* *
     * Размер блока в градусах для масштаба 0.
     * 72x128 задаёт 6x6 блоков в видимой области
     * 64x96 -- 8x8
     * * */
    public static $blockSize = array(
        'lat'  => 64.0,
        'lng'  => 96.0
    );

    /**
     * Calculates map block dimensions for the provided zoom
     *
     * @param int $zoom Map zoom
     * @return array Block dimensions in lat/lng
     */
    public static function getBlockSize($zoom = 10)
    {
        $rect = array();
        foreach (array('lat', 'lng') as $k) {
            $rect[$k] = round(self::$blockSize[$k] / (1 << $zoom), 4);
            $rect[$k.'_half'] = round($rect[$k] / 2, 4);
        }
        return $rect;
    }

    /**
     * Fetches markers for visible map area, dynamically grouping them if needed
     *
     * @param array $params Array of search params, provide at least (lat, lng) pairs
     *                      for top left/bottom right corners, and current zoom.
     * @param array $dbWhere Additional SQL query conditions (optional)
     * @return array Found items
     *
     * TODO: SW/NE corners instead of top left/bottom right for better consistence w/ Google Maps API
     */
    public static function getMarkersDynamic(array $params, array $dbWhere = array())
    {
        $block = self::getBlockSize($params['zoom']);

        $dbWhere[] = '(`marker`.`lat` BETWEEN '.$params['lat1'].' AND '.$params['lat2'].')';
        // Longitude 180 -> -180 degrees wrap workaround
        if ($params['lng2'] < $params['lng1']) {
            $dbWhere[] = '((`marker`.`lng` BETWEEN '.$params['lng1'].' AND 180.0) OR (`marker`.`lng` BETWEEN -180.0 AND '.$params['lng2'].'))';
        } else {
            $dbWhere[] = '(`marker`.`lng` BETWEEN '.$params['lng1'].' AND '.$params['lng2'].')';
        }
        if (($params['area_min'] || $params['area_max']) && $params['units']) {
            $unit = DB::FindFirst('ezem_units', array('rate'), array('id' => $params['units']));
            if ($params['area_min']) {
                $dbWhere[] = '`o`.`area` >= '.($params['area_min'] * $unit['rate']);
            }
            if ($params['area_max']) {
                $dbWhere[] = '`o`.`area` <= '.($params['area_max'] * $unit['rate']);
            }
        }
        $a = $params['appointment'];

        if (count($a) && $a[0]) {
            $dbWhere[] = "`appointment`.`type` IN ('".implode("', '", $a)."')";
        }
        $a = $params['except_objects'];
        if (count($a) && $a[0]) {


            $dbWhere[] = '`o`.`id` NOT IN ('.implode(', ', $a).')';
        }

        $dbGroupBy = '`marker`.`id`';
        if (($params['zoom'] < 13) && !$params['no_groups']) {
            // Group only for zooms <= 12 and with no 'no_groups' flag set
            $dbGroupBy = '`grp_lat`, `grp_lng`';
        }

        $query = '
            SELECT
                `marker`.`id`,
                `marker`.`object_id`,
                `appointment`.`type` AS `appointment_type`,
                AVG(`marker`.`lat`) AS `lat`,
                AVG(`marker`.`lng`) AS `lng`,
                FLOOR((`marker`.`lat` - '.$params['lat1'].') / '.$block['lat'].') AS `grp_lat`,
                FLOOR((`marker`.`lng` - '.$params['lng1'].') / '.$block['lng'].') AS `grp_lng`,
                COUNT(`marker`.`id`) AS `qty`
            FROM
                `ezem_gmap` AS `marker`
                JOIN `ezem_object` AS `o` ON (`o`.`id` = `marker`.`object_id`)
                JOIN `ezem_appointment` AS `appointment` ON (`appointment`.`id` = `o`.`appointment_id`)
                JOIN `ezem_deal` AS `deal` ON (`deal`.`id` = `o`.`deal_id`)
            WHERE
                '.implode(' AND ', $dbWhere).'
            GROUP BY
                '.$dbGroupBy;
        return DB::Query($query);
    }
}


Портал реализован на Zend Framework, Smarty, Yandex Server и Jquery. Работающий пример того, о чем я говорил, можно посмотреть у нас на сайте.

p.s. В планах на ближайшее будущее: добавить поддержку полигонов и заменить Yandex search на sphinx, но об этом уже в следующих статьях.
p.p.s Мы до сих пор ищем толковых программистов.
Теги:
Хабы:
+102
Комментарии 81
Комментарии Комментарии 81

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн