Мне в работе с одним из сайтов недвижимости нужно было сделать функционал который для заданной точке на карте города ищет ближайшие станции метро и рассчитывает расстояние до них. Все это должно отрабатывать на PHP скрипте которому передается через аякс адрес, который он обрабатывает и возвращает только 3 станции метро и расстояние до них (как к примеру в Яндекс картах), также рассчитывает время пешком до метро.
Список станций метрополитена
Начнем с того что нужно сделать список станций метро и проставить им координаты широты и долготы. Список станций Московского метрополитена можно получить несколькими способами. Первый способ самый простой - ручной. Находим список станций в интернете, например на официальном сайте Московского метро или в статье на википедии, и вручную перенести их в таблицу БД. Этот вариант немного однообразный и не очень интересный, поэтому есть и второй вариант.
Второй вариант использовать API сайтов которые отдают список станций метро с координатами, распарсить их и автоматически добавить в базу. Этот вариант был немного быстрее и интересней. Нужные нам данные по API из тех сайтов что я просмотрел, отдает только HeadHunter, остальные Superjob, Cian, Avito - отдают только список станций метро без координат.
Чтобы получить список всех станций для метрополитена, необходимо отправить GET запрос на указанный адрес https://api.hh.ru/metro/1, где 1 - это ID города в базе HeadHunter. Если вам нужен будет другой город смотрите в документации HH.
API возвращает список станций с группировкой по веткам метро в формате JSON, также он отдает и цвет линии, что очень удобно.
Хранить данные мы будем в MySQL в двух таблицах, станции и линии метро. Примерная структура таблиц такая:
CREATE TABLE `metro_lines` ( `id` int(11) NOT NULL auto_increment, `name` varchar(250) NOT NULL, `color` varchar(6) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1; CREATE TABLE `metro` ( `id` int(11) NOT NULL auto_increment, `name` varchar(250) NOT NULL, `latitude` decimal(9,6) NOT NULL, `longitude` decimal(9,6) NOT NULL, `line_id` int(11) NOT NULL, `order` tinyint(4) NOT NULL default '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
Скрипт для импорта станций метро выглядит примерно так:
/** * Импортируем станции метро в нашу базу */ public function importFromApi(){ //Ссылка на json выгрузку станций метро для города Москвы (ID:1) $url = "https://api.hh.ru/metro/1"; // Метод getUrl делает запрос с помощью cURL $file = $this->getUrl($url); $file = json_decode($file, true); // Очищаем таблицы с линиями и станциями метро $this->pdo->prepare("TRUNCATE TABLE `metro`")->execute(); $this->pdo->prepare("TRUNCATE TABLE `metro_lines`")->execute(); foreach($file['lines'] as $lines){ // Сохраняем линию метро $query = $this->pdo->prepare("INSERT INTO `metro_lines` SET `color` = :color, `name` = :name"); $query->execute( array(':color'=>$lines['hex_color'], ':name'=>$lines['name'])); $line_id = $this->pdo->lastInsertId(); if($line_id){ foreach($lines['stations'] as $station){ // Сохраняем станции и привязываем их к линиям метро $query = $this->pdo->prepare("INSERT INTO `metro` SET `latitude` = :latitude, `longitude` = :longitude, `line_id` = :line_id, `name` = :name, `order` = :order"); $query->execute( array(':longitude'=>$station['lng'], ':latitude'=>$station['lat'], ':line_id'=>$line_id, ':name'=>$station['name'], ':order'=>$station['order'])); } } } }
Находим координаты нужного объекта
Для того чтобы определить ближайшие станции метро нам нужно определить сначала координаты для нашего объекта, зная его адрес. Для это отправим запрос к геокодеру Яндекса.
/** * Находим координаты объекта через API Yandex * * @param string $street Адрес объекта */ public function getLngLatStreet($street){ $url = 'https://geocode-maps.yandex.ru/1.x/?format=json&geocode='.urlencode('Москва, '.$street); $yandex_street_response = $this->getUrl($url); $geocodeInfo = json_decode($yandex_street_response, true); $lngLat = $geocodeInfo['response']['GeoObjectCollection']['featureMember'][0]['GeoObject']['Point']['pos']; // Если улица не найдено то precision == other и скорей всего координаты будут центра указанного города, в нашем случае это Москва. if($lngLat && $geocodeInfo['response']['GeoObjectCollection']['featureMember'][0]['GeoObject']['metaDataProperty']['GeocoderMetaData']['precision'] != 'other'){ list($longitude, $latitude) = explode(" ", $lngLat); return array('longitude'=>$longitude, 'latitude'=>$latitude); } return false; }
Находим ближайшие станции метро
Дальнейшим нашим действием будет рассчитать какие станции метро находятся в круге с нужным нам радиусом точнее, для простоты расчетов, квадрате см. рис 1.
Рис. 1
Точка О - здесь находится нужный нам объект, зеленым обозначена область в которой мы будем искать станции метро. Для этого нам нужно найти координаты точек A, B, C, D зная при этом радиус AO и координаты точки О. Код нахождения координат нужных нам точек будет такой:
/** * Нахождение координат точки по координатам другой точки и известным длине и дирекционному углу данного направления, соединяющей эти точки * * @param float $longitude1 Долгота первой точки * @param float $latitude1 Широта первой точки * @param int $distance Максимальное расстояние до метро в км * @param int $angle Дирекционный угол * @param string $type Если нужно только одна координата широта или высота, то указываем ее */ public function getPoint($longitude, $latitude, $distance=2, $angle=0, $type=null){ // Переводим км в метры $distance = $distance * 1000; // Длина дуги параллели в 1° на экваторе в метрах $latMeter = 111321; $angle = deg2rad($angle); $dx = $distance / ($latMeter * cos(deg2rad($latitude))) * cos($angle); $dy = $distance / $latMeter * sin($angle); $result = array( 'longitude' => $longitude + $dx, 'latitude' => $latitude + $dy ); // Если нам нужна только одна координата (широта или высота), то возвращаем только ее if($type){ return $result[$type]; } return $result; }
Для точек A и B нам нужно найти долготу, для C и D - широту. После нахождения координат можно выбирать станции из базы:
/** * Находим N ближайших станций метро от заданного объекта * * @param float $longitude Долгота заданного объекта * @param float $latitude Широта заданного объекта * @param int $distance Максимальное расстояние до метро в км * @param int $limit Максимальное количество станций */ public function getMetro($longitude, $latitude, $distance=2, $limit=null){ $sLng = $this->getPoint($longitude, $latitude, $distance, 180, 'longitude'); $eLng = $this->getPoint($longitude, $latitude, $distance, 0, 'longitude'); $sLat = $this->getPoint($longitude, $latitude, $distance, 270, 'latitude'); $eLat = $this->getPoint($longitude, $latitude, $distance, 90, 'latitude'); /** Выбираем информацию о станции, а также цвет ветки метро */ $query = $this->pdo->prepare("SELECT m.id, m.name, m.latitude, m.longitude, l.color FROM metro as m INNER JOIN metro_lines as l ON l.id = m.line_id WHERE m.latitude BETWEEN :sLat AND :eLat AND m.longitude BETWEEN :sLng AND :eLng"); $query->execute(array(':sLat'=>$sLat, ':eLat'=>$eLat, ':sLng'=>$sLng, ':eLng'=>$eLng)); $data = $query->fetchAll(PDO::FETCH_ASSOC); if($data){ $list = array(); foreach($data as $v){ // Рассчитываем расстояние от объекта до метро в метрах $v['distance'] = $this->distance($longitude, $latitude,$v['longitude'],$v['latitude']); // Если расстояние более 1000 метров то переводим результат в км. Можно вынести в отдельный метод $v['distance_format'] = ($v['distance'] >= 1000 ? round($v['distance']/1000,1).' км' : $v['distance'].' м'); $list[$v['distance']] = $v; } // Сортируем по расстоянию метро ksort($list); if($limit){ // Если задан лимит то выводим только первые N станций return array_slice($list, 0, $limit); }else{ return $list; } } return false; }
Рассчитываем расстояние до метро
Метод distance для расчета расстояния между двумя точками на карте:
/** * Рассчитываем расстояние между двумя точками на карте * * @param float $longitude1 Долгота первой точки * @param float $latitude1 Широта первой точки * @param float $longitude2 Долгота второй точки * @param float $latitude2 Широта второй точки */ public function distance($longitude1, $latitude1, $longitude2, $latitude2){ // Средний радиус Земли в метрах $earthRadius = 6372797; $dLat = deg2rad($latitude2 - $latitude1); $dLon = deg2rad($longitude2 - $longitude1); $a = sin($dLat/2) * sin($dLat/2) + cos(deg2rad($latitude1)) * cos(deg2rad($latitude2)) * sin($dLon/2) * sin($dLon/2); $c = 2 * asin(sqrt($a)); $distance = ceil($earthRadius * $c); return $distance; }
И на последок функция для загрузки страницы с помощью cURL:
/** * Подгружаем страницу по URL * * @param string $url Ссылка на страницу */ public function getUrl($url){ $browser = array ( "user_agent" => "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 (.NET CLR 3.5.30729)", "language" => "en-us,en;q=0.5" ); $ch = curl_init(); if(isset($browser)){ curl_setopt($ch, CURLOPT_HTTPHEADER, array( "User-Agent: {$browser['user_agent']}", "Accept-Language: {$browser['language']}" ) ); } curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($ch, CURLOPT_AUTOREFERER, 1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0); curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0); $output = curl_exec($ch); $chinfo = curl_getinfo($ch); curl_close($ch); if ($chinfo['http_code'] && !in_array($chinfo['http_code'],array(404,403))) { return $output; } return false; }
Пример использования
Скрипт для нахождения ближайших станций метрополитена готов. Ниже пример как его можно использовать:
// Подключаем класс нашего скрипта include("./metro.class.php"); // Доступы к БД $setting = array( 'username' => 'root', 'password' => '', 'database' => 'test', 'host' => 'localhost', ); // Максимальное расстояние до метро в км. $distance = 3; $metroClass = new Metro($setting); $lngLat = $metroClass->getLngLatStreet('Красная площадь, 3'); if($lngLat){ $data = $metroClass->getMetro($lngLat['longitude'], $lngLat['latitude'], $distance, 3); var_dump($data); }
На этом все, класс и готовый пример можете скачать внизу по ссылке. Если будут вопросы или предложения пишите в комментариях.
А как изменить путь для выгрузки станций метро СПБ
url = "https://api.hh.ru/metro/2";
В каком файле прописан этот путь?
Или придется Менять построчно Ваш файл dump?
это нужно изменить в файле metro.class.php в строке 176
Уважаемый админ, а подскажите еще, в каком файле находится ссылка url = "https://api.hh.ru/metro/1";
Т.е я сейчас пытаюсь сделать то же самое, но для Спб, нашел, что его ID=2 и как теперь правильно прописать путь, для выгрузки станций метро?
Неужели построчно менять ваш файл dump
это нужно изменить в файле metro.class.php в строке 176
Да, увидел, поменял
Мои знания в этой области минимальны, поэтому прошу простить заранее за излишнюю тупость, но файл дамп же не изменился, а соответственно и результатом обработки пока являются станции Москвы..
Как заменить теперь данные таблиц в phpmyadmin?
include("./metro.class.php");
// Укажите доступы к БД
$setting = array(
'username' => 'root',
'password' => '',
'database' => 'test',
'host' => 'localhost',
);
$metroClass = new Metro($setting);
$metroClass->importFromApi();
Да, таблички обновились, но при любой улице теперь выдает "Нет ближайших станций метро".
Менял параметр максимального расстояния, но это не помогло.(
Доброго времени суток!
Скажите пожалуйста, как нужно импортировать данные в таблицу?
Т.е. тот файл, который предназначен для импорта данных, как его внедрить, как сделать на него ссылку?
Файл в архиве это sql дамп. Его можно импортировать в mysql через phpmyadmin или adminer
Все, нашел это в своем Денвере, в phpmyadmin, большое спасибо за помощь!
А почему просто не использовать геокодер Яндекса?
Он из коробки умеет искать ближайшие метро к точке, заданной координатами: https://tech.yandex.ru/maps/doc/geocoder/desc/concepts/input_params-docpage/
Просто нужно указать kind=metro
Это одно из применений и понятное для всех, на практике мне нужно было искать ближайшие объекты, которые забитые в БД с координатами, от местоположения пользователя