module_title = _t('geo', 'Geo'); # Register maps providers $this->app->hooks()->geoMapsProviderRegister(GoogleMapsProvider::class); $this->app->hooks()->geoMapsProviderRegister(YandexMapsProvider::class); # Register geocode providers $this->app->hooks()->geoGeocodeProviderRegister(GoogleGeocodeProvider::class); $this->app->hooks()->geoGeocodeProviderRegister(YandexGeocodeProvider::class); } public function onNewRequest($request) { parent::onNewRequest($request); $this->cache = []; $this->cacheParents = []; } /** * Maximum regions tree depth * @return mixed */ public function maxDeep() { return $this->model->regionMaxDeep(); } /** * Получаем данные о регионе по ID * @param int $id ID региона(города/области/страны) * @param mixed $updateCache, если не NULL => сохраняем в кеш * @param array $opts * @return mixed */ public function regionData($id, $updateCache = null, $opts = []) { return $this->regionDataCache((int)$id, $updateCache); } /** * Получаем данные о регионе по Keyword * @param string $keyword Keyword региона(города/области) * @param mixed $updateCache, если не NULL => сохраняем в кеш * @param array $opts * @return mixed */ public function regionDataByKeyword($keyword, $updateCache = null, $opts = []) { return $this->regionDataCache((string)$keyword, $updateCache); } /** * Получаем данные о регионе с кешированием результатов в памяти * @param string|int $id ID или Keyword региона(города/области) * @param mixed $updateCache, если не NULL => сохраняем в кеш * @param array $opts * @return mixed */ public function regionDataCache($id, $updateCache = null, $opts = []) { if (! is_null($updateCache)) { if (! empty($updateCache['id'])) { $this->cache[$updateCache['id']] = $updateCache; } if (! empty($updateCache['keyword'])) { $this->cache[$updateCache['keyword']] = $updateCache; } return $updateCache; } if (isset($this->cache[$id])) { return $this->cache[$id]; } if (empty($id)) { return []; } if (is_numeric($id)) { $filter = ['id' => $id]; } elseif (is_string($id)) { $filter = ['keyword' => $id]; } else { return []; } $data = $this->model->regionData($filter, $opts); if (! empty($data)) { $this->cache[$data['id']] = $data; $this->cache[$data['keyword']] = $data; } return $data; } /** * Данные о регионе с полем экстра * @param $regionID * @return array|mixed */ public function regionDataExtra($regionID) { if (empty($regionID)) { return []; } $data = $this->regionData($regionID); if (empty($data) || !isset($data['numlevel'])) { return []; } if (! isset($data['extra'])) { $data = $this->model->regionData($regionID, ['fields' => ['extra']]); $this->regionData($regionID, $data); } return $data; } /** * Получаем ID региона(города) по IP адресу * @param string|null $ipAddress IP адрес или null - текущий * @return mixed */ public function regionDataByIp(?string $ipAddress = null) { do { $provider = $this->ipLocationProvider( $this->config('geo.ip.location.provider', '', TYPE_STR) ); if (! $provider) { break; } $regionID = (int)$provider->ipLocationQuery($ipAddress ?? $this->request->remoteAddress()); if (! $regionID) { break; } return $this->regionData($regionID); } while (false); return []; } /** * Получаем название региона по ID * @param int $regionID ID региона(города/области) * @param string $default текст по-умолчанию, для случая если неудалось определить регион по ID * todo: language * @return string */ public function regionTitle($regionID, $default = '') { if (! $regionID) { return $default; } $data = $this->regionData($regionID); if (empty($data) || !isset($data['title'])) { return $default; } return $data['title']; } /** * Получаем, ID страны для регииона * @param int|array $regionID ID региона или данные о регионе (полученные методом regionData или regionDataByKeyword) * @return int */ public function regionCountry($regionID) { if (empty($regionID)) { return 0; } if (is_numeric($regionID)) { $data = $this->regionData($regionID); } else { $data = $regionID; } if (empty($data['numlevel'])) { return 0; } if ($data['numlevel'] == 1) { return $data['id'] ?? 0; } return $data['parents'][1]['id'] ?? 0; } /** * Получаем, является ли регион городом * @param int|array $regionID ID региона или данные о регионе (полученные методом regionData или regionDataByKeyword) * @return bool */ public function isCity($regionID) { if (is_numeric($regionID)) { $data = $this->regionData($regionID); } else { $data = $regionID; } return ! empty($data['city']); } /** * Разворачиваем данные о регионе для записи в базу и для формирования url * @param int|array $regionID ID регионна ил данные о регионе * @return array */ public function regionParents($regionID) { $result = [ # Пустые значения не добавляем в результат # 'geo_region2' => 0, 'geo_region3' => 0, 'geo_region4' => 0 'db' => ['geo_region1' => 0], # 'region2' => '', 'region3' => '', 'region4' => '' 'keys' => ['region1' => '', 'city' => ''], ]; do { if (empty($regionID)) { break; } if (is_array($regionID)) { $data = $regionID; } else { $data = $this->regionData($regionID); } if (empty($data)) { break; } if ($data['numlevel'] == 1) { $result['db']['geo_region1'] = $data['id']; $result['keys']['region1'] = $data['keyword']; } if (empty($data['parents'])) { break; } foreach ($data['parents'] as $k => $v) { $result['db']['geo_region' . $k] = $v['id']; $result['keys']['region' . $k] = $v['keyword']; } $lvl = $data['numlevel']; $result['db']['geo_region' . $lvl] = $data['id']; $result['keys']['region' . $lvl] = $data['keyword']; if (! empty($data['city'])) { $result['keys']['city'] = $data['keyword']; } } while (false); return $result; } /** * Разворачиваем данные о регионе c кешированием в памяти до 1000 регионов (для крон задач) * @param $regionID * @param array $opts * @return array */ public function regionParentsCache($regionID, $opts = []) { $regionID = (int)$regionID; $cache = &$this->cacheParents; if (isset($cache[$regionID])) { return $cache[$regionID]; } if (count($cache) > 1000) { $k = array_key_first($cache); unset($cache[$k]); } $data = $this->model->regionData(['id' => $regionID], $opts); return $cache[$regionID] = $this->regionParents($data); } /** * Использовать метро городов * @return bool */ public function metroEnabled(): bool { return $this->config('geo.metro', true, TYPE_BOOL); } /** * Получаем, есть ли в регионе метро * @param int $regionID ID города * @return bool */ public function hasMetro($regionID) { if (! $this->metroEnabled()) { return false; } $data = $this->regionDataExtra($regionID); if (empty($data['city'])) { return false; } return ! empty($data['extra']['metro']); } /** * Получаем список веток и станций метро по ID города * @param int $cityID ID города * @param array $opts array * @return array */ public function cityMetro($cityID = 0, $opts = []) { $opts = array_merge([ 'selected' => 0, # ID выбранной станции 'html' => false, # формировать html 'template' => false, # название шаблона (без расширения ".php") 'module' => false, # название модуля / плагина к которому относится шаблон ], $opts); $result = [ 'data' => [], # данные о ветках + станциях метро города(tree) 'html' => '', # html формат 'city_id' => $cityID, 'sel' => [ 'id' => $opts['selected'], # ID выбранной станции метро 'branch' => [], # данные о выбранной ветке метро 'station' => [] # данные о выбранной станции метро ], ]; do { if (empty($cityID)) { break; } # это город + помечено наличие метро if (! $this->hasMetro($cityID)) { break; } # получаем список веток+станций метро города(tree) $metro = $this->model->metroList($cityID); if (empty($metro)) { break; } $result['data'] = $metro; # помечаем данные о выбранной ветке / станции if ($opts['selected']) { foreach ($metro as $v) { foreach ($v['st'] as $vv) { if ($vv['id'] == $opts['selected']) { $result['sel']['station'] = $vv; unset($v['st']); $result['sel']['branch'] = $v; break 2; } } } # не удалось получить данные о выбранной станции метро # считаем что id станции указан некорректно if (empty($result['sel']['station'])) { $result['sel']['id'] = 0; } } # формируем HTML if ($opts['html']) { $html = $opts['html']; if ($html === true) { # для admin-панели результат формируем в select::options if ($this->isAdminPanel()) { $html = 'select'; } } if ($html === 'select') { $html = ''; if (! $result['sel']['id']) { $html .= ''; } foreach ($metro as $v) { $html .= ''; $html .= HTML::selectOptions($v['st'], $opts['selected'], false, 'id', 'title'); $html .= ''; } $result['html'] = $html; } else { # для frontend используем шаблон $result['html'] = $this->view->template($opts['template'], $result, $opts['module']); } } } while (false); return $result; } /** * Использовать районы города * @return bool */ public function districtsEnabled(): bool { return $this->config('geo.districts', false, TYPE_BOOL); } public function hasDistricts($regionID) { if (! $this->districtsEnabled()) { return false; } $data = $this->regionDataExtra($regionID); if (empty($data['city'])) { return false; } return ! empty($data['extra']['districts']); } /** * ID страны по-умолчанию * @return mixed */ public function defaultCountry() { return $this->config('geo.default.country', 690791/* Ukraine todo */, TYPE_UINT); } /** * Описание данных в поле declension * @param bool $keyOnly * @return array */ public function declensionKeys($keyOnly = true) { $data = [ 'where' => [ 't' => _t('geo', 'Declination, Where'), 'placeholder' => _t('geo', 'in the region, for example: London, England'), ], 'to' => [ 't' => _t('geo', 'where'), 'placeholder' => _t('geo', 'to the region, for example: to London, to England'), ], 'from' => [ 't' => _t('geo', 'from where'), 'placeholder' => _t('geo', 'from a region, for example: from London, from England'), ], ]; return $keyOnly ? array_keys($data) : $data; } /** * Подготовка поля declension * @param array $data данные * @param string|null $lang ключ языка, null - для всех языков * @param string|array|null $default значение по умолчанию * @return array */ public function prepareDeclension($data, $lang = null, $default = '') { $fields = $this->declensionKeys(); $langs = $this->locale->getLanguages(); $result = []; if (is_string($data)) { $data = json_decode($data, true); } if (! is_array($default)) { if (is_string($default)) { $default = [ 'where' => _t('geo', 'in [region]', ['region' => $default], $lang), 'to' => _t('geo', 'to [region]', ['region' => $default], $lang), 'from' => _t('geo', 'from [region]', ['region' => $default], $lang), ]; } else { $default = array_combine($fields, array_fill(0, sizeof($fields), $default)); } } if (empty($data) || ! is_array($data)) { $data = []; foreach ($fields as $v) { $data[$v] = []; foreach ($langs as $vv) { $data[$v][$vv] = $default[$v] ?? ''; } } } foreach ($fields as $v) { $result[$v] = $data[$v] ?? []; foreach ($langs as $vv) { $result[$v][$vv] = $data[$v][$vv] ?? $default[$v] ?? ''; } } if (! is_null($lang)) { foreach ($result as $k => &$v) { $v = $v[$lang] ?? $default[$k] ?? ''; } unset($v); } return $result; } /** * List of registered maps providers * @param string|null $key to get particular provider * @return MapsProvider[] | MapsProvider | null */ public function mapsProvidersList(?string $key = null) { return Providers::get('geo.maps.providers', MapsProvider::class, $key); } /** * Get maps provider instance * @param string|null $key of maps provider or null (from settings) * @return MapsProvider|null */ public function mapsProvider(?string $key = null): ?MapsProvider { $key = $key ?? $this->mapsType(); if (! $key) { return null; } return $this->mapsProvidersList($key); } /** * Get maps provider config key * @param string $providerKey * @param string $configKey * @return string */ public function mapsProviderConfigKey(string $providerKey, string $configKey): string { return 'geo.maps.providers.' . $providerKey . '.' . $configKey; } /** * Тип карт * @return mixed */ public function mapsType() { return $this->config('geo.maps.provider', '', TYPE_STR); } /** * Include Maps Provider API (JavaScript) * @param array $opts map options (specific provider options) * @param string|null $provider maps provider key or null (from settings) * @return bool|MapsProvider */ public function mapsAPI(array $opts = [], ?string $provider = null) { $provider = $this->mapsProvider($provider); if (! $provider) { return false; } $this->view->script( $provider->scripts($opts) ); return $provider; } /** * Map default coords * @param bool $asArray * @return string|array */ public function mapDefaultCoords($asArray = false) { $coords = $this->config('geo.maps.default.coords'); if (empty($coords) || strpos($coords, ',') === false) { $coords = '50.4496,30.5254'; # Kiev } return ($asArray ? array_map('trim', explode(',', $coords)) : $coords); } /** * Check coords and use default in case no coords set * todo refs + facade * @param mixed $lat @ref latitude * @param mixed $lon @ref longitude */ public function mapDefaultCoordsCorrect(&$lat, &$lon) { if (!floatval($lat) || !floatval($lon)) { list($lat, $lon) = $this->mapDefaultCoords(true); } } /** * List of registered geocode providers * @param string|null $key to get particular provider * @return GeocodeProvider[] | GeocodeProvider | null */ public function geocodeProvidersList(?string $key = null) { return Providers::get('geo.geocode.providers', GeocodeProvider::class, $key); } /** * Get geocode provider instance * @param string $key of geocode provider * @return GeocodeProvider|null */ public function geocodeProvider(string $key): ?GeocodeProvider { if (! $key) { return null; } return $this->geocodeProvidersList($key); } /** * Geocode query * @param string $providerKey * @param string $address * @param int $limit * @param array $opts * @return array|bool */ public function geocodeQuery($providerKey, $address, $limit = 1, array $opts = []) { $provider = $this->geocodeProvider($providerKey); if (! $provider) { return false; } $provider->setLimit($limit); return $provider->geocodeQuery($address, $opts); } /** * Reverse geocode query * @param string $providerKey * @param string|float $latitude * @param string|float $longitude * @param array|\Closure $format * @param int $limit * @param array $opts * @return array|string|bool */ public function geocodeReverseQuery($providerKey, $latitude, $longitude, $format = GeocodeProvider::FORMAT_DEFAULT, $limit = 1, array $opts = []) { $provider = $this->geocodeProvider($providerKey); if (! $provider) { return false; } $provider->setLimit($limit); return $provider->reverseQuery($latitude, $longitude, $format, $opts); } /** * List of registered ip location providers * @param string|null $key to get particular provider * @return IpLocationProvider[] | IpLocationProvider | null */ public function ipLocationProvidersList(?string $key = null) { return Providers::get('geo.ip.location.providers', IpLocationProvider::class, $key); } /** * Get ip location provider instance * @param string $key of ip location provider * @return IpLocationProvider|null */ public function ipLocationProvider(string $key): ?IpLocationProvider { if (! $key) { return null; } return $this->ipLocationProvidersList($key); } /** * Сбрасываем кеш * @param mixed $level * @param string $extra * @return void */ public function resetCache($level = false, $extra = '') { Cache::group('geo')->flush(); } /** * Обработка копирования данных локализации * @param $from * @param $to */ public function onLocaleDataCopy($from, $to) { $this->resetCache(); } }