diff --git a/appinfo/application.php b/appinfo/application.php
index bf55beee0..d129f72fc 100644
--- a/appinfo/application.php
+++ b/appinfo/application.php
@@ -15,6 +15,8 @@
use OC\AppFramework\Utility\SimpleContainer;
use \OCP\AppFramework\App;
use OCA\Maps\Controller\PageController;
+use OCA\Maps\Hook\FileHooks;
+use OCA\Maps\Service\PhotofilesService;
class Application extends App {
@@ -22,5 +24,17 @@ public function __construct (array $urlParams=array()) {
parent::__construct('maps', $urlParams);
$container = $this->getContainer();
+
+ $this->getContainer()->registerService('FileHooks', function($c) {
+ return new FileHooks(
+ $c->query('ServerContainer')->getRootFolder(),
+ \OC::$server->query(PhotofilesService::class),
+ $c->query('ServerContainer')->getLogger(),
+ $c->query('AppName')
+ );
+ });
+
+ $this->getContainer()->query('FileHooks')->register();
}
+
}
diff --git a/appinfo/database.xml b/appinfo/database.xml
index 0a092fcb3..a475c2444 100644
--- a/appinfo/database.xml
+++ b/appinfo/database.xml
@@ -65,4 +65,39 @@
+
+ *dbprefix*maps_photos
+
+
+ id
+ integer
+ 0
+ true
+ 1
+ 41
+
+
+ user_id
+ text
+ true
+ 64
+
+
+ file_id
+ integer
+ true
+ 10
+
+
+ lat
+ float
+ true
+
+
+ lng
+ float
+ true
+
+
+
diff --git a/appinfo/info.xml b/appinfo/info.xml
index a44a326ae..8716bb05b 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -20,4 +20,10 @@
maps.page.index
+
+
+
+
+ OCA\Maps\Command\RescanPhotos
+
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 47100e439..5464eeffa 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -10,6 +10,9 @@
return [
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
- ['name' => 'page#do_echo', 'url' => '/echo', 'verb' => 'POST'],
+ ['name' => 'page#do_echo', 'url' => '/echo', 'verb' => 'POST'],
+
+ //photos
+ ['name' => 'photos#getPhotosFromDb', 'url' => '/photos', 'verb' => 'GET'],
]
];
diff --git a/css/style.css b/css/style.css
index a5a2c4943..a88fdbf49 100644
--- a/css/style.css
+++ b/css/style.css
@@ -105,3 +105,48 @@ tr.selected td {
.leaflet-popup-content-wrapper {
border-radius: 3px !important;
}
+
+.leaflet-marker-photo {
+ border: 2px solid #fff;
+ box-shadow: 3px 3px 10px #888;
+}
+
+.leaflet-marker-photo.photo-marker{
+ top: -10px;
+}
+
+.leaflet-marker-photo.photo-marker:after {
+ content:"";
+ position: relative;
+ bottom: 16px;
+ border-width: 10px 10px 0;
+ border-style: solid;
+ border-color: #fff transparent;
+ display: block;
+ width: 0;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.leaflet-marker-photo .thumbnail {
+ width: 100%;
+ height: 100%;
+ background-size: cover;
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-color: white;
+}
+
+.leaflet-marker-photo .label {
+ position: absolute;
+ top: -7px;
+ right: -11px;
+ color: #fff;
+ background-color: #333;
+ border-radius: 9px;
+ height: 18px;
+ min-width: 18px;
+ line-height: 12px;
+ text-align: center;
+ padding: 3px;
+}
\ No newline at end of file
diff --git a/js/photosController.js b/js/photosController.js
new file mode 100644
index 000000000..1fd08efd7
--- /dev/null
+++ b/js/photosController.js
@@ -0,0 +1,155 @@
+function PhotosController () {
+ this.PHOTO_MARKER_VIEW_SIZE = 40;
+ this.photosDataLoaded = false;
+ this.photosRequestInProgress = false;
+}
+
+PhotosController.prototype = {
+
+ appendToMap : function(map) {
+ this.map = map;
+ this.photoLayer = L.markerClusterGroup({
+ iconCreateFunction : this.getClusterIconCreateFunction(),
+ showCoverageOnHover : false,
+ maxClusterRadius: this.PHOTO_MARKER_VIEW_SIZE + 10,
+ icon: {
+ iconSize: [this.PHOTO_MARKER_VIEW_SIZE, this.PHOTO_MARKER_VIEW_SIZE]
+ }
+ });
+ this.photoLayer.on('click', this.getPhotoMarkerOnClickFunction());
+ this.photoLayer.addTo(this.map);
+ },
+
+ showLayer: function() {
+ if (!this.photosDataLoaded && !this.photosRequestInProgress) {
+ this.callForImages();
+ }
+ if (!this.map.hasLayer(this.photoLayer)) {
+ this.map.addLayer(this.photoLayer);
+ }
+ },
+
+ hideLayer: function() {
+ if (this.map.hasLayer(this.photoLayer)) {
+ this.map.removeLayer(this.photoLayer);
+ }
+ },
+
+ getPhotoMarkerOnClickFunction() {
+ var _app = this;
+ return function(evt) {
+ var marker = evt.layer;
+ var content;
+ if (marker.data.hasPreview) {
+ var previewUrl = _app.generatePreviewUrl(marker.data.path);
+ var img = "";
+ //Workaround for https://github.com/Leaflet/Leaflet/issues/5484
+ $(img).on('load', function() {
+ marker.getPopup().update();
+ });
+ content = img;
+ } else {
+ content = marker.data.path;
+ }
+ marker.bindPopup(content, {
+ className: 'leaflet-popup-photo',
+ maxWidth: "auto"
+ }).openPopup();
+ }
+ },
+
+ getClusterIconCreateFunction() {
+ var _app = this;
+ return function(cluster) {
+ var marker = cluster.getAllChildMarkers()[0].data;
+ var iconUrl;
+ if (marker.hasPreview) {
+ iconUrl = _app.generatePreviewUrl(marker.path);
+ } else {
+ iconUrl = _app.getImageIconUrl();
+ }
+ var label = cluster.getChildCount();
+ return new L.DivIcon(L.extend({
+ className: 'leaflet-marker-photo cluster-marker',
+ html: '' + label + ''
+ }, this.icon));
+ }
+ },
+
+ createPhotoView: function(markerData) {
+ var iconUrl;
+ if (markerData.hasPreview) {
+ iconUrl = this.generatePreviewUrl(markerData.path);
+ } else {
+ iconUrl = this.getImageIconUrl();
+ }
+ this.generatePreviewUrl(markerData.path);
+ return L.divIcon(L.extend({
+ html: '',
+ className: 'leaflet-marker-photo photo-marker'
+ }, markerData, {
+ iconSize: [this.PHOTO_MARKER_VIEW_SIZE, this.PHOTO_MARKER_VIEW_SIZE],
+ iconAnchor: [this.PHOTO_MARKER_VIEW_SIZE / 2, this.PHOTO_MARKER_VIEW_SIZE]
+ }));
+ },
+
+ addPhotosToMap : function(photos) {
+ var markers = this.preparePhotoMarkers(photos);
+ this.photoLayer.addLayers(markers);
+ },
+
+ preparePhotoMarkers : function(photos) {
+ var markers = [];
+ for (var i = 0; i < photos.length; i++) {
+ var markerData = {
+ lat: photos[i].lat,
+ lng: photos[i].lng,
+ path: photos[i].path,
+ albumId: photos[i].folderId,
+ hasPreview : photos[i].hasPreview
+ };
+ var marker = L.marker(markerData, {
+ icon: this.createPhotoView(markerData)
+ });
+ marker.data = markerData;
+ markers.push(marker);
+ }
+ return markers;
+ },
+
+ callForImages: function() {
+ this.photosRequestInProgress = true;
+ $.ajax({
+ 'url' : OC.generateUrl('apps/maps/photos'),
+ 'type': 'GET',
+ 'context' : this,
+ 'success': function(response) {
+ if (response.length == 0) {
+ //showNoPhotosMessage();
+ } else {
+ this.addPhotosToMap(response);
+ }
+ this.photosDataLoaded = true;
+ },
+ 'complete': function(response) {
+ this.photosRequestInProgress = false;
+ }
+ });
+ },
+
+ /* Preview size 32x32 is used in files view, so it sould be generated */
+ generateThumbnailUrl: function (filename) {
+ return OC.generateUrl('core') + '/preview.png?file=' + encodeURI(filename) + '&x=32&y=32';
+ },
+
+ /* Preview size 375x211 is used in files details view */
+ generatePreviewUrl: function (filename) {
+ return OC.generateUrl('core') + '/preview.png?file=' + encodeURI(filename) + '&x=375&y=211&a=1';
+ },
+
+ getImageIconUrl: function() {
+ return OC.generateUrl('/apps/theming/img/core/filetypes') + '/image.svg?v=2';
+ }
+
+};
+
diff --git a/js/script.js b/js/script.js
index 16458f574..6e07ef40b 100644
--- a/js/script.js
+++ b/js/script.js
@@ -1,6 +1,8 @@
(function($, OC) {
$(function() {
mapController.initMap();
+ photosController.appendToMap(mapController.map);
+ photosController.showLayer();
// Popup
$(document).on('click', '#opening-hours-header', function() {
@@ -65,6 +67,8 @@
}
};
+ var photosController = new PhotosController();
+
var searchController = {
isGeocodeabe: function(str) {
var pattern = /^\s*\d+\.?\d*\,\s*\d+\.?\d*\s*$/;
diff --git a/lib/Command/RescanPhotos.php b/lib/Command/RescanPhotos.php
new file mode 100644
index 000000000..aa54d15dc
--- /dev/null
+++ b/lib/Command/RescanPhotos.php
@@ -0,0 +1,77 @@
+
+ * @copyright Piotr Bator 2017
+ */
+
+namespace OCA\Maps\Command;
+
+use OCP\Encryption\IManager;
+use OCP\Files\NotFoundException;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+use OCA\Maps\Service\PhotofilesService;
+
+class RescanPhotos extends Command {
+
+ protected $userManager;
+
+ protected $output;
+
+ protected $encryptionManager;
+
+ private $photofilesService;
+
+ public function __construct(IUserManager $userManager,
+ IManager $encryptionManager,
+ PhotofilesService $photofilesService) {
+ parent::__construct();
+ $this->userManager = $userManager;
+ $this->encryptionManager = $encryptionManager;
+ $this->photofilesService = $photofilesService;
+ }
+ protected function configure() {
+ $this->setName('maps:scan-photos')
+ ->setDescription('Rescan photos GPS exif data')
+ ->addArgument(
+ 'user_id',
+ InputArgument::OPTIONAL,
+ 'Rescan photos GPS exif data for the given user'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ if ($this->encryptionManager->isEnabled()) {
+ $output->writeln('Encryption is enabled. Aborted.');
+ return 1;
+ }
+ $this->output = $output;
+ $userId = $input->getArgument('user_id');
+ if ($userId === null) {
+ $this->userManager->callForSeenUsers(function (IUser $user) {
+ $this->rescanUserPhotos($user->getUID());
+ });
+ } else {
+ $user = $this->userManager->get($userId);
+ if ($user !== null) {
+ $this->rescanUserPhotos($userId);
+ }
+ }
+ return 0;
+ }
+
+ private function rescanUserPhotos($userId) {
+ $this->photofilesService->rescan($userId);
+ }
+}
\ No newline at end of file
diff --git a/lib/Controller/PhotosController.php b/lib/Controller/PhotosController.php
new file mode 100644
index 000000000..878b26b4f
--- /dev/null
+++ b/lib/Controller/PhotosController.php
@@ -0,0 +1,44 @@
+
+ * @copyright Piotr Bator 2017
+ */
+
+namespace OCA\Maps\Controller;
+
+use OCP\IRequest;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\Controller;
+use OCP\ILogger;
+
+use OCA\Maps\Service\GeophotoService;
+
+class PhotosController extends Controller {
+ private $userId;
+ private $geophotoService;
+ private $logger;
+
+ public function __construct($AppName, ILogger $logger, IRequest $request, GeophotoService $GeophotoService, $UserId){
+ parent::__construct($AppName, $request);
+ $this->logger = $logger;
+ $this->userId = $UserId;
+ $this->geophotoService = $GeophotoService;
+ }
+
+ /**
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ */
+ public function getPhotosFromDb() {
+ $result = $this->geophotoService->getAllFromDB($this->userId);
+ return new DataResponse($result);
+ }
+
+}
diff --git a/lib/DB/Geophoto.php b/lib/DB/Geophoto.php
new file mode 100644
index 000000000..993fa54a2
--- /dev/null
+++ b/lib/DB/Geophoto.php
@@ -0,0 +1,29 @@
+
+ * @copyright Piotr Bator 2017
+ */
+
+namespace OCA\Maps\DB;
+
+use OCP\AppFramework\Db\Entity;
+
+class Geophoto extends Entity {
+
+ protected $fileId;
+ protected $lat;
+ protected $lng;
+ protected $userId;
+
+ public function __construct() {
+ $this->addType('fileId', 'integer');
+ $this->addType('lat', 'float');
+ $this->addType('lng', 'float');
+ }
+}
\ No newline at end of file
diff --git a/lib/DB/GeophotoMapper.php b/lib/DB/GeophotoMapper.php
new file mode 100644
index 000000000..d17e6e9c1
--- /dev/null
+++ b/lib/DB/GeophotoMapper.php
@@ -0,0 +1,46 @@
+
+ * @copyright Piotr Bator 2017
+ */
+
+ namespace OCA\Maps\DB;
+
+use OCP\IDBConnection;
+use OCP\AppFramework\Db\Mapper;
+
+class GeophotoMapper extends Mapper {
+
+ public function __construct(IDBConnection $db) {
+ parent::__construct($db, 'maps_photos');
+ }
+
+ public function find($id) {
+ $sql = 'SELECT * FROM `*PREFIX*maps_photos` ' .
+ 'WHERE `id` = ?';
+ return $this->findEntity($sql, [$id]);
+ }
+
+
+ public function findAll($userId, $limit=null, $offset=null) {
+ $sql = 'SELECT * FROM `*PREFIX*maps_photos` where `user_id` = ?';
+ return $this->findEntities($sql, [$userId], $limit, $offset);
+ }
+
+ public function deleteByFileId($fileId) {
+ $sql = 'DELETE FROM `*PREFIX*maps_photos` where `file_id` = ?';
+ return $this->execute($sql, [$fileId]);
+ }
+
+ public function deleteAll($userId) {
+ $sql = 'DELETE FROM `*PREFIX*maps_photos` where `user_id` = ?';
+ return $this->execute($sql, [$userId]);
+ }
+
+}
diff --git a/lib/Hook/FileHooks.php b/lib/Hook/FileHooks.php
new file mode 100644
index 000000000..e5ef124d7
--- /dev/null
+++ b/lib/Hook/FileHooks.php
@@ -0,0 +1,89 @@
+
+ * @copyright Piotr Bator 2017
+ */
+
+namespace OCA\Maps\Hook;
+
+use OC\Files\Filesystem;
+use OC\Files\View;
+use OCP\Files\FileInfo;
+use OCP\ILogger;
+use OCP\Files\Node;
+use OCP\Files\IRootFolder;
+use OCP\Util;
+
+use OCA\Maps\Service\PhotofilesService;
+
+/**
+ * Handles files events
+ */
+class FileHooks {
+
+ private $photofilesService;
+
+ private $logger;
+
+ private $root;
+
+ public function __construct(IRootFolder $root, PhotofilesService $photofilesService, ILogger $logger, $appName) {
+ $this->photofilesService = $photofilesService;
+ $this->logger = $logger;
+ $this->root = $root;
+ }
+
+ public function register() {
+ $fileWriteCallback = function(\OCP\Files\Node $node) {
+ if($this->isUserNode($node)) {
+ $this->photofilesService->addByFile($node);
+ }
+ };
+ $this->root->listen('\OC\Files', 'postWrite', $fileWriteCallback);
+
+ $fileDeletionCallback = function(\OCP\Files\Node $node) {
+ if($this->isUserNode($node)) {
+ if ($node->getType() === FileInfo::TYPE_FOLDER) {
+ $this->photofilesService->deleteByFolder($node);
+ } else {
+ $this->photofilesService->deleteByFile($node);
+ }
+ }
+ };
+ $this->root->listen('\OC\Files', 'preDelete', $fileDeletionCallback);
+
+ Util::connectHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', $this, 'restore');
+ }
+
+ public static function restore($params) {
+ $node = $this->getNodeForPath($params['filePath']);
+ if($this->isUserNode($node)) {
+ if ($node->getType() === FileInfo::TYPE_FOLDER) {
+ $this->photofilesService->addByFolder($node);
+ } else {
+ $this->photofilesService->addByFile($node);
+ }
+ }
+ }
+
+ private function getNodeForPath($path) {
+ $user = \OC::$server->getUserSession()->getUser();
+ $fullPath = Filesystem::normalizePath('/' . $user->getUID() . '/files/' . $path);
+ return $this->root->get($fullPath);
+ }
+
+ /**
+ * Ugly Hack, find API way to check if file is added by user.
+ */
+ private function isUserNode(\OCP\Files\Node $node) {
+ //return strpos($node->getStorage()->getId(), "home::", 0) === 0;
+ return $node->getStorage()->instanceOfStorage('\OC\Files\Storage\Home');
+ }
+
+}
\ No newline at end of file
diff --git a/lib/Service/GeophotoService.php b/lib/Service/GeophotoService.php
new file mode 100644
index 000000000..a8b031781
--- /dev/null
+++ b/lib/Service/GeophotoService.php
@@ -0,0 +1,98 @@
+
+ * @copyright Piotr Bator 2017
+ */
+
+namespace OCA\Maps\Service;
+
+use OCP\Files\FileInfo;
+use OCP\IL10N;
+use OCP\Files\IRootFolder;
+use OCP\Files\Storage\IStorage;
+use OCP\Files\Folder;
+use OCP\IPreview;
+use OCP\ILogger;
+
+use OCA\Maps\Service\PhotofilesService;
+use OCA\Maps\DB\Geophoto;
+use OCA\Maps\DB\GeophotoMapper;
+
+class GeophotoService {
+
+ private $l10n;
+ private $root;
+ private $photoMapper;
+ private $logger;
+ private $preview;
+
+ public function __construct (ILogger $logger, IRootFolder $root, IL10N $l10n, GeophotoMapper $photoMapper, IPreview $preview) {
+ $this->root = $root;
+ $this->l10n = $l10n;
+ $this->photoMapper = $photoMapper;
+ $this->logger = $logger;
+ $this->preview = $preview;
+ }
+
+ /**
+ * @param string $userId
+ * @return array with geodatas of all photos
+ */
+ public function getAllFromDB ($userId) {
+ $photoEntities = $this->photoMapper->findAll($userId);
+ $userFolder = $this->getFolderForUser($userId);
+ $filesById = [];
+ $cache = $userFolder->getStorage()->getCache();
+ $previewEnableMimetypes = $this->getPreviewEnabledMimetypes();
+ foreach ($photoEntities as $photoEntity) {
+ $cacheEntry = $cache->get($photoEntity->getFileId());
+ $path = $cacheEntry->getPath();
+ $file_object = new \stdClass();
+ $file_object->fileId = $photoEntity->getFileId();
+ $file_object->lat = $photoEntity->getLat();
+ $file_object->lng = $photoEntity->getLng();
+ /* 30% longer
+ * $file_object->folderId = $cache->getParentId($path);
+ */
+ $file_object->path = $this->normalizePath($path);
+ $file_object->hasPreview = in_array($cacheEntry->getMimeType(), $previewEnableMimetypes);
+ $filesById[] = $file_object;
+ }
+ return $filesById;
+ }
+
+ private function getPreviewEnabledMimetypes() {
+ $enabledMimeTypes = [];
+ foreach (PhotofilesService::PHOTO_MIME_TYPES as $mimeType) {
+ if ($this->preview->isMimeSupported($mimeType)) {
+ $enabledMimeTypes[] = $mimeType;
+ }
+ }
+ return $enabledMimeTypes;
+ }
+
+ private function normalizePath($path) {
+ return str_replace("files","", $path);
+ }
+
+ /**
+ * @param string $userId the user id
+ * @return Folder
+ */
+ private function getFolderForUser ($userId) {
+ $path = '/' . $userId . '/files';
+ if ($this->root->nodeExists($path)) {
+ $folder = $this->root->get($path);
+ } else {
+ $folder = $this->root->newFolder($path);
+ }
+ return $folder;
+ }
+
+}
diff --git a/lib/Service/PhotofilesService.php b/lib/Service/PhotofilesService.php
new file mode 100644
index 000000000..d77869daa
--- /dev/null
+++ b/lib/Service/PhotofilesService.php
@@ -0,0 +1,189 @@
+
+ * @copyright Piotr Bator 2017
+ */
+
+namespace OCA\Maps\Service;
+
+use OCP\Files\FileInfo;
+use OCP\IL10N;
+use OCP\Files\IRootFolder;
+use OCP\Files\Storage\IStorage;
+use OCP\Files\Folder;
+use OCP\Files\Node;
+use OCP\ILogger;
+
+use OCA\Maps\DB\Geophoto;
+use OCA\Maps\DB\GeophotoMapper;
+
+class PhotofilesService {
+
+ const PHOTO_MIME_TYPES = ['image/jpeg', 'image/tiff'];
+
+ private $l10n;
+ private $root;
+ private $photoMapper;
+ private $logger;
+
+ public function __construct (ILogger $logger, IRootFolder $root, IL10N $l10n, GeophotoMapper $photoMapper) {
+ $this->root = $root;
+ $this->l10n = $l10n;
+ $this->photoMapper = $photoMapper;
+ $this->logger = $logger;
+ }
+
+ public function rescan ($userId){
+ $userFolder = $this->root->getUserFolder($userId);
+ $photos = $this->gatherPhotoFiles($userFolder, true);
+ $this->photoMapper->deleteAll($userId);
+ foreach($photos as $photo) {
+ $this->addPhoto($photo, $userId);
+ }
+ }
+
+ public function addByFile(Node $file) {
+ $userFolder = $this->root->getUserFolder($file->getOwner()->getUID());
+ if($this->isPhoto($file)) {
+ $this->addPhoto($file, $file->getOwner()->getUID());
+ }
+ }
+
+ public function addByFolder(Node $folder) {
+ $photos = $this->gatherPhotoFiles($folder, true);
+ foreach($photos as $photo) {
+ $this->addPhoto($photo, $folder->getOwner()->getUID());
+ }
+ }
+
+ public function deleteByFile(Node $file) {
+ $this->photoMapper->deleteByFileId($file->getId());
+ }
+
+ public function deleteByFolder(Node $folder) {
+ $photos = $this->gatherPhotoFiles($folder, true);
+ foreach($photos as $photo) {
+ $this->photoMapper->deleteByFileId($photo->getId());
+ }
+ }
+
+ private function addPhoto($photo, $userId) {
+ $exif = $this->getExif($photo);
+ if (!is_null($exif) AND !is_null($exif->lat)) {
+ $photoEntity = new Geophoto();
+ $photoEntity->setFileId($photo->getId());
+ $photoEntity->setLat($exif->lat);
+ $photoEntity->setLng($exif->lng);
+ $photoEntity->setUserId($userId);
+ $this->photoMapper->insert($photoEntity);
+ }
+ }
+
+ private function normalizePath($node) {
+ return str_replace("files","", $node->getInternalPath());
+ }
+
+ public function getPhotosByFolder($userId, $path) {
+ $userFolder = $this->root->getUserFolder($userId);
+ $folder = $userFolder->get($path);
+ return $this->getPhotosListForFolder($folder);
+ }
+
+ private function getPhotosListForFolder($folder) {
+ $FilesList = $this->gatherPhotoFiles($folder, false);
+ $notes = [];
+ foreach($FilesList as $File) {
+ $file_object = new \stdClass();
+ $file_object->fileId = $File->getId();
+ $file_object->path = $this->normalizePath($File);
+ $notes[] = $file_object;
+ }
+ return $notes;
+ }
+
+ private function gatherPhotoFiles ($folder, $recursive) {
+ $notes = [];
+ $nodes = $folder->getDirectoryListing();
+ foreach($nodes as $node) {
+ if($node->getType() === FileInfo::TYPE_FOLDER AND $recursive) {
+ $notes = array_merge($notes, $this->gatherPhotoFiles($node, $recursive));
+ continue;
+ }
+ if($this->isPhoto($node)) {
+ $notes[] = $node;
+ }
+ }
+ return $notes;
+ }
+
+ private function isPhoto($file) {
+ if($file->getType() !== \OCP\Files\FileInfo::TYPE_FILE) return false;
+ if(!in_array($file->getMimetype(), self::PHOTO_MIME_TYPES)) return false;
+ return true;
+ }
+
+ private function hasValidExifGeoTags($exif) {
+ if (!isset($exif["GPSLatitude"]) OR !isset($exif["GPSLongitude"])) {
+ return false;
+ }
+ if (count($exif["GPSLatitude"]) != 3 OR count($exif["GPSLongitude"]) != 3) {
+ return false;
+ }
+ //Check photos are on the earth
+ if ($exif["GPSLatitude"][0]>=90 OR $exif["GPSLongitude"][0]>=180) {
+ return false;
+ }
+ //Check photos are not on NULL island, remove if they should be.
+ if($exif["GPSLatitude"][0]==0 AND $exif["GPSLatitude"][1]==0 AND $exif["GPSLongitude"][0]==0 AND $exif["GPSLongitude"][1]==0){
+ return false;
+ }
+ return true;
+ }
+
+ private function getExif($file) {
+ $path = $file->getStorage()->getLocalFile($file->getInternalPath());
+ $exif = @exif_read_data($path);
+ if($this->hasValidExifGeoTags($exif)){
+ //Check if there is exif infor
+ $LatM = 1; $LongM = 1;
+ if($exif["GPSLatitudeRef"] == 'S'){
+ $LatM = -1;
+ }
+ if($exif["GPSLongitudeRef"] == 'W'){
+ $LongM = -1;
+ }
+ //get the GPS data
+ $gps['LatDegree']=$exif["GPSLatitude"][0];
+ $gps['LatMinute']=$exif["GPSLatitude"][1];
+ $gps['LatgSeconds']=$exif["GPSLatitude"][2];
+ $gps['LongDegree']=$exif["GPSLongitude"][0];
+ $gps['LongMinute']=$exif["GPSLongitude"][1];
+ $gps['LongSeconds']=$exif["GPSLongitude"][2];
+
+ //convert strings to numbers
+ foreach($gps as $key => $value){
+ $pos = strpos($value, '/');
+ if($pos !== false){
+ $temp = explode('/',$value);
+ $gps[$key] = $temp[0] / $temp[1];
+ }
+ }
+ $file_object = new \stdClass();
+ //calculate the decimal degree
+ $file_object->lat = $LatM * ($gps['LatDegree'] + ($gps['LatMinute'] / 60) + ($gps['LatgSeconds'] / 3600));
+ $file_object->lng = $LongM * ($gps['LongDegree'] + ($gps['LongMinute'] / 60) + ($gps['LongSeconds'] / 3600));
+ if (isset($exif["DateTimeOriginal"])) {
+ $file_object->dateTaken = strtotime($exif["DateTimeOriginal"]);
+ }
+ return $file_object;
+ }
+ return null;
+ }
+
+}
diff --git a/package.json b/package.json
index 8c78a2fa5..15c6e9a5f 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"gulp": "^3.9.1",
"gulp-cli": "^1.4.0",
"leaflet": "^1.2.0",
+ "leaflet.markercluster": "^1.1.0",
"opening_hours": "^3.5.0"
}
}
diff --git a/templates/content/index.php b/templates/content/index.php
index 9a436e2ea..32ab20555 100644
--- a/templates/content/index.php
+++ b/templates/content/index.php
@@ -20,6 +20,7 @@
style('maps', '../node_modules/leaflet/dist/leaflet');
script('maps', '../node_modules/leaflet/dist/leaflet');
+script('maps', '../node_modules/leaflet.markercluster/dist/leaflet.markercluster');
script('maps', '../node_modules/opening_hours/opening_hours');
?>
diff --git a/templates/index.php b/templates/index.php
index 128c7a28b..d7418f7c5 100644
--- a/templates/index.php
+++ b/templates/index.php
@@ -1,4 +1,5 @@