Сграда Road Trip

История за това как се е развил един прост страничен проект.

Road Trip ви позволява да се възползвате максимално от пътуването си, като ви позволява да откриете страхотни места за посещение по пътя до вашата дестинация. Въвеждате началните и крайните местоположения в горната част и избирате какви типове места могат да представляват интерес за вас (напр. ресторанти, барове, развлечения и т.н.) Когато щракнете върху Генериране, пътуването ще създаде маршрут с интерактивни маркери, показващи места до посетете по време на пътуването до вашата дестинация. След това можете да щракнете върху всеки маркер, за да научите повече за всяко място.

Можете да играете с Road Trip на този уебсайт и също да видите източника.

За мен Road Trip трябваше да бъде прост страничен проект през уикенда, който използва съществуващи API, за да покаже идея. Вместо това се сблъсках с поредица от предизвикателства при работата с услуги за местоположение, което ме накара да внедря свои собствени решения, които използват персонализирани структури от данни и усъвършенствани конструкции на Javascript. Тук обяснявам как изградих Road Trip от началото до края и как се справих с предизвикателствата, пред които се изправих по пътя.

Внедряването на Road Trip със сигурност не е перфектно. Ако имате коментари, въпроси или обратна връзка относно тази публикация или кода, който написах, моля, изпратете ми имейл: [email protected]

Резюме

Тази публикация описва в дълбочина как обмислях и се справях с всяко предизвикателство от техническа гледна точка и гледна точка на разработване на продукти. Полезно е за всеки, който иска да надгради върху Google Maps, Yelp и API като цяло. По-долу е кратко резюме на това как изграждам Road Trip, което служи като ръководство за цялостната публикация:

  1. Използвах Google Places за автоматично попълване на начална и крайна дестинация за вашето пътуване. След това генерирах упътвания между двете места.
  2. Услугата за упътвания на Google Maps ви предоставя обект DirectionsResult, който описва упътванията. След няколко подхода използвах масива paths в обекта DirectionsStep, за да получа координатите на географската ширина и дължина по маршрута.
  3. Създадох свой собствен геокодер върху базата данни Geonames на градове, използвайки k-d дърво. За всяка координата на пътя потърсих най-близкия град. След това попълних хеш на всички уникални градове по вашия маршрут.
  4. За всеки град генерирах масив от Promise заявки към API за търсене на Yelp. Използвах Q, за да обобщя резултатите от всички тези заявки и след това върнах на потребителя всички фирми за всички уникални градове по маршрута.
  5. В браузъра обърнах геокодирах всеки бизнес адрес с Open Street Map, за да получа неговата географска ширина и дължина (които не се предоставят от Yelp) и го добавих като маркер на картата.
  6. Докато бизнесите на Yelp се геокодират обратно, потребителят се актуализира чрез лента за зареждане. Освен това, тъй като всеки адрес е успешно преобразуван, веднага го добавям към картата, за да покажа осезаемо на потребителя, че пътуването се генерира.

Конфигурацията

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

Основната идея за Road Trip е проста. Хората предприемат многодневни пътувания през цялото време. Когато спират обаче, обикновено е необходимо да си набавят газ или да си починат, което прави пътуването досадно и монотонно. Ами ако имаше инструмент, който да ви помогне да откриете страхотни места за посещение по пътя?

Знам, че трябваше да работя в рамките на текущото потребителско изживяване, когато създавах Road Trip. След известно проучване открих, че когато потребителите пътуват, те често получават своите упътвания от Google Maps. Ако искат да проучат бизнеси наблизо, те обикновено ще го потърсят в Yelp.

След това проучих и научих, че Google Maps има невероятен набор от инструменти за разработчици, които ви позволяват да създавате и персонализирате вашите карти. Потребителите могат да предоставят своето начално и крайно местоположение, а Google Maps може да ви предостави данни за упътвания между тези места. Към картата могат да се добавят и маркери, които могат да показват информация за тази точка. Научих също, че Yelp има страхотен, публично достъпен API, който ви позволява да търсите бизнеси по категория и местоположение и да получавате богати данни за всеки бизнес.

Ако измисля начин да използвам и двата инструмента заедно, тогава бих могъл да имам карта, която чертае бизнесите на Yelp по маршрут. Първата стъпка тогава беше да се създаде път от А до Б.

Получаване на упътвания

За щастие това беше най-лесната стъпка в изграждането на Road Trip. Google има страхотна документация за използването на своите API. В началото е малко объркващо. Ще обясня как го използвах и как модифицирах пробите, за да отговарят на моите нужди.

За Road Trip Google Maps се използва по два начина: (1) създаване на карта и (2) попълване с указания, въведени от потребителя.

API, който конкретно използвам, е Google Maps JavaScript API. Има друг потенциално релевантен API, който е API за уеб услуги. Разликата между двете е, че API на JavaScript е предназначен да се използва в браузъра (от страна на клиента), докато API на уеб услугата може да се използва в различни други ситуации, като например в задната част и с други видове езици (може да е JavaScript с Node или дори Python и т.н.). Наистина е важно да не ги смесвате, защото върнатите резултати може да съдържат различни данни и също така да бъдат организирани по различен начин.

Документацията е разделена на три важни за нас части:

  • Ръководства, които обхващат обичайни случаи на употреба, какви части от API се използват, примери за кодове и какъв тип данни се предоставят.
  • Справка, която обхваща всички инструменти, достъпни за вас, когато използвате API (т.е. какви обекти и методи са предоставени).
  • Примери, което е обобщение на примерите за код.

Често се оказвах, че превключвам между Ръководства, които предоставяха информация за данните и структурата на данните, които получавам от Google, и Примери, които предоставяха лесен за използване шаблон, който след това можех да променям.

Създадох картата си, използвайки този пример. След това добавих указания, използвайки „този“ пример. И накрая, можете да научите как да добавяте маркери, като погледнете това.

Важно е да се отбележи, че вторият пример използва падащо меню за места, докато аз използвах текстово поле, което се попълва автоматично с помощта на Google Places (вижте „тук“). Не бях сигурен дали данните, които Google Directions очаква, трябва да бъдат специален обект или форматиран низ, затова проверих кода и открих, че стойностите за всеки елемент от падащото меню са само техните имена на градове и щати, т.е. Чикаго, Илинойс. Не бях сигурен какъв тип данни получих от API на Google Places, но когато отпечатах стойността в текстовото си поле с активирано автоматично довършване, това също беше просто обикновен низ с адреса. Това означава, че и двете са работили едно с друго извън кутията.

Намиране на бизнеси наблизо

Това беше най-трудната част от проекта досега и отне няколко повторения, за да стане правилно.

Знаех, че крайната ми цел за тази част е или да получа поредица от адреси или координати, които след това да използвам за търсене на бизнес в Yelp, използвайки неговия API за търсене.

Позволете ми да дефинирам какво означава да имаш близки фирми. Има редица начини за тълкуване на това, всеки със своите съответни компромиси. Най-буквалното определение е да намерите топ места до магистралата/маршрута, по който пътувате. По-широко определение е да разгледате всички области, през които преминавате, и да получите най-добрите места и за двете. Първото решение осигурява най-голямо удобство по отношение на близостта. Второто решение предоставя повече стойност по отношение на намирането на страхотни места (които не са непременно до магистралата). Като имаме предвид тези опции, трябва да разгледаме данните, с които трябва да работим.

Когато попитате Google за упътвания между две точки, той ще ви даде „обект с резултат от упътванията“, който включва различни данни.

Използване на всяка стъпка от инструкцията

Когато получите посоката от А до Б (без точки по средата), вашите резултати ще се състоят от поредица от DirectionsSteps. Стъпките представляват всяка инструкция от пътуването (т.е. завийте надясно тук, слейте наляво там). За щастие всяка стъпка има start_locationи end_location набор от координати.

Признавайки, че start_location също е еквивалентен на end_location от последната стъпка, мога да третирам всяко start_location като отделна заявка в Yelp и да получавам фирми там наоколо.

Проблемът? Пътуванията често се простират на десетки до стотици мили по магистрала, което се превръща в една дълга DirectionsStep. Това означава, че ако погледнем началото или края на стъпка, пропускаме редица потенциално интересни местоположения за запитване.

Използване на всяка пътека в хода на стъпка

Google трябва да има някои данни за действителния ход на дадена стъпка, за да може например да я начертае на карта. За щастие, всеки обект DirectionsStep също има масив paths, съдържащ набор от координати по протежение на тази стъпка.

Проблемът? Има много пътища. При едно пътуване получих дължината на масива от pathsза всички стъпки, за да включва до 10 000 набора координати. Ако трябваше да пингвам Yelp 10 000 пъти за еднопътуване (вероятно получавам излишна информация, тъй като много от тези координати са близо една до друга), вероятно ще бъда забранен от API.

Също така не можах да изчисля разстоянието между пътеките и да прескоча заявки въз основа на известно разстояние (т.е. заявка Yelp на всеки брой мили). Това е така, защото все още е възможно да пропусна дестинация и не мога да предположа, че популярните дестинации са равномерно разпределени географски (т.е. винаги има страхотен град за проверка на всеки 10 мили).

Геокодиране на пътеки за получаване на уникални градове

След това си помислих да конвертирам всеки път в адрес, да получа града от този адрес и да пингвам Yelp за всички уникални градове, през които минавам.

Преобразуването на координатите на географска ширина и дължина е известно като геокодиране. Google предоставя „API за геокодиране“ точно за тази цел. Google обаче не е сам, тъй като много други компании с подобни на карти продукти предоставят тази услуга.

Проблемът? Изглежда, че геокодирането е скъп изчислителен процес, тъй като услугите са силно ограничени в скоростта. Някои услуги дори таксуват за геокод. Като цяло компаниите ви насърчават да „ограничите броя на геокодирането и да кеширате резултатите“. Ще трябва изкуствено да забавя програмата си, за да геокодирам всеки път всяка секунда. За 10 000 маршрута това може да отнеме до 3 часа!

Създавам собствен геокодер

След известно търсене успях да открия набора от данни Geonames със списък на градовете, съответните им държави и координатите на географската ширина и дължина на всеки град. Основният недостатък на този набор от данни е, че градовете са представени от точки, а не от граници, така че ще има някои проблеми с точността. Тъй като не можах да намеря по-добра алтернатива достатъчно бързо, реших да продължа.

Мога да добавя тази информация към база данни и след това за всяка координата на пътя да попитам кой е най-близкият град. Това е известно като „търсене на най-близкия съсед“. Един лесен начин за излизане от това би бил да добавя тези данни към хранилище на данни като MongoDB и след това да използвам параметъра за почти заявка, за да получа моя резултат. Тъй като това беше учебно упражнение за мен, исках да разбера каква структура от данни ми трябва и да я внедря.

Предвид ограничения брой елементи в моя набор от данни, открих, че „k-d дървовидна структура от данни“ най-добре отговаря на нуждите ми. Едно k-d дърво може да се разглежда като многомерно двоично дърво. Когато търсим k-d дърво, първо ще търсим по едно измерение (т.е. географска ширина). Актуализираме стойността на най-добрия резултат с точката, която ни дава най-късото разстояние между тази точка и точката, с която търсим. След това се връщаме към други части на дървото, за да видим дали тези точки могат да ни дадат по-късо разстояние.

В Road Trip изграждам k-d дърво, използвайки градовете в набора от данни Geonames. Функцията за разстояние, която използвах, отчита, че това са координати, а не цели точки. Преобразувам тези координати в разстояние, за да установя праг. С k-d дървото, заредено на моя сървър в паметта, става наистина лесно да се намерят градове близо до определена точка от пътя и дори по-лесно да се превърне това в набор от уникални градове, които мога да използвам, за да пингвам Yelp за бизнес данни.

Пакетно заявяване на Yelp за фирми

За да обобщим досега, Road Trip ще получи начален и краен адрес от потребителя и след това ще поиска от Google Maps обект DirectionsResult. Пътищата от всички стъпки на този обект се предават от клиента към сървъра чрез AJAX POST заявка. Сървърът получава уникалния набор от градове, най-близки до точките по пътя, използвайки k-d дърво, което се генерира при стартиране на сървъра. Използваме тези градове, за да накараме най-добрите бизнеси в тези области да се върнат обратно на клиента, така че да може да бъде изобразено за потребителя.

Всеки град представлява индивидуална заявка към Yelp. Това означава, че трябва да пингваме API за търсене на Yelp за всеки уникален град в нашия набор. Извикванията към Yelp API, подобно на много API извиквания в JavaScript, са асинхронни. Това означава, че програмата няма да чака резултатите от асинхронното повикване, преди да продължи да изпълнява. Наистина е трудно да свикнете с това, когато идвате от традиционните езици за програмиране, които обикновено са синхронни и чакат кодът да завърши изпълнението, преди да продължите. Това разграничение е важно, защото искаме да получим бизнес резултати за всяка заявка за град към Yelp, преди да я изобразим на потребителя. Не искаме да изпращаме данни обратно на клиента, докато всяко запитване не получи отговор.

Обратно обаждане по дяволите

Един от начините да принудите програмата да изчака всяко асинхронно извикване да завърши е чрез използването на вложени обратни извиквания.

Когато някой извика API, от вас се иска да предоставите функция, която се изпълнява, когато API е завършен. Тази функция обикновено включва аргументи за грешка и резултат. В случая на API за търсене на Yelp, вашето обратно извикване трябва да има аргументи за грешка (които ще бъдат дефинирани, ако е възникнала грешка, null в противен случай) и резултат (който ще включва данни за вашия бизнес).

Ако искаме да свържем няколко извиквания на API, можем да извикаме Yelp чрез поредица от вложени обратни извиквания. Всяко обратно извикване ще бъде изпълнено само при успешно изпълнено предходно. При всяко обратно извикване можем да изместим резултатите в масив. При последното ни обратно извикване връщаме данните.

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

Използване на обещания

Обещанията ни помагат да избягваме вложени обратни извиквания. Използваме синтаксиса .then() за свързване на асинхронни извиквания. Последващите повиквания се изпълняват само когато предходното повикване е било успешно или разрешено. Всъщност аргументите на функцията .then() са функции, които се активират в зависимост от това дали предходното извикване е било успешно или неуспешно. Q е обща библиотека на Promise за Node.

Динамично попълване на обещания

В Road Trip използвам библиотеката node-yelp като обвивка за Yelp API. Дори и да използвам Promises, за да поискам информация от Yelp, срещам два проблема:

  1. Функцията за търсене на Yelp все още се основава на обратно извикване. Това означава, че няма да върне Promise и няма да можете да го свържете с други функции на Promise.
  2. Както общият брой заявки на Yelp, така и филтрите за търсене за всяка заявка се променят в зависимост от града, който търсим. Всеки опит за ръчно писане на обратните извиквания/обещанията би бил в най-добрия случай болезнен.

Документацията на Q предоставя информация как да превърнете функция за обратно извикване в Promise. Вие по същество идентифицирате три части в обратното извикване към вашата възлова функция: (1) кога обратното извикване е завършило успешно (и какви данни се връщат), (2) когато е възникнала грешка и, по избор, (3) известие за прогрес на обратното извикване (стъпката за уведомяване).

След като вече знаем как да направим едно обещание, трябва да знаем как да направим динамичен брой обещания, специфични за всеки град, за който питаме. Q има функция .all(), която очаква да се изпълни масив от Promise функции. Преди да направим това, можем да попълним масив от заявки или Promise функции за всеки уникален град, който намерихме чрез търсенето на най-близкия съсед. Функцията .all() ще изпълни всяко Promise асинхронно (което всъщност е асинхронно извикване на Yelp) и след като всички Promise бъдат разрешени, тя обединява данните в масив, който след това можем да изведем към потребителя.

Promises са невероятно мощен инструмент, за да направите вашия асинхронен код многократно използваем и лесен за четене. В Road Trip успях да напиша един набор от кодове, за да получа данните за всички мои бизнеси и категории. Окончателната версия на този код може да бъде намерена тук (между редове 60 и 114).

Преди да разреша моите обещания, дезинфекцирам данните, за да получа само полетата, които искам, използвайки функцията .map(). Това е удобен начин да се гарантира, че предавате обратно само информацията, от която се нуждаете.

Обратно геокодиране на адреси за маркери

Когато сървърът приключи обработката на нашия път, той изпраща JSON обект с бизнес резултати обратно към нашия проект. Конкретната структура от данни е масив от масиви. Всеки отделен масив е списък с резултати за всеки град.

За да добавите маркер към обект на Google Maps, трябва да дефинирате координатите му за географска ширина и дължина. API за търсене на Yelp вече не предоставя координатите на географската ширина и дължина на отделните фирми. По този начин ще трябва да вземем адресните данни от Yelp и след това да ги преобразуваме в географска ширина и дължина. Това е известно като обратно геокодиране.

Трудно ми беше да заобиколя ограничаването на скоростта на Google за API за геокодиране. Преместих кода от сървъра към клиента, но все пак достигнах лимита на заявките, въпреки че общите заявки на ден бяха по-малко от 2500, а броят заявки в секунда беше по-малък от 5. За щастие имаше много алтернативи, така че написа AJAX GET заявка към инструмента Nominatim на Open Street Map за обръщане на геокодиране на моите Yelp адреси. Използвам функцията window.setTimeout(), за да създам забавяне от 1 секунда между всяко преобразуване. След като обратното геокодиране завърши, към картата се добавя маркер.

Ускоряване за потребителите

Най-голямото затруднение на Road Trip е обратното геокодиране на всеки резултат от Yelp, преди да го добавите като маркер на картата. За едно пътуване може да има между 4 и 30+ резултата. Това означава, че може да отнеме до 30 секунди, докато картата бъде напълно попълнена. Не може да се очаква потребителите да чакат толкова дълго, без нищо да се случва на екрана. Взех две мерки, за да улесня потребителското изживяване, докато се добавят данни.

Прозрачност при зареждане

При подаване на данни Road Trip дава сигнал на потребителя и го информира за напредъка на обработката. Показва се известие на лентата с анимиран кръг за зареждане. Вдигам тази лента една крачка напред и добавям лента за напредъка във фонов режим, която се актуализира, когато добавяме още маркери към картата. Това сигнализира на потребителя колко остава за обработка и добавяне.

Поточна информация

Въпреки че потребителите може да имат повече информация за напредъка на обработката, те все пак може да са разочаровани, че отнема толкова време, за да получат данните. За щастие, като направя обратното геокодиране от страна на клиента, мога да предам функция за добавяне на маркер към картата, когато координатите са извлечени. Тъй като това се случва асинхронно, не е нужно да чакам геокодирането на всички мои резултати, преди да ги добавя към картата. В този случай асинхронните обратни извиквания са полезни, защото мога прогресивно да зареждам картата, докато получавам повече информация.

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

Какво следва

Текущата версия на Road Trip е в състояние, в което може ефективно да демонстрира идеята и стойността на откриването на бизнеси, когато планирате маршрута си. След като го показах на приятели и семейство, измислих редица потенциални подобрения:

  • Потребителите трябва да могат да създават персонализиран маршрут, като добавят фирми към подбран списък с любими.
  • Черпете от повече източници на данни, за да научите повече за конкретна дестинация. Бих могъл да добавя описание на града от Wikipedia и още повече сайтове за посещение от места като WikTravel или TripAdvisor.
  • Направете пътуването интелигентно, като попитате потребителя какво превозно средство ще кара и предвидете най-добрите места за спиране, почивка, бензин и изследване.

Когато създавам проекти, дори малки като Road Trip, обичам да мисля за голямата идея зад софтуера. Когато проектирате продукт с мисъл за голямата картина, много потребители също ще го забележат и това ще се отрази на първото им впечатление и на начина, по който възприемат стойностното предложение на приложението. Тъй като потребителите осъзнаха всички неща, които вече могат да правят с Road Trip, те започнаха да измислят много начини да го направят още по-добър. В много отношения това е една от най-добрите части от изграждането на страничен проект.

Благодарим ви много, че отделихте време да прочетете тази публикация. Надявам се, че успяхте да извлечете някои прозрения, които можете да използвате за следващия си страничен проект. Ако имате някакви коментари, въпроси или точки за обратна връзка, моля, не се колебайте да ми изпратите имейл на [email protected].