├── Classes └── Service │ ├── GeoService.php │ └── RadiusService.php ├── Configuration └── Services.yaml ├── README.md ├── Resources └── Public │ └── Icons │ └── Extension.svg ├── composer.json ├── ext_conf_template.txt ├── ext_emconf.php └── ext_localconf.php /Classes/Service/GeoService.php: -------------------------------------------------------------------------------- 1 | get('geocoding'); 39 | // load from extension configuration 40 | $apiKey = $geoCodingConfig['googleApiKey'] ?? ''; 41 | if (!empty($apiKey)) { 42 | $this->geocodingUrl .= '&key=' . $apiKey; 43 | } 44 | $this->maxRetries = (int)($geoCodingConfig['maxRetries'] ?? 0); 45 | } 46 | 47 | /** 48 | * core functionality: asks google for the coordinates of an address 49 | * stores known addresses in a local cache. 50 | * 51 | * @return array an array with latitude and longitude 52 | */ 53 | public function getCoordinatesForAddress(?string $street = null, ?string $zip = null, ?string $city = null, ?string $country = 'Germany'): array 54 | { 55 | $addressParts = []; 56 | foreach ([$street, $zip . ' ' . $city, $country] as $addressPart) { 57 | if ($addressPart === null) { 58 | continue; 59 | } 60 | if (strlen(trim($addressPart)) <= 0) { 61 | continue; 62 | } 63 | $addressParts[] = trim($addressPart); 64 | } 65 | 66 | if ($addressParts === []) { 67 | return []; 68 | } 69 | 70 | $address = ltrim(implode(',', $addressParts), ','); 71 | if (empty($address)) { 72 | return []; 73 | } 74 | 75 | $cacheKey = 'geocode-' . strtolower(str_replace(' ', '-', preg_replace('/[^0-9a-zA-Z ]/m', '', $address))); 76 | 77 | // Found in cache? Return it. 78 | if ($this->cache->has($cacheKey)) { 79 | return $this->cache->get($cacheKey); 80 | } 81 | 82 | $result = $this->getApiCallResult( 83 | $this->geocodingUrl . '&address=' . urlencode($address), 84 | $this->maxRetries 85 | ); 86 | 87 | if (empty($result['results']) || empty($result['results'][0]['geometry'])) { 88 | return []; 89 | } 90 | $geometry = $result['results'][0]['geometry']; 91 | $result = [ 92 | 'latitude' => $geometry['location']['lat'], 93 | 'longitude' => $geometry['location']['lng'], 94 | ]; 95 | // Now store the $result in cache and return 96 | $this->cache->set($cacheKey, $result, [], $this->cacheTime); 97 | return $result; 98 | } 99 | 100 | /** 101 | * geocodes all missing records in a DB table and then stores the values 102 | * in the DB record. 103 | * 104 | * only works if your DB table has the necessary fields 105 | * helpful when calculating a batch of addresses and save the latitude/longitude automatically 106 | */ 107 | public function calculateCoordinatesForAllRecordsInTable( 108 | string $tableName, 109 | string $latitudeField = 'latitude', 110 | string $longitudeField = 'longitude', 111 | string $streetField = 'street', 112 | string $zipField = 'zip', 113 | string $cityField = 'city', 114 | string $countryField = 'country', 115 | string $addWhereClause = '' 116 | ): int { 117 | // Fetch all records without latitude/longitude 118 | $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName); 119 | $queryBuilder = $connection->createQueryBuilder(); 120 | $queryBuilder->getRestrictions() 121 | ->removeAll() 122 | ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 123 | $queryBuilder 124 | ->select('*') 125 | ->from($tableName) 126 | ->where( 127 | $queryBuilder->expr()->orX( 128 | $queryBuilder->expr()->isNull($latitudeField), 129 | $queryBuilder->expr()->eq($latitudeField, $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)), 130 | $queryBuilder->expr()->eq($latitudeField, 0.00000000000), 131 | $queryBuilder->expr()->isNull($longitudeField), 132 | $queryBuilder->expr()->eq($longitudeField, $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)), 133 | $queryBuilder->expr()->eq($longitudeField, 0.00000000000) 134 | ) 135 | ) 136 | ->setMaxResults(500); 137 | 138 | if (!empty($addWhereClause)) { 139 | $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($addWhereClause)); 140 | } 141 | 142 | $records = $queryBuilder->executeQuery()->fetchAllAssociative(); 143 | 144 | foreach ($records as $record) { 145 | $country = $record[$countryField] ?? ''; 146 | // resolve the label for the country 147 | if (($GLOBALS['TCA'][$tableName]['columns'][$countryField]['config']['type'] ?? '') === 'select') { 148 | foreach ($GLOBALS['TCA'][$tableName]['columns'][$countryField]['config']['items'] ?? [] as $itm) { 149 | if (($itm[1] ?? null) === $country) { 150 | if (is_object($GLOBALS['TSFE'])) { 151 | $country = $GLOBALS['TSFE']->sL($itm[0]); 152 | } else { 153 | $country = $GLOBALS['LANG']->sL($itm[0]); 154 | } 155 | } 156 | } 157 | } 158 | // do the geocoding 159 | if (!empty($record[$zipField]) || !empty($record[$cityField])) { 160 | $coords = $this->getCoordinatesForAddress($record[$streetField] ?? null, $record[$zipField] ?? null, $record[$cityField] ?? null, $country); 161 | if ($coords) { 162 | // Update the record to fill in the latitude and longitude values in the DB 163 | $connection->update( 164 | $tableName, 165 | [ 166 | $latitudeField => $coords['latitude'], 167 | $longitudeField => $coords['longitude'], 168 | ], 169 | [ 170 | 'uid' => $record['uid'], 171 | ] 172 | ); 173 | } 174 | } 175 | } 176 | 177 | return count($records); 178 | } 179 | 180 | protected function getApiCallResult(string $url, int $remainingTries = 10): array 181 | { 182 | $response = $this->requestFactory->request($url); 183 | 184 | $result = json_decode($response->getBody()->getContents(), true) ?? []; 185 | if (!in_array(($result['status'] ?? ''), ['OK', 'OVER_QUERY_LIMIT'], true)) { 186 | throw new \RuntimeException( 187 | sprintf( 188 | 'Request to Google Maps API returned status "%s". Got following error message: "%s"', 189 | $result['status'] ?? 0, 190 | $result['error_message'] ?? '' 191 | ), 192 | 1621512170 193 | ); 194 | } 195 | 196 | if (($result['status'] ?? '') === 'OVER_QUERY_LIMIT' || $remainingTries <= 0) { 197 | return $result; 198 | } 199 | return $this->getApiCallResult($url, $remainingTries - 1); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Classes/Service/RadiusService.php: -------------------------------------------------------------------------------- 1 | earthRadius * $c; 45 | } 46 | 47 | /** 48 | * fetches all records within a certain radius of given coordinates 49 | * see http://spinczyk.net/blog/2009/10/04/radius-search-with-google-maps-and-mysql/. 50 | * 51 | * @param array $coordinates an associative array with "latitude" and "longitude" keys 52 | * @param int $maxDistance the radius in kilometers 53 | * @param string $tableName the DB table that should be queried 54 | * @param string $latitudeField the DB field that holds the latitude coordinates 55 | * @param string $longitudeField the DB field that holds the longitude coordinates 56 | * @param string $additionalFields additional fields to be selected from the table (uid is always selected) 57 | */ 58 | public function findAllDatabaseRecordsInRadius( 59 | array $coordinates, 60 | int $maxDistance = 250, 61 | string $tableName = 'pages', 62 | string $latitudeField = 'latitude', 63 | string $longitudeField = 'longitude', 64 | string $additionalFields = '' 65 | ): array { 66 | $fields = GeneralUtility::trimExplode(',', 'uid,' . $additionalFields, true); 67 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName); 68 | 69 | $distanceSqlCalc = 'ACOS(SIN(RADIANS(' . $queryBuilder->quoteIdentifier($latitudeField) . ')) * SIN(RADIANS(' . (float)$coordinates['latitude'] . ')) + COS(RADIANS(' . $queryBuilder->quoteIdentifier($latitudeField) . ')) * COS(RADIANS(' . (float)($coordinates['latitude'] ?? 0) . ')) * COS(RADIANS(' . $queryBuilder->quoteIdentifier($longitudeField) . ') - RADIANS(' . (float)($coordinates['longitude'] ?? 0) . '))) * ' . $this->earthRadius; 70 | 71 | return $queryBuilder 72 | ->select(...$fields) 73 | ->addSelectLiteral( 74 | $distanceSqlCalc . ' AS `distance`' 75 | ) 76 | ->from($tableName) 77 | ->where( 78 | $queryBuilder->expr()->comparison($distanceSqlCalc, ExpressionBuilder::LT, $maxDistance) 79 | ) 80 | ->orderBy('distance') 81 | ->executeQuery() 82 | ->fetchAllAssociative(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Configuration/Services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autowire: true 4 | autoconfigure: true 5 | public: false 6 | 7 | B13\Geocoding\: 8 | resource: '../Classes/*' 9 | 10 | cache.ext-geocoding: 11 | class: TYPO3\CMS\Core\Cache\Frontend\FrontendInterface 12 | factory: [ '@TYPO3\CMS\Core\Cache\CacheManager', 'getCache' ] 13 | arguments: [ 'geocoding' ] 14 | 15 | B13\Geocoding\Service\GeoService: 16 | public: true 17 | arguments: 18 | $cache: '@cache.ext-geocoding' 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TYPO3 Extension: Geocoding 2 | 3 | Provides services for querying Google Maps GeoCoding API v3 in your own extensions. 4 | 5 | * Extension Key: geocoding 6 | * Author: Benjamin Mack, b13 GmbH, 2012-2024 7 | * Licensed under: GPLv2+ 8 | * Requires TYPO3 11+ and PHP 8.1 (see older versions of EXT:geocoding for support for previous TYPO3 versions) 9 | * All code can be found and developed on github: https://github.com/b13/geocoding/ 10 | 11 | ## Introduction 12 | This extension provides an abstract way to get geo coordinates of addresses around the world. "Geocoding" let you fetch information about an address and stores it in the DB, by using the TYPO3 Caching Framework. 13 | 14 | ## Installation 15 | Use `composer req b13/geocoding` or install it via TYPO3's Extension Manager from the TYPO3 Extension Repository using the extension key `geocoding`. 16 | 17 | ## Configuration 18 | Fetch a Google API key (https://code.google.com/apis/console) and add it to the extension configuration info in the Extension Manager. For more information see here: https://developers.google.com/maps/documentation/geocoding/?hl=en 19 | 20 | ## How to use 21 | Inject the class in your TYPO3 extension. In the rare case, that you cannot use dependency injection, `GeneralUtility::makeInstance()` works as well. 22 | 23 | ## GeoService 24 | The extension provides the `GeoService`, a PHP service class to fetch latitude and longitude for a specific address string. 25 | 26 | ### GeoService->calculateCoordinatesForAllRecordsInTable 27 | 28 | If you need to query user input, a JavaScript API is probably the best way to do so. However, it can be done via PHP as well, by calling `GeoService->getCoordinatesForAddress($street, $zip, $city, $country)` 29 | 30 | $coordinates = $this->geoServiceObject->getCoordinatesForAddress('Breitscheidstr. 65', 70176, 'Stuttgart', 'Germany'); 31 | 32 | The method also caches the result of the query. 33 | 34 | ### GeoService->calculateCoordinatesForAllRecordsInTable 35 | 36 | The method `GeoService->calculateCoordinatesForAllRecordsInTable($tableName, $latitudeField, $longitudeField, $streetField, $zipField, $cityField, $countryField, $addWhereClause)` allows you to bulk-encode latitude and longitude fields for existing addresses. The call can easily be built inside a Scheduler Task (see example below). 37 | 38 | This way you can fetch the information about an address of a DB record (e.g. tt_address) and store the data in the database table, given that you add two new fields latitude and longitude to that target table in your extension (no TCA information required for that). 39 | 40 | ### Example: Using GeoService as a Scheduler Task for tt_address 41 | 42 | Put this into `EXT:my_extension/Classes/Task/GeocodingTask.php`: 43 | 44 | ```php 45 | calculateCoordinatesForAllRecordsInTable( 62 | 'tt_address', 63 | 'latitude', 64 | 'longitude', 65 | 'address', 66 | 'zip', 67 | 'city', 68 | 'country' 69 | ); 70 | return true; 71 | } 72 | } 73 | ``` 74 | 75 | And also register this class within `ext_localconf.php` of your extension: 76 | 77 | ```php 78 | $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\MyVendor\MyExtension\Task\GeocodingTask::class] = [ 79 | 'extension' => 'myextension', 80 | 'title' => 'Geocoding of address records', 81 | 'description' => 'Check all tt_address records for geocoding information and write them into the fields' 82 | ]; 83 | ``` 84 | 85 | ### RadiusService 86 | The other main service class is used for the calculating distances between two coordinates (`RadiusService->getDistance()`, and querying records from a DB table with latitude and longitude (works perfectly in conjunction with `calculateCoordinatesForAllRecordsInTable()`) given a certain radius and base coordinates. 87 | 88 | 89 | ## Thanks / Contributions 90 | 91 | Thanks go to 92 | 93 | * The crew at b13, making use of these features 94 | * Jesus Christ, who saved my life. 95 | 96 | 2013-07-05, Benni. 97 | -------------------------------------------------------------------------------- /Resources/Public/Icons/Extension.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "b13/geocoding", 3 | "type": "typo3-cms-extension", 4 | "description": "Services for using geocoding from Google inside TYPO3 database records", 5 | "homepage": "https://github.com/b13/t3ext-geocoding", 6 | "license": "GPL-2.0-or-later", 7 | "keywords": ["TYPO3 CMS", "TYPO3", "Google Geocoding"], 8 | "require": { 9 | "php": "^8.1", 10 | "typo3/cms-core": "^11.5 || ^12.4" 11 | }, 12 | "extra": { 13 | "typo3/cms": { 14 | "extension-key": "geocoding" 15 | } 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "B13\\Geocoding\\": "Classes/" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ext_conf_template.txt: -------------------------------------------------------------------------------- 1 | #cat=basic;type=string;label=Google API Key 2 | googleApiKey = 3 | 4 | #cat=basic;type=int;label=Maximum amount of retries in case of an OVER_QUERY_LIMIT response 5 | maxRetries = 10 6 | -------------------------------------------------------------------------------- /ext_emconf.php: -------------------------------------------------------------------------------- 1 | 'Service: Geocoding via Google Maps', 5 | 'description' => 'Provides services for google maps GeoCoding API and radius search on the database.', 6 | 'category' => 'sv', 7 | 'author' => 'Benjamin Mack', 8 | 'author_email' => 'benjamin.mack@b13.com', 9 | 'author_company' => 'b13 GmbH', 10 | 'shy' => '', 11 | 'state' => 'stable', 12 | 'uploadfolder' => 0, 13 | 'createDirs' => '', 14 | 'clearCacheOnLoad' => 0, 15 | 'lockType' => '', 16 | 'version' => '5.0.0', 17 | 'constraints' => [ 18 | 'depends' => [ 19 | 'typo3' => '11.5.0-12.4.99', 20 | ], 21 | 'conflicts' => [ 22 | ], 23 | 'suggests' => [ 24 | ], 25 | ], 26 | ]; 27 | -------------------------------------------------------------------------------- /ext_localconf.php: -------------------------------------------------------------------------------- 1 | \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class, 9 | 'backend' => \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class, 10 | ]; 11 | } 12 | --------------------------------------------------------------------------------