dbconnection = $dbconnection; $this->memcache = $cacheFactory->createLocal('maps'); $this->jobList = $jobList; $this->appData = $appData; } // converts the address to geo lat;lon public function addressToGeo($adr, $uri): string { $geo = $this->lookupAddress($adr, $uri); return strval($geo[0]) . ';' . strval($geo[1]); } /** * Safely looks up an adr string * First: Checks if the adress is known and in the db * Uses this geo if it was looked up externally * Look's it up if it was not looked up * @param $adr * @param $uri ressource identifier (contact URI for example) * @return array($lat,$lng,$lookedUp) */ public function lookupAddress($adr, $uri): array { $adr_norm = strtolower(preg_replace('/\s+/', '', $adr)); $qb = $this->dbconnection->getQueryBuilder(); $qb->select('id', 'lat', 'lng', 'looked_up') ->from('maps_address_geo') ->where($qb->expr()->eq('object_uri', $qb->createNamedParameter($uri, IQueryBuilder::PARAM_STR))) ->andWhere($qb->expr()->eq('adr_norm', $qb->createNamedParameter($adr_norm, IQueryBuilder::PARAM_STR))); $req = $qb->executeQuery(); $lat = null; $lng = null; $inDb = false; while ($row = $req->fetch()) { if ($row['looked_up']) { $id = $row['id']; $lat = $row['lat']; $lng = $row['lng']; $lookedUp = false; $inDb = true; } else { $id = $row['id']; // if it's in the DB but not yet looked up, we can do it now // we first check if this address was already looked up $geo = $this->lookupAddressInternal($adr); // if not, ask external service if (!$geo[2]) { $geo = $this->lookupAddressExternal($adr); } $lat = $geo[0]; $lng = $geo[1]; $lookedUp = $geo[2]; $inDb = true; } break; } $req->closeCursor(); $qb = $this->dbconnection->getQueryBuilder(); // if it's still not in the DB, it means the lookup did not happen yet // so we can schedule it for later if (!$inDb) { if (strlen($adr) > 255) { $this->logger->notice('lookupAddress: Truncating $adr (entry too long) ' . $adr); $adr = substr($adr, 0, 255); } $foo = $this->scheduleForLookup($adr, $uri); $id = $foo[0]; $lat = $foo[1]; $lng = $foo[2]; $lookedUp = $foo[3]; } else { if ($lookedUp) { $qb->update('maps_address_geo') ->set('lat', $qb->createNamedParameter($lat, IQueryBuilder::PARAM_STR)) ->set('lng', $qb->createNamedParameter($lng, IQueryBuilder::PARAM_STR)) ->set('object_uri', $qb->createNamedParameter($uri, IQueryBuilder::PARAM_STR)) ->set('looked_up', $qb->createNamedParameter($lookedUp, IQueryBuilder::PARAM_BOOL)) ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_STR))); $qb->executeStatement(); } } return [$lat, $lng, $lookedUp]; } private function lookupAddressInternal($adr): array { $res = [null, null, false]; if (OpenLocationCode::isFull($adr)) { $decoded = OpenLocationCode::decode($adr); $res[0] = $decoded['latitudeCenter']; $res[1] = $decoded['longitudeCenter']; $res[2] = true; return $res; } $adr_norm = strtolower(preg_replace('/\s+/', '', $adr)); $qb = $this->dbconnection->getQueryBuilder(); $qb->select('lat', 'lng') ->from('maps_address_geo') ->where($qb->expr()->eq('looked_up', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))) ->andWhere($qb->expr()->eq('adr_norm', $qb->createNamedParameter($adr_norm, IQueryBuilder::PARAM_STR))); $req = $qb->executeQuery(); while ($row = $req->fetch()) { $res[0] = $row['lat']; $res[1] = $row['lng']; $res[2] = true; } $req->closeCursor(); return $res; } // looks up the address on external provider returns lat, lon, lookupstate // do lookup only if last one occured more than one second ago private function lookupAddressExternal($adr): array { if (time() - intval($this->memcache->get('lastAddressLookup')) >= 1) { $opts = [ 'http' => [ 'method' => 'GET', 'user_agent' => 'Nextcloud Maps app', ] ]; $context = stream_context_create($opts); // we get rid of "post office box" field $splitted_adr = explode(';', $adr); // remove blank lines (#706) $splitted_adr = array_filter(array_map('trim', $splitted_adr)); // ADR in VCard is mandated to 7 fields if (sizeof($splitted_adr) == 7) { $query_adr_parts = []; // This matches the nominatim query with the fields of 'ADR' in VCard $query_key_part = ['','','street', 'city','state', 'postalcode', 'country']; foreach ($query_key_part as $index => $query_key) { if ($query_key !== '' && $splitted_adr[$index] !== '') { $query_adr_parts += $query_key . '=' . urlencode($splitted_adr[$index]); } } $query_adr = implode(';', $query_adr_parts); } else { // Try to do our best with a naive query $query_adr = 'q=' . urlencode(implode(', ', $splitted_adr)); } $result_json = @file_get_contents( 'https://nominatim.openstreetmap.org/search?format=jsonv2&' . $query_adr, false, $context ); if ($result_json !== false) { $result = \json_decode($result_json, true); if (!(key_exists('request_failed', $result) and $result['request_failed'])) { $this->logger->debug('External looked up address: ' . $adr . ' with result' . print_r($result, true)); $this->memcache->set('lastAddressLookup', time()); $lat = null; $lon = null; foreach ($result as $addr) { if (key_exists('lat', $addr) and key_exists('lon', $addr)) { if (is_null($lat) or (key_exists('category', $addr) and in_array($addr['category'], ['place', 'building', 'amenity']))) { $lat = $addr['lat']; $lon = $addr['lon']; } } } return [$lat, $lon, true]; } } $this->logger->debug('Externally looked failed'); } return [null, null, false]; } // launch lookup for all addresses of the vCard public function scheduleVCardForLookup($cardData, $cardUri) { $vCard = Reader::read($cardData); $this->cleanUpDBContactAddresses($vCard, $cardUri); foreach ($vCard->children() as $property) { if ($property->name === 'ADR') { $adr = $property->getValue(); if ($adr !== ';;;;;;') { $this->lookupAddress($property->getValue(), $cardUri); } } } } private function cleanUpDBContactAddresses($vCard, $uri) { $qb = $this->dbconnection->getQueryBuilder(); // get all vcard addresses $vCardAddresses = []; foreach ($vCard->children() as $property) { if ($property->name === 'ADR') { $adr = $property->getValue(); array_push($vCardAddresses, $adr); } } // check which addresses from DB is not in the vCard anymore $adrIdToDelete = []; $qb->select('id', 'adr') ->from('maps_address_geo') ->where($qb->expr()->eq('object_uri', $qb->createNamedParameter($uri, IQueryBuilder::PARAM_STR))); $req = $qb->executeQuery(); while ($row = $req->fetch()) { if (!in_array($row['adr'], $vCardAddresses)) { array_push($adrIdToDelete, $row['id']); } } $req->closeCursor(); foreach ($adrIdToDelete as $id) { $qb = $this->dbconnection->getQueryBuilder(); $qb->delete('maps_address_geo') ->where( $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) ); $qb->executeStatement(); } } public function deleteDBContactAddresses($uri) { $qb = $this->dbconnection->getQueryBuilder(); $qb->delete('maps_address_geo') ->where( $qb->expr()->eq('object_uri', $qb->createNamedParameter($uri, IQueryBuilder::PARAM_STR)) ); $qb->executeStatement(); } // schedules the address for an external lookup private function scheduleForLookup($adr, $uri): array { $geo = $this->lookupAddressInternal($adr); // if not found internally, ask external service if (!$geo[2]) { $geo = $this->lookupAddressExternal($adr); } $adr_norm = strtolower(preg_replace('/\s+/', '', $adr)); $qb = $this->dbconnection->getQueryBuilder(); $qb->insert('maps_address_geo') ->values([ 'adr' => $qb->createNamedParameter($adr, IQueryBuilder::PARAM_STR), 'adr_norm' => $qb->createNamedParameter($adr_norm, IQueryBuilder::PARAM_STR), 'object_uri' => $qb->createNamedParameter($uri, IQueryBuilder::PARAM_STR), 'lat' => $qb->createNamedParameter($geo[0], IQueryBuilder::PARAM_STR), 'lng' => $qb->createNamedParameter($geo[1], IQueryBuilder::PARAM_STR), 'looked_up' => $qb->createNamedParameter($geo[2], IQueryBuilder::PARAM_BOOL), ]); $qb->executeStatement(); $id = $qb->getLastInsertId(); if (!$geo[2]) { $this->jobList->add(LookupMissingGeoJob::class, []); } return [$id, $geo[0], $geo[1], $geo[2]]; } // looks up the geo information which have not been looked up // this is called by the Cron job public function lookupMissingGeo($max = 200):bool { // stores if all addresses where looked up $lookedUpAll = true; $qb = $this->dbconnection->getQueryBuilder(); $qb->select('adr', 'object_uri') ->from('maps_address_geo') ->where($qb->expr()->eq('looked_up', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) ->setMaxResults($max); $req = $qb->executeQuery(); $result = $req->fetchAll(); $req->closeCursor(); $i = 0; foreach ($result as $row) { $i++; $geo = $this->lookupAddress($row['adr'], $row['object_uri']); // lookup failed if (!$geo[2]) { $lookedUpAll = false; } \sleep(1); \usleep(\rand(100, 100000)); } // not all addresses where loaded from database if ($i === $max) { $lookedUpAll = false; } if ($lookedUpAll) { $this->logger->debug('Successfully looked up all addresses during cron job'); } else { $this->logger->debug('Failed to look up all addresses during cron job'); } return $lookedUpAll; } }