Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 221 additions & 20 deletions Here.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@
*/
final class Here extends AbstractHttpProvider implements Provider
{
/**
* @var string
*/
public const GS7_GEOCODE_ENDPOINT_URL = 'https://geocode.search.hereapi.com/v1/geocode';

/**
* @var string
*/
public const GS7_REVERSE_ENDPOINT_URL = 'https://revgeocode.search.hereapi.com/v1/revgeocode';

/**
* @var string
*/
Expand Down Expand Up @@ -96,55 +106,67 @@ final class Here extends AbstractHttpProvider implements Provider
];

/**
* @var string
* @var string|null
*/
private $appId;
private ?string $appId;

/**
* @var string
* @var string|null
*/
private $appCode;
private ?string $appCode;

/**
* @var bool
*/
private $useCIT;
private bool $useCIT;

/**
* @var string|null
*/
private ?string $apiKey = null;

/**
* @var string
*/
private $apiKey;
private string $version;

/**
* @param ClientInterface $client an HTTP adapter
* @param string $appId an App ID
* @param string $appCode an App code
* @param string|null $appId an App ID
* @param string|null $appCode an App code
* @param bool $useCIT use Customer Integration Testing environment (CIT) instead of production
* @param string $version version of the API to use ('6.2' or '7')
*/
public function __construct(ClientInterface $client, ?string $appId = null, ?string $appCode = null, bool $useCIT = false)
public function __construct(ClientInterface $client, ?string $appId = null, ?string $appCode = null, bool $useCIT = false, string $version = '6.2')
{
$this->appId = $appId;
$this->appCode = $appCode;
$this->useCIT = $useCIT;
$this->version = $version;

parent::__construct($client);
}

public static function createUsingApiKey(ClientInterface $client, string $apiKey, bool $useCIT = false): self
public static function createUsingApiKey(ClientInterface $client, string $apiKey, bool $useCIT = false, string $version = '7'): self
{
$client = new self($client, null, null, $useCIT);
$client = new self($client, null, null, $useCIT, $version);
$client->apiKey = $apiKey;

return $client;
}

public function geocodeQuery(GeocodeQuery $query): Collection
{

// This API doesn't handle IPs
if (filter_var($query->getText(), FILTER_VALIDATE_IP)) {
throw new UnsupportedOperation('The Here provider does not support IP addresses, only street addresses.');
}

if ('7' === $this->version && null !== $this->apiKey) {
return $this->geocodeQueryGS7($query);
}

$queryParams = $this->withApiCredentials([
'searchtext' => $query->getText(),
'gen' => 9,
Expand Down Expand Up @@ -174,8 +196,56 @@ public function geocodeQuery(GeocodeQuery $query): Collection
return $this->executeQuery(sprintf('%s?%s', $this->getBaseUrl($query), http_build_query($queryParams)), $query->getLimit());
}

private function geocodeQueryGS7(GeocodeQuery $query): Collection
{
$queryParams = $this->withApiCredentials([
'q' => $query->getText(),
'limit' => $query->getLimit(),
]);

// Pass-through for GS7 geo filters / sorting reference point.
// See https://www.here.com/docs/bundle/geocoding-and-search-api-v7-api-reference/page/index.html#/paths/~1geocode/get
if (null !== $at = $query->getData('at')) {
$queryParams['at'] = $at;
}
if (null !== $in = $query->getData('in')) {
$queryParams['in'] = $in;
}
if (null !== $types = $query->getData('types')) {
$queryParams['types'] = $types;
}

$qq = [];
if (null !== $country = $query->getData('country')) {
$qq[] = 'country=' . $country;
}
if (null !== $state = $query->getData('state')) {
$qq[] = 'state=' . $state;
}
if (null !== $county = $query->getData('county')) {
$qq[] = 'county=' . $county;
}
if (null !== $city = $query->getData('city')) {
$qq[] = 'city=' . $city;
}

if (!empty($qq)) {
$queryParams['qq'] = implode(';', $qq);
}

if (null !== $query->getLocale()) {
$queryParams['lang'] = $query->getLocale();
}

return $this->executeQuery(sprintf('%s?%s', $this->getBaseUrl($query), http_build_query($queryParams)), $query->getLimit());
}

public function reverseQuery(ReverseQuery $query): Collection
{
if ('7' === $this->version && null !== $this->apiKey) {
return $this->reverseQueryGS7($query);
}

$coordinates = $query->getCoordinates();

$queryParams = $this->withApiCredentials([
Expand All @@ -188,12 +258,34 @@ public function reverseQuery(ReverseQuery $query): Collection
return $this->executeQuery(sprintf('%s?%s', $this->getBaseUrl($query), http_build_query($queryParams)), $query->getLimit());
}

private function reverseQueryGS7(ReverseQuery $query): Collection
{
$coordinates = $query->getCoordinates();

$queryParams = $this->withApiCredentials([
'at' => sprintf('%s,%s', $coordinates->getLatitude(), $coordinates->getLongitude()),
'limit' => $query->getLimit(),
]);

if (null !== $query->getLocale()) {
$queryParams['lang'] = $query->getLocale();
}

return $this->executeQuery(sprintf('%s?%s', $this->getBaseUrl($query), http_build_query($queryParams)), $query->getLimit());
}

private function executeQuery(string $url, int $limit): Collection
{
$content = $this->getUrlContents($url);

$json = json_decode($content, true);

if (isset($json['error'])) {
if ('Unauthorized' === $json['error']) {
throw new InvalidCredentials('Invalid or missing api key.');
}
}

if (isset($json['type'])) {
switch ($json['type']['subtype']) {
case 'InvalidInputData':
Expand All @@ -205,6 +297,10 @@ private function executeQuery(string $url, int $limit): Collection
}
}

if (isset($json['items'])) {
return $this->parseGS7Response($json['items'], $limit);
}

if (!isset($json['Response']) || empty($json['Response'])) {
return new AddressCollection([]);
}
Expand All @@ -215,6 +311,112 @@ private function executeQuery(string $url, int $limit): Collection

$locations = $json['Response']['View'][0]['Result'];

return $this->parseV6Response($locations, $limit);
}

private function parseGS7Response(array $items, int $limit): Collection
{

$results = [];

foreach ($items as $item) {
$builder = new AddressBuilder($this->getName());

$position = $item['position'];
$builder->setCoordinates($position['lat'], $position['lng']);

if (isset($item['mapView'])) {
$mapView = $item['mapView'];
$builder->setBounds($mapView['south'], $mapView['west'], $mapView['north'], $mapView['east']);
}

$address = $item['address'];
$builder->setStreetNumber($address['houseNumber'] ?? null);
$builder->setStreetName($address['street'] ?? null);
$builder->setPostalCode($address['postalCode'] ?? null);
$builder->setLocality($address['city'] ?? null);
// GS7 may provide both `district` and `subdistrict`. Prefer `district` for backward compatibility,
// but fall back to `subdistrict` when `district` is missing.
$builder->setSubLocality($address['district'] ?? ($address['subdistrict'] ?? null));
$builder->setCountryCode($address['countryCode'] ?? null);
$builder->setCountry($address['countryName'] ?? null);

/** @var HereAddress $hereAddress */
$hereAddress = $builder->build(HereAddress::class);
$hereAddress = $hereAddress->withLocationId($item['id'] ?? null);
$hereAddress = $hereAddress->withLocationType($item['resultType'] ?? null);
$hereAddress = $hereAddress->withLocationName($item['title'] ?? null);

$additionalData = [];
if (isset($address['label'])) {
$additionalData[] = ['key' => 'Label', 'value' => $address['label']];
}
if (isset($address['countryName'])) {
$additionalData[] = ['key' => 'CountryName', 'value' => $address['countryName']];
}
if (isset($address['state'])) {
$additionalData[] = ['key' => 'StateName', 'value' => $address['state']];
}
if (isset($address['stateCode'])) {
$additionalData[] = ['key' => 'StateCode', 'value' => $address['stateCode']];
}
if (isset($address['county'])) {
$additionalData[] = ['key' => 'CountyName', 'value' => $address['county']];
}
if (isset($address['countyCode'])) {
$additionalData[] = ['key' => 'CountyCode', 'value' => $address['countyCode']];
}
if (isset($address['district'])) {
$additionalData[] = ['key' => 'District', 'value' => $address['district']];
}
if (isset($address['subdistrict'])) {
$additionalData[] = ['key' => 'Subdistrict', 'value' => $address['subdistrict']];
}
if (isset($address['streets'])) {
$additionalData[] = ['key' => 'Streets', 'value' => $address['streets']];
}
if (isset($address['block'])) {
$additionalData[] = ['key' => 'Block', 'value' => $address['block']];
}
if (isset($address['subblock'])) {
$additionalData[] = ['key' => 'Subblock', 'value' => $address['subblock']];
}
if (isset($address['building'])) {
$additionalData[] = ['key' => 'Building', 'value' => $address['building']];
}
if (isset($address['unit'])) {
$additionalData[] = ['key' => 'Unit', 'value' => $address['unit']];
}

// Item metadata
foreach (
[
'politicalView' => 'PoliticalView',
'houseNumberType' => 'HouseNumberType',
'addressBlockType' => 'AddressBlockType',
'localityType' => 'LocalityType',
'administrativeAreaType' => 'AdministrativeAreaType',
'distance' => 'Distance',
] as $sourceKey => $targetKey
) {
if (isset($item[$sourceKey])) {
$additionalData[] = ['key' => $targetKey, 'value' => $item[$sourceKey]];
}
}

$hereAddress = $hereAddress->withAdditionalData($additionalData);
$results[] = $hereAddress;

if (count($results) >= $limit) {
break;
}
}

return new AddressCollection($results);
}

private function parseV6Response(array $locations, int $limit): Collection
{
$results = [];

foreach ($locations as $loc) {
Expand Down Expand Up @@ -245,7 +447,7 @@ private function executeQuery(string $url, int $limit): Collection
$address = $builder->build(HereAddress::class);
$address = $address->withLocationId($location['LocationId'] ?? null);
$address = $address->withLocationType($location['LocationType']);
$address = $address->withAdditionalData(array_merge($additionalData, $extraAdditionalData));
$address = $address->withAdditionalData(array_merge($additionalData ?? [], $extraAdditionalData));
$address = $address->withShape($location['Shape'] ?? null);
$results[] = $address;

Expand Down Expand Up @@ -289,18 +491,13 @@ private function getAdditionalDataParam(GeocodeQuery $query): string
*/
private function withApiCredentials(array $queryParams): array
{
if (
empty($this->apiKey)
&& (empty($this->appId) || empty($this->appCode))
) {
throw new InvalidCredentials('Invalid or missing api key.');
}

if (null !== $this->apiKey) {
$queryParams['apiKey'] = $this->apiKey;
} else {
} elseif (!empty($this->appId) && !empty($this->appCode)) {
$queryParams['app_id'] = $this->appId;
$queryParams['app_code'] = $this->appCode;
} else {
throw new InvalidCredentials('Invalid or missing api key.');
}

return $queryParams;
Expand All @@ -310,6 +507,10 @@ public function getBaseUrl(Query $query): string
{
$usingApiKey = null !== $this->apiKey;

if ('7' === $this->version && $usingApiKey) {
return ($query instanceof ReverseQuery) ? self::GS7_REVERSE_ENDPOINT_URL : self::GS7_GEOCODE_ENDPOINT_URL;
}

if ($query instanceof ReverseQuery) {
if ($this->useCIT) {
return $usingApiKey ? self::REVERSE_CIT_ENDPOINT_URL_API_KEY : self::REVERSE_CIT_ENDPOINT_URL_APP_CODE;
Expand Down
Loading
Loading