From 68a39918b5405ebe9a01974818661d602dbe7bd8 Mon Sep 17 00:00:00 2001 From: Raymond Huang Date: Thu, 26 Jan 2023 02:41:55 +0800 Subject: [PATCH] feat: show photos taken in locations visible in map --- appinfo/routes.php | 6 +- lib/Controller/LocationsController.php | 299 ++++++++++++++++++ lib/Controller/PageController.php | 25 +- lib/Db/TimelineQueryDays.php | 179 ++++++++++- package-lock.json | 164 +++++++--- package.json | 3 + src/App.vue | 25 +- src/components/Timeline.vue | 152 ++++----- .../top-matter/LocationTopMatter.vue | 105 ++++++ src/components/top-matter/TopMatter.vue | 15 +- src/router.ts | 9 + src/services/API.ts | 9 + src/types.ts | 8 + 13 files changed, 840 insertions(+), 159 deletions(-) create mode 100644 lib/Controller/LocationsController.php create mode 100644 src/components/top-matter/LocationTopMatter.vue diff --git a/appinfo/routes.php b/appinfo/routes.php index aa549204..ee0ca26c 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -21,6 +21,7 @@ return [ ['name' => 'Page#videos', 'url' => '/videos', 'verb' => 'GET'], ['name' => 'Page#archive', 'url' => '/archive', 'verb' => 'GET'], ['name' => 'Page#thisday', 'url' => '/thisday', 'verb' => 'GET'], + ['name' => 'Page#locations', 'url' => '/locations', 'verb' => 'GET'], // Routes with params w(['name' => 'Page#folder', 'url' => '/folders/{path}', 'verb' => 'GET'], 'path'), @@ -76,10 +77,13 @@ return [ ['name' => 'Download#file', 'url' => '/api/download/{handle}', 'verb' => 'GET'], ['name' => 'Download#one', 'url' => '/api/stream/{fileid}', 'verb' => 'GET'], + ['name' => 'Locations#daysWithBounds', 'url' => '/api/location/days/{minLat}/{maxLat}/{minLng}/{maxLng}', 'verb' => 'GET'], + ['name' => 'Locations#dayWithBounds', 'url' => '/api/location/days/{id}/{minLat}/{maxLat}/{minLng}/{maxLng}', 'verb' => 'GET'], + // Config API ['name' => 'Other#setUserConfig', 'url' => '/api/config/{key}', 'verb' => 'PUT'], // Service worker ['name' => 'Other#serviceWorker', 'url' => '/service-worker.js', 'verb' => 'GET'], ] -]; +]; \ No newline at end of file diff --git a/lib/Controller/LocationsController.php b/lib/Controller/LocationsController.php new file mode 100644 index 00000000..c9fe847a --- /dev/null +++ b/lib/Controller/LocationsController.php @@ -0,0 +1,299 @@ + + * @author Varun Patil , Raymond Huang + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\Memories\Controller; + +use OCA\Memories\Db\TimelineRoot; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; + +class LocationsController extends ApiBase +{ + use FoldersTrait; + + /** + * @NoAdminRequired + * + * @PublicPage + */ + public function daysWithBounds(string $minLat, string $maxLat, string $minLng, string $maxLng): JSONResponse + { + if (null === $minLat || null === $maxLat || null === $minLng || null === $maxLng) { + return new JSONResponse([], Http::STATUS_BAD_REQUEST); + } + + // Get the folder to show + try { + $uid = $this->getUID(); + } catch (\Exception $e) { + return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_PRECONDITION_FAILED); + } + + // Get the folder to show + $root = null; + + try { + $root = $this->getRequestRoot(); + } catch (\Exception $e) { + return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + + // Run actual query + try { + $list = $this->timelineQuery->getDaysWithBounds( + $root, + $uid, + $this->isRecursive(), + $this->isArchive(), + $this->getTransformations(true), + $minLat, + $maxLat, + $minLng, + $maxLng + ); + + if ($this->isMonthView()) { + // Group days together into months + $list = $this->timelineQuery->daysToMonths($list); + } else { + // Preload some day responses + $this->preloadDays($list, $uid, $root); + } + + // Reverse response if requested. Folders still stay at top. + if ($this->isReverse()) { + $list = array_reverse($list); + } + + // Add subfolder info if querying non-recursively + if (!$this->isRecursive()) { + array_unshift($list, $this->getSubfoldersEntry($root->getFolder($root->getOneId()))); + } + + return new JSONResponse($list, Http::STATUS_OK); + } catch (\Exception $e) { + return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + public function dayWithBounds(string $id, string $minLat, string $maxLat, string $minLng, string $maxLng): JSONResponse + { + // $minLat = (float) $minLat; + // $maxLat = (float) $maxLat; + // $minLng = (float) $minLng; + // $maxLng = (float) $maxLng; + if (null === $minLat || null === $maxLat || null === $minLng || null === $maxLng) { + return new JSONResponse([], Http::STATUS_BAD_REQUEST); + } + + // Get user + $uid = $this->getUID(); + + // Check for wildcard + $dayIds = []; + if ('*' === $id) { + $dayIds = null; + } else { + // Split at commas and convert all parts to int + $dayIds = array_map(function ($part) { + return (int) $part; + }, explode(',', $id)); + } + + // Check if $dayIds is empty + if (null !== $dayIds && 0 === \count($dayIds)) { + return new JSONResponse([], Http::STATUS_OK); + } + + // Get the folder to show + $root = null; + + try { + $root = $this->getRequestRoot(); + } catch (\Exception $e) { + return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + + // Convert to actual dayIds if month view + if ($this->isMonthView()) { + $dayIds = $this->timelineQuery->monthIdToDayIds((int) $dayIds[0]); + } + + // Run actual query + try { + $list = $this->timelineQuery->getDayWithBounds( + $root, + $uid, + $dayIds, + $this->isRecursive(), + $this->isArchive(), + $this->getTransformations(false), + $minLat, + $maxLat, + $minLng, + $maxLng + ); + + // Force month id for dayId for month view + if ($this->isMonthView()) { + foreach ($list as &$photo) { + $photo['dayid'] = (int) $dayIds[0]; + } + } + + // Reverse response if requested. + if ($this->isReverse()) { + $list = array_reverse($list); + } + + return new JSONResponse($list, Http::STATUS_OK); + } catch (\Exception $e) { + return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Get transformations depending on the request. + * + * @param bool $aggregateOnly Only apply transformations for aggregation (days call) + */ + private function getTransformations(bool $aggregateOnly) + { + $transforms = []; + + // Add extra information, basename and mimetype + if (!$aggregateOnly && ($fields = $this->request->getParam('fields'))) { + $fields = explode(',', $fields); + $transforms[] = [$this->timelineQuery, 'transformExtraFields', $fields]; + } + + // Filter for one album + if ($this->albumsIsEnabled()) { + if ($albumId = $this->request->getParam('album')) { + $transforms[] = [$this->timelineQuery, 'transformAlbumFilter', $albumId]; + } + } + + // Other transforms not allowed for public shares + if (null === $this->userSession->getUser()) { + return $transforms; + } + + // Filter only favorites + if ($this->request->getParam('fav')) { + $transforms[] = [$this->timelineQuery, 'transformFavoriteFilter']; + } + + // Filter only videos + if ($this->request->getParam('vid')) { + $transforms[] = [$this->timelineQuery, 'transformVideoFilter']; + } + + // Filter only for one face on Recognize + if (($recognize = $this->request->getParam('recognize')) && $this->recognizeIsEnabled()) { + $transforms[] = [$this->timelineQuery, 'transformPeopleRecognitionFilter', $recognize]; + + $faceRect = $this->request->getParam('facerect'); + if ($faceRect && !$aggregateOnly) { + $transforms[] = [$this->timelineQuery, 'transformPeopleRecognizeRect', $recognize]; + } + } + + // Filter only for one face on Face Recognition + if (($face = $this->request->getParam('facerecognition')) && $this->facerecognitionIsEnabled()) { + $currentModel = (int) $this->config->getAppValue('facerecognition', 'model', -1); + $transforms[] = [$this->timelineQuery, 'transformPeopleFaceRecognitionFilter', $currentModel, $face]; + + $faceRect = $this->request->getParam('facerect'); + if ($faceRect && !$aggregateOnly) { + $transforms[] = [$this->timelineQuery, 'transformPeopleFaceRecognitionRect', $face]; + } + } + + // Filter only for one tag + if ($this->tagsIsEnabled()) { + if ($tagName = $this->request->getParam('tag')) { + $transforms[] = [$this->timelineQuery, 'transformTagFilter', $tagName]; + } + } + + // Limit number of responses for day query + $limit = $this->request->getParam('limit'); + if ($limit) { + $transforms[] = [$this->timelineQuery, 'transformLimitDay', (int) $limit]; + } + + return $transforms; + } + + /** + * Preload a few "day" at the start of "days" response. + * + * @param array $days the days array + * @param string $uid User ID or blank for public shares + * @param TimelineRoot $root the root folder + */ + private function preloadDays(array &$days, string $uid, TimelineRoot &$root) + { + $transforms = $this->getTransformations(false); + $preloaded = 0; + $preloadDayIds = []; + $preloadDays = []; + foreach ($days as &$day) { + if ($day['count'] <= 0) { + continue; + } + + $preloaded += $day['count']; + $preloadDayIds[] = $day['dayid']; + $preloadDays[] = & $day; + + if ($preloaded >= 50 || \count($preloadDayIds) > 5) { // should be enough + break; + } + } + + if (\count($preloadDayIds) > 0) { + $allDetails = $this->timelineQuery->getDay( + $root, + $uid, + $preloadDayIds, + $this->isRecursive(), + $this->isArchive(), + $transforms, + ); + + // Group into dayid + $detailMap = []; + foreach ($allDetails as &$detail) { + $detailMap[$detail['dayid']][] = & $detail; + } + foreach ($preloadDays as &$day) { + $m = $detailMap[$day['dayid']]; + if (isset($m) && null !== $m && \count($m) > 0) { + $day['detail'] = $m; + } + } + } + } +} \ No newline at end of file diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index df9e17d6..49438ac9 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -34,7 +34,8 @@ class PageController extends Controller IInitialState $initialState, IUserSession $userSession, IConfig $config - ) { + ) + { parent::__construct($AppName, $request); $this->userId = $UserId; $this->appName = $AppName; @@ -68,19 +69,22 @@ class PageController extends Controller Application::APPNAME, 'timelinePath', 'EMPTY' - )); + ) + ); $this->initialState->provideInitialState('foldersPath', $this->config->getUserValue( $uid, Application::APPNAME, 'foldersPath', '/' - )); + ) + ); $this->initialState->provideInitialState('showHidden', $this->config->getUserValue( $uid, Application::APPNAME, 'showHidden', false - )); + ) + ); // Apps enabled $this->initialState->provideInitialState('systemtags', true === $this->appManager->isEnabledForUser('systemtags')); @@ -117,6 +121,7 @@ class PageController extends Controller // Allow nominatim for metadata $policy->addAllowedConnectDomain('nominatim.openstreetmap.org'); $policy->addAllowedFrameDomain('www.openstreetmap.org'); + $policy->addAllowedImageDomain('https://*.tile.openstreetmap.org'); return $policy; } @@ -223,4 +228,14 @@ class PageController extends Controller { return $this->main(); } -} + + /** + * @NoAdminRequired + * + * @NoCSRFRequired + */ + public function locations() + { + return $this->main(); + } +} \ No newline at end of file diff --git a/lib/Db/TimelineQueryDays.php b/lib/Db/TimelineQueryDays.php index 1e238863..f576c57e 100644 --- a/lib/Db/TimelineQueryDays.php +++ b/lib/Db/TimelineQueryDays.php @@ -99,7 +99,8 @@ trait TimelineQueryDays bool $recursive, bool $archive, array $queryTransforms = [] - ): array { + ): array + { $query = $this->connection->getQueryBuilder(); // Get all entries also present in filecache @@ -124,6 +125,64 @@ trait TimelineQueryDays return $this->processDays($rows); } + /** + * Get the geomatrically filtered days response from the database for the location timeline. + * + * @param TimelineRoot $root The root to get the days from + * @param bool $recursive Whether to get the days recursively + * @param bool $archive Whether to get the days only from the archive folder + * @param array $queryTransforms An array of query transforms to apply to the query + * @param float $minLat The minimum latitude + * @param float $maxLat The maximum latitude + * @param float $minLng The minimum longitude + * @param float $maxLng The maximum longitude + * + * @return array The days response + */ + public function getDaysWithBounds( + TimelineRoot &$root, + string $uid, + bool $recursive, + bool $archive, + array $queryTransforms = [], + string $minLat, + string $maxLat, + string $minLng, + string $maxLng + ): array + { + $query = $this->connection->getQueryBuilder(); + + // Get all entries also present in filecache + $count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count'); + $query->select('m.dayid', $count) + ->from('memories', 'm') + ->where( + $query->expr()->andX( + $query->expr()->gte('m.latitude', $query->createNamedParameter($minLat, IQueryBuilder::PARAM_STR)), + $query->expr()->lte('m.latitude', $query->createNamedParameter($maxLat, IQueryBuilder::PARAM_STR)), + $query->expr()->gte('m.longitude', $query->createNamedParameter($minLng, IQueryBuilder::PARAM_STR)), + $query->expr()->lte('m.longitude', $query->createNamedParameter($maxLng, IQueryBuilder::PARAM_STR)) + ) + ) + ; + $query = $this->joinFilecache($query, $root, $recursive, $archive); + + // Group and sort by dayid + $query->groupBy('m.dayid') + ->orderBy('m.dayid', 'DESC') + ; + + // Apply all transformations + $this->applyAllTransforms($queryTransforms, $query, $uid); + + $cursor = $this->executeQueryWithCTEs($query); + $rows = $cursor->fetchAll(); + $cursor->closeCursor(); + + return $this->processDays($rows); + } + /** * Get the day response from the database for the timeline. * @@ -144,7 +203,8 @@ trait TimelineQueryDays bool $recursive, bool $archive, array $queryTransforms = [] - ): array { + ): array + { $query = $this->connection->getQueryBuilder(); // Get all entries also present in filecache @@ -195,6 +255,95 @@ trait TimelineQueryDays return $this->processDay($rows, $uid, $root); } + /** + * Get the geomatrically filtered day response from the database for the location timeline. + * + * @param TimelineRoot $root The root to get the day from + * @param string $uid The user id + * @param int[] $day_ids The day ids to fetch + * @param bool $recursive If the query should be recursive + * @param bool $archive If the query should include only the archive folder + * @param array $queryTransforms The query transformations to apply + * @param mixed $day_ids + * @param mixed $minLat The minimum latitude + * @param mixed $maxLat The maximum latitude + * @param mixed $minLng The minimum longitude + * @param mixed $maxLng The maximum longitude + * + * @return array An array of day responses + */ + + public function getDayWithBounds( + TimelineRoot &$root, + string $uid, + ?array $day_ids, + bool $recursive, + bool $archive, + array $queryTransforms = [], + string $minLat, + string $maxLat, + string $minLng, + string $maxLng + ): array + { + $query = $this->connection->getQueryBuilder(); + + // Get all entries also present in filecache + $fileid = $query->createFunction('DISTINCT m.fileid'); + + // We don't actually use m.datetaken here, but postgres + // needs that all fields in ORDER BY are also in SELECT + // when using DISTINCT on selected fields + $query->select($fileid, 'm.isvideo', 'm.video_duration', 'm.datetaken', 'm.dayid', 'm.w', 'm.h', 'm.liveid') + ->from('memories', 'm') + ->where( + $query->expr()->andX( + $query->expr()->gte('m.latitude', $query->createNamedParameter($minLat, IQueryBuilder::PARAM_STR)), + $query->expr()->lte('m.latitude', $query->createNamedParameter($maxLat, IQueryBuilder::PARAM_STR)), + $query->expr()->gte('m.longitude', $query->createNamedParameter($minLng, IQueryBuilder::PARAM_STR)), + $query->expr()->lte('m.longitude', $query->createNamedParameter($maxLng, IQueryBuilder::PARAM_STR)) + ) + ) + ; + + // JOIN with filecache for existing files + $query = $this->joinFilecache($query, $root, $recursive, $archive); + $query->addSelect('f.etag', 'f.path', 'f.name AS basename'); + + // SELECT rootid if not a single folder + if ($recursive && !$root->isEmpty()) { + $query->addSelect('cte_f.rootid'); + } + + // JOIN with mimetypes to get the mimetype + $query->join('f', 'mimetypes', 'mimetypes', $query->expr()->eq('f.mimetype', 'mimetypes.id')); + $query->addSelect('mimetypes.mimetype'); + + // Filter by dayid unless wildcard + if (null !== $day_ids) { + $query->andWhere($query->expr()->in('m.dayid', $query->createNamedParameter($day_ids, IQueryBuilder::PARAM_INT_ARRAY))); + } else { + // Limit wildcard to 100 results + $query->setMaxResults(100); + } + + // Add favorite field + $this->addFavoriteTag($query, $uid); + + // Group and sort by date taken + $query->orderBy('m.datetaken', 'DESC'); + $query->addOrderBy('m.fileid', 'DESC'); // tie-breaker + + // Apply all transformations + $this->applyAllTransforms($queryTransforms, $query, $uid); + + $cursor = $this->executeQueryWithCTEs($query); + $rows = $cursor->fetchAll(); + $cursor->closeCursor(); + + return $this->processDay($rows, $uid, $root); + } + /** * Process the days response. * @@ -259,7 +408,7 @@ trait TimelineQueryDays $actualPath[1] = $actualPath[2]; $actualPath[2] = $tmp; $davPath = implode('/', $actualPath); - $davPaths[$fileid] = Exif::removeExtraSlash('/'.$davPath.'/'); + $davPaths[$fileid] = Exif::removeExtraSlash('/' . $davPath . '/'); } } } @@ -292,7 +441,7 @@ trait TimelineQueryDays if (0 === strpos($row['path'], $basePath)) { $rpath = substr($row['path'], \strlen($basePath)); - $row['filename'] = Exif::removeExtraSlash($davPath.$rpath); + $row['filename'] = Exif::removeExtraSlash($davPath . $rpath); } unset($row['path']); @@ -322,7 +471,7 @@ trait TimelineQueryDays // Add WITH clause if needed if (false !== strpos($sql, 'cte_folders')) { - $sql = $CTE_SQL.' '.$sql; + $sql = $CTE_SQL . ' ' . $sql; } return $this->connection->executeQuery($sql, $params, $types); @@ -335,7 +484,8 @@ trait TimelineQueryDays IQueryBuilder &$query, TimelineRoot &$root, bool $archive - ) { + ) + { // Add query parameters $query->setParameter('topFolderIds', $root->getIds(), IQueryBuilder::PARAM_INT_ARRAY); $query->setParameter('cteFoldersArchive', $archive, IQueryBuilder::PARAM_BOOL); @@ -354,7 +504,8 @@ trait TimelineQueryDays TimelineRoot &$root, bool $recursive, bool $archive - ) { + ) + { // Join with memories $baseOp = $query->expr()->eq('f.fileid', 'm.fileid'); if ($root->isEmpty()) { @@ -372,9 +523,13 @@ trait TimelineQueryDays $pathOp = $query->expr()->eq('f.parent', $query->createNamedParameter($root->getOneId(), IQueryBuilder::PARAM_INT)); } - return $query->innerJoin('m', 'filecache', 'f', $query->expr()->andX( - $baseOp, - $pathOp, - )); + return $query->innerJoin( + 'm', + 'filecache', + 'f', $query->expr()->andX( + $baseOp, + $pathOp, + ) + ); } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 15a62c01..f52a1777 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "camelcase": "^7.0.1", "filerobot-image-editor": "^4.3.7", "justified-layout": "^4.1.0", + "leaflet": "^1.9.3", "moment": "^2.29.4", "path-posix": "^1.0.0", "photoswipe": "^5.3.4", @@ -27,6 +28,8 @@ "vue-material-design-icons": "^5.1.2", "vue-router": "^3.6.5", "vue-virtual-scroller": "1.1.2", + "vue2-leaflet": "^2.7.1", + "vue2-leaflet-markercluster": "^3.1.0", "webdav": "^4.11.2" }, "devDependencies": { @@ -2297,6 +2300,12 @@ "@types/range-parser": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.10", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==", + "peer": true + }, "node_modules/@types/http-proxy": { "version": "1.17.9", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", @@ -2319,6 +2328,15 @@ "dev": true, "peer": true }, + "node_modules/@types/leaflet": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.0.tgz", + "integrity": "sha512-7LeOSj7EloC5UcyOMo+1kc3S1UT3MjJxwqsMT1d2PTyvQz53w0Y0oSSk9nwZnOZubCmBvpSNGceucxiq+ZPEUw==", + "peer": true, + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -6166,9 +6184,9 @@ "peer": true }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -6253,6 +6271,19 @@ "resolved": "https://registry.npmjs.org/layerr/-/layerr-0.1.2.tgz", "integrity": "sha512-ob5kTd9H3S4GOG2nVXyQhOu9O8nBgP555XxWPkJI0tR0JeRilfyTp8WtPdIJHLXBmHMSdEq5+KMxiYABeScsIQ==" }, + "node_modules/leaflet": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz", + "integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==" + }, + "node_modules/leaflet.markercluster": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", + "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", + "peerDependencies": { + "leaflet": "^1.3.1" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -6287,9 +6318,9 @@ } }, "node_modules/loader-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz", - "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "peer": true, "dependencies": { @@ -9518,9 +9549,9 @@ } }, "node_modules/vue-loader/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "peer": true, "dependencies": { @@ -9531,9 +9562,9 @@ } }, "node_modules/vue-loader/node_modules/loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", "dev": true, "peer": true, "dependencies": { @@ -9600,9 +9631,9 @@ } }, "node_modules/vue-style-loader/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "peer": true, "dependencies": { @@ -9613,9 +9644,9 @@ } }, "node_modules/vue-style-loader/node_modules/loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", "dev": true, "peer": true, "dependencies": { @@ -9677,6 +9708,27 @@ "vue": "^2.5.0" } }, + "node_modules/vue2-leaflet": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/vue2-leaflet/-/vue2-leaflet-2.7.1.tgz", + "integrity": "sha512-K7HOlzRhjt3Z7+IvTqEavIBRbmCwSZSCVUlz9u4Rc+3xGCLsHKz4TAL4diAmfHElCQdPPVdZdJk8wPUt2fu6WQ==", + "peerDependencies": { + "@types/leaflet": "^1.5.7", + "leaflet": "^1.3.4", + "vue": "^2.5.17" + } + }, + "node_modules/vue2-leaflet-markercluster": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vue2-leaflet-markercluster/-/vue2-leaflet-markercluster-3.1.0.tgz", + "integrity": "sha512-v94tns6/PmBlFaPY2XvCUL69yUwRO43qYAP7T9hLmPDdu3Vgl15SDcC1qara8M56aNzWMPAcfP/uy4toUE5wcA==", + "dependencies": { + "leaflet.markercluster": "^1.4.1" + }, + "peerDependencies": { + "leaflet": "^1.3.4" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -12124,6 +12176,12 @@ "@types/range-parser": "*" } }, + "@types/geojson": { + "version": "7946.0.10", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==", + "peer": true + }, "@types/http-proxy": { "version": "1.17.9", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", @@ -12146,6 +12204,15 @@ "dev": true, "peer": true }, + "@types/leaflet": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.0.tgz", + "integrity": "sha512-7LeOSj7EloC5UcyOMo+1kc3S1UT3MjJxwqsMT1d2PTyvQz53w0Y0oSSk9nwZnOZubCmBvpSNGceucxiq+ZPEUw==", + "peer": true, + "requires": { + "@types/geojson": "*" + } + }, "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -15203,9 +15270,9 @@ "peer": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "jsonfile": { @@ -15259,6 +15326,17 @@ "resolved": "https://registry.npmjs.org/layerr/-/layerr-0.1.2.tgz", "integrity": "sha512-ob5kTd9H3S4GOG2nVXyQhOu9O8nBgP555XxWPkJI0tR0JeRilfyTp8WtPdIJHLXBmHMSdEq5+KMxiYABeScsIQ==" }, + "leaflet": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz", + "integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==" + }, + "leaflet.markercluster": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", + "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", + "requires": {} + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -15285,9 +15363,9 @@ "peer": true }, "loader-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz", - "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "peer": true, "requires": { @@ -17760,9 +17838,9 @@ }, "dependencies": { "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "peer": true, "requires": { @@ -17770,9 +17848,9 @@ } }, "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", "dev": true, "peer": true, "requires": { @@ -17829,9 +17907,9 @@ }, "dependencies": { "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "peer": true, "requires": { @@ -17839,9 +17917,9 @@ } }, "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", "dev": true, "peer": true, "requires": { @@ -17896,6 +17974,20 @@ "date-format-parse": "^0.2.7" } }, + "vue2-leaflet": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/vue2-leaflet/-/vue2-leaflet-2.7.1.tgz", + "integrity": "sha512-K7HOlzRhjt3Z7+IvTqEavIBRbmCwSZSCVUlz9u4Rc+3xGCLsHKz4TAL4diAmfHElCQdPPVdZdJk8wPUt2fu6WQ==", + "requires": {} + }, + "vue2-leaflet-markercluster": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vue2-leaflet-markercluster/-/vue2-leaflet-markercluster-3.1.0.tgz", + "integrity": "sha512-v94tns6/PmBlFaPY2XvCUL69yUwRO43qYAP7T9hLmPDdu3Vgl15SDcC1qara8M56aNzWMPAcfP/uy4toUE5wcA==", + "requires": { + "leaflet.markercluster": "^1.4.1" + } + }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index b512a87f..31705526 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "camelcase": "^7.0.1", "filerobot-image-editor": "^4.3.7", "justified-layout": "^4.1.0", + "leaflet": "^1.9.3", "moment": "^2.29.4", "path-posix": "^1.0.0", "photoswipe": "^5.3.4", @@ -47,6 +48,8 @@ "vue-material-design-icons": "^5.1.2", "vue-router": "^3.6.5", "vue-virtual-scroller": "1.1.2", + "vue2-leaflet": "^2.7.1", + "vue2-leaflet-markercluster": "^3.1.0", "webdav": "^4.11.2" }, "browserslist": [ diff --git a/src/App.vue b/src/App.vue index 519d2ae2..0d6f5105 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,23 +1,13 @@