Route Me — альтернатива встроенному Google Maps контролу из iPhone SDK 3.0+

    image

    Я уже устал от ограниченности встроенного контрола карт, даже скорее от прикручивания костылей. Постой пример: Google Maps app маршрут показывать умеет, a контрол не умеет. Приходится рисовать своими силами поверх карты.

    Сейчас передо мной стоит конкретная задача: надо добавить отображение пройденного маршрута и его экспорт (share) в мое скромное приложение GPS Speed, которое, кстати, уже который день висит в Топ 30 американского App Store в разделе Navigation. Причина для меня остается загадкой, потому как приложение среднее и уникальностью не блещет.

    Возвращаюсь к проблеме. Нужный функционал уже реализованПочти рабочие костыли для Google Maps уже сделаны, но это хороший шанс пролить свет на альтернативы Google Maps для iPhone SDK.

    Мат часть


    Еще когда не было официального контрола карт, я работал с такими «заменителями»:

    Route Me:
    1. BSD лицензия (решать вам хорошо это или плохо)
    2. поддержка многих источников карт: OpenStreetMap, Microsoft Virtual Earth, Cloud Made, Yahoo Maps и другие менеее популярные
    3. сформированное активное сообщество разработчиков — Google Group

    Cloud Made:
    1. часть большого проекта и имеет официальную команду разработки (воспользуюсь случаем и передам привет Диме ;), потому потенциально имеет большую интеграцию с картами Cloud Made
    2. имеет много тем (цветовые наборы) для карт
    3. использовал весной, было еще сырое

    Хочу начать цикл статей с чего то простого, например, встроить карты в приложение и дать пользователю возможность выбирать источник карт. Я предпочитаю Route Me, потому как, кроме всего остального, он может показыват и Cloud Made.

    Подготовка

    1. Откройте xCode и создайте новый проект. Я выбрал View-Based Application и назвал его RouteMeSourceSelection
    2. Загрузите последнюю версию Route Me
    3. Установите контрол шаг за шагом по этому гайду

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

    image

    Добавляем контрол (код)


    Открываем хедер UIViewController (мой называется «RouteMeSourceSelectionViewController.h») и:
    1. #import «RMMapView.h»
    2. добавляем протокол «RMMapViewDelegate»
    3. создаем IBOutlet RMMapView *mapView;

    image

    Переопределяем метод "- (void)viewDidLoad":
    - (void)viewDidLoad {
    [super viewDidLoad];

    [RMMapView class]; // важно! без этого карта не покажется. стеснительная.
    [mapView setDelegate:self];
    }


    Добавляем контрол (дизайн)


    1. Открываем .xib file в Interface Builder (IB)
    2. Добавляем UIView (subview) в уже существующий и определяем его, как RMMapView
    3. Соединяем IBOutlet из нашего UIViewController с только что созданным RMMapView

    Понимаю, что трудно воспринимать такой текст. Короче, должно выглядеть вот так:
    image

    Теперь, запустив приложение, вы должны увидеть карту с источником по умолчанию — Open Street Maps. Я вижу такое, надеюсь вы тоже:
    image

    Все это было очень легко. Давайте дадим пользователю возможность выбирать источник карты. Для этого разместим контрол UIPickerView и свяжим его с нашим UIViewController.

    UIPickerView для выбора источника карты (код)


    Октрываем хедер контролера и добавляем два протокола UIPickerViewDelegate и UIPickerViewDataSource, и несколько IBOutlet:
    @interface RouteMeSourceSelectionViewController : UIViewController
    <RMMapViewDelegate,
    UIPickerViewDelegate, UIPickerViewDataSource>
    {
    IBOutlet RMMapView *mapView;
    IBOutlet UIPickerView *mapSourcePicker;
    IBOutlet UIBarButtonItem *mapSettingsBarButton;
    }

    - (IBAction) showMapsSettings;

    @end


    Теперь реализуем несколько обязательных методов для UIPickerView. Это просто, но будет полезным для тех, кто этого еще не делал:

    static NSArray *titles = nil;

    - (NSString *)pickerView:(UIPickerView *)pickerView
    titleForRow:(NSInteger)row
    forComponent:(NSInteger)component
    {
    return [titles objectAtIndex:row];
    }

    - (NSInteger)pickerView:(UIPickerView *)pickerView
    numberOfRowsInComponent:(NSInteger)component
    {
    if (!titles)
    {
    titles = [[NSArray alloc] initWithObjects:
    @"Open Street Maps",
    @"Yahoo Map",
    @"Virtual Earth Aerial",
    @"Virtual Earth Hybrid",
    @"Virtual Earth Road",
    @"Cloud Made Map",
    nil];
    }

    return [titles count];
    }

    - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
    {
    return 1;
    }


    Создадим метод showMapsSettings для того, что бы показывать и прятать UIPickerView, и еще для парочки UI трюков:

    - (IBAction) showMapsSettings
    {
    BOOL toShow = [mapSourcePicker isHidden];

    if (toShow)
    {
    [mapSettingsBarButton setStyle:UIBarButtonItemStyleDone];
    }
    else // hidding
    {
    [mapSettingsBarButton setStyle:UIBarButtonItemStyleBordered];
    [self setMapSourceWithNumber:[mapSourcePicker selectedRowInComponent:0]];
    }

    [mapSourcePicker setHidden:![mapSourcePicker isHidden]];
    [mapView setUserInteractionEnabled:[mapSourcePicker isHidden]];
    }


    А теперь самое главное — метод для изменения источника карты. Если были внимательны, то видели, что он вызывается в «showMapsSettings». Тут есть небольшой трюк: карта не обновляется автоматически при смене источника, потому приходится сдвигать и возвращать в прежнее положение. Это незаметно для пользователя. В самом конце сохраняем выбранный номер источника в общие настройки приложения.

    #import "RMVirtualEarthSource.h"
    #import "RMYahooMapSource.h"
    #import "RMCloudMadeMapSource.h"
    #import "RMOpenStreetMapsSource.h"

    #define CONST_MAP_KEY_bing @""
    #define CONST_MAP_KEY_cloud @""

    - (void) setMapSourceWithNumber:(int)number
    {
    if (mapSourceNumber == number)
    return;

    switch (number) {
    case 0:
    mapView.contents.tileSource = [[RMOpenStreetMapsSource alloc] init];
    break;
    case 1:
    mapView.contents.tileSource = [[RMYahooMapSource alloc] init];
    break;

    case 2:
    mapView.contents.tileSource = [[RMVirtualEarthSource alloc] initWithAerialThemeUsingAccessKey:CONST_MAP_KEY_bing];
    break;
    case 3:
    mapView.contents.tileSource = [[RMVirtualEarthSource alloc] initWithHybridThemeUsingAccessKey:CONST_MAP_KEY_bing];
    break;
    case 4:
    mapView.contents.tileSource = [[RMVirtualEarthSource alloc] initWithRoadThemeUsingAccessKey:CONST_MAP_KEY_bing];
    break;

    case 5:
    mapView.contents.tileSource = [[RMCloudMadeMapSource alloc] initWithAccessKey:CONST_MAP_KEY_cloud styleNumber:1];
    break;

    default:
    return;
    break;
    }

    // this trick refreshs maps with new source
    [mapView moveBy:CGSizeMake(640,960)];
    [mapView moveBy:CGSizeMake(-640,-960)];

    mapSourceNumber = number;
    // remember user choice between runnings
    [[NSUserDefaults standardUserDefaults] setInteger:mapSourceNumber forKey:@"mapSourceNumber"];
    }


    Для того, что бы заработали карты Cloud Made и Virtual Earth (Bing) надо получить «developer API key»:


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

    - (void)viewDidLoad {
    [super viewDidLoad];

    [RMMapView class];
    [mapView setDelegate:self];

    int number = [[NSUserDefaults standardUserDefaults] integerForKey:@"mapSourceNumber"];
    [self setMapSourceWithNumber:number];
    [mapSourcePicker selectRow:mapSourceNumber inComponent:0 animated:NO];
    }


    UIPickerView для выбора источника карты (дизайн)


    Открываем IB и:
    1. добавляем UIToolbar с одной кнопкой UIBarButtonItem, соединенной с ее IBOutlet из контроллера
    2. соединяем событие нажатия на UIBarButtonItem с showMapsSettings IBAction
    3. добавляем UIPickerView над картой и делаем ее скрытой (hidden)
    4. соединяем UIPickerView с его IBOutlet и устанавливаем его delegate и dataSource, как «file's owner»

    Опять же картинка заменит тысячу слов:
    image

    Если все сделано верно и ни я, ни вы ни чего не забыли, то вы увидите рабочее приложение:



    Все что было показано используется в реальном приложении — Meeting Point, оно бесплатное, попробуйте.

    Загрузить исходики можно тут
    Английский вариант статьи из моего тех.блога

    В комментариях предлагайте, что добавить и я буду расширять этот подробный гид:
    • Определения местоположения пользователя на карте и отображения на карте. Это сделаю точно, слишком просто.
    • Гео поиск. Еще не знаю, что это, но звучит круто
    • Предлагайте свое

    Комментарии 24

      +4
      Спасибо за статью. Route-Me действительно хорошая библиотека. Пользуясь случаем скажу несколько слов о CloudMade.
      Уже несколько месяцев CloudMade использует Route-Me для отрисовки карт. Mapzen POI Collector также использует Route-Me. В данный момент есть геокодинг для этой библиотеки от CloudMade, интеграция с Data Market Place и через несколько недель будет роутинг. Все вышеперечисленное является расширениями для Route-Me от CloudMade.
      П.С за привет тоже спасибо.
        +1
        отлично, уверен, что будут все использовать, так что ждите опусов.
        +3
        Object C меня убивает.
        Интересно, что Apple предпочитает минимализм в дизайне, а синтаксис такой перенасышенный :(
          +1
          Возражать трудно. Замечу только, что минимализм в коде жаждут не все. Читабильность куда важнее. За свою каръеру использовал около 10 языков.
          Давайте не будем пускаться в треп, слишком это субъективно и опытно.
            0
            «int number = [[NSUserDefaults standardUserDefaults] integerForKey:@»mapSourceNumber"];"
            Это пример читабельности? :)
            Да я просто так к языку придераюсь :)
              0
              придераюсь = придираюсь :)
                +2
                У меня нет уверенности, что пишу очень хорошо на objc. На кнопку «New Project» нажимал не больше сотни раз. Но как я понял, вы вообще не писали на objc. Очевидно, что прийдется немного почитать и пописать на новом языке прежде, чем общаться на равных с носителем. Ваши замечания на этой почве звучать довольно профанистически.
            –7
            оффтоп, конечно, но, имхо, любой уважающий себя программист должен быть еще и грамотным, знать, как пишется такое простое слово, как «костыль».
              +2
              Исправил. Не привык еще к chrome спелчекеру под Mac.
              Удачи в отплыве кармы за офтоп.
                –3
                за мою «карму» не беспокойтесь
                  –2
                  Ну прямо сами говорите: «Налетай» ;)
              0
              Очень не хватает нормального ПО для записи трека, все что есть не очень удобно. Ждем вашу программу.
                +1
                Пиши подробнее, что надо. Сделаем.
                  +1
                  Например что-то типо чекпоинтов. Что бы я мог по пути следования отмечать на треке остановки, банкоматы и т.п. Может такое уже где то есть, я не ставил дорогие программы.
                    0
                    Да эта проблема уже хорошо прорабатывается. Яндекс пробки делают это для автомобилистов. Для пеших тоже есть. Гляньте mapzen.cloudmade.com/mapzen-poi-collector.
                    Есть и другие. Названия лень искать, но их не трудно найти.
                +1
                Спасибо за статью — всегда приятно узнать про имеющиеся открытые альтернативы. Смотрел месяца 3 назад на Cloud Made'овскую библиотеку, был неприятно поражен чужеродностью. Кругом бросалось в глаза явное портирование с Plain C (к примеру методы без именнованых параметров). Приведенный здесь код выглядит приятнее, хотя на отрисовку маршрута неплохо было бы посмотреть.

                Незначительный вопрос/замечание по коду: после
                BOOL toShow = [mapSourcePicker isHidden];

                этот вызов используется еще пару раз. Это специально сделано? Потому что использование переменной вместо вызовов кажется предпочтительныи не только по соображениям производительности, но еще и читаемости.
                  0
                  Просто привык сообщениями все писать. Со временем дойду и до оптимизации, пока прерогатива на стиль.
                  Спасибо.
                  +1
                  Спасибо за статью очень интересно, и главное супер-актуально.
                  Встречный вопрос. Необходимо на Гугл карте нарисовать множество маршрутов. Рисуем маршруты в оверлее на карте. Все как бы хорошо, только беда скроллинг по карте и зум практически не юзабельны — уж слишком долго отрисовуются все координаты в маршрут на карте. Буду признателен за советы или помощь.
                    0
                    Да, эту проблему пока сам не решил.
                    Кривые маршрутов сами генерите или взяли где-то?
                      0
                      В принципе большого значения это не имеет, но делали кривые и из файла и из GPS.
                      А вообще, почему бы не создать отдельную ветку на Хабре посвященную мобильны картам? Тема очень актуальная в последнее время.
                        0
                        Нет, я имел в виду именно способ рисования поверх катры. От куда брали данные это не важно.
                          0
                          Прозрачный вид наладываем поверх карты, у него есть — (void)drawRect:(CGRect)rect
                          Порисовку вызываем через SetNeedsDisplay
                          Само рисование происходит через UIGraphicsGetCurrentContext
                            0
                            Ну да, а как передаете Touches карте?
                    0
                    Буду благодарен автору за помощь в том как получить координаты текущего местоположения с GPS модуля в CloudMade SDK.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое