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 .= '';
}
$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();
}
}