diff --git a/appinfo/application.php b/appinfo/application.php
index 0b2daa102..e30ca99f3 100644
--- a/appinfo/application.php
+++ b/appinfo/application.php
@@ -19,9 +19,11 @@
use OCA\Maps\Controller\FavoritesController;
use OCA\Maps\Controller\FavoritesApiController;
use OCA\Maps\Controller\RoutingController;
+use OCA\Maps\Controller\TracksController;
use OCA\Maps\Hook\FileHooks;
use OCA\Maps\Service\PhotofilesService;
use OCA\Maps\Service\FavoritesService;
+use OCA\Maps\Service\TracksService;
class Application extends App {
@@ -103,6 +105,28 @@ public function __construct (array $urlParams=array()) {
}
);
+ $container->registerService(
+ 'TracksController', function ($c) {
+ return new TracksController(
+ $c->query('AppName'),
+ $c->query('Request'),
+ $c->query('UserId'),
+ $c->query('ServerContainer')->getUserFolder($c->query('UserId')),
+ $c->query('ServerContainer')->getConfig(),
+ $c->getServer()->getShareManager(),
+ $c->getServer()->getAppManager(),
+ $c->getServer()->getUserManager(),
+ $c->getServer()->getGroupManager(),
+ $c->query('ServerContainer')->getL10N($c->query('AppName')),
+ $c->query('ServerContainer')->getLogger(),
+ new TracksService(
+ $c->query('ServerContainer')->getLogger(),
+ $c->query('ServerContainer')->getL10N($c->query('AppName'))
+ )
+ );
+ }
+ );
+
$container->registerService(
'UtilsController', function ($c) {
return new UtilsController(
diff --git a/appinfo/database.xml b/appinfo/database.xml
index 32f055bb4..f00de3309 100644
--- a/appinfo/database.xml
+++ b/appinfo/database.xml
@@ -141,5 +141,36 @@
+
+ *dbprefix*maps_tracks
+
+
+ id
+ integer
+ 0
+ true
+ 1
+ 41
+
+
+ user_id
+ text
+ true
+ 64
+
+
+ file_id
+ integer
+ true
+ 10
+
+
+ color
+ text
+ false
+ 7
+
+
+
diff --git a/appinfo/info.xml b/appinfo/info.xml
index f790f387f..fcfb45140 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -7,7 +7,7 @@
agpl
Vinzenz Rosenkranz
- 0.0.4
+ 0.0.5
Maps
multimedia
https://github.com/nextcloud/maps/issues
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 85e9b284d..c905efa44 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -51,5 +51,14 @@
['name' => 'favorites#exportAllFavorites', 'url' => '/export/favorites', 'verb' => 'GET'],
['name' => 'favorites#exportFavorites', 'url' => '/export/favorites', 'verb' => 'POST'],
['name' => 'favorites#importFavorites', 'url' => '/import/favorites', 'verb' => 'POST'],
+
+ // tracks
+ ['name' => 'tracks#getTracks', 'url' => '/tracks', 'verb' => 'GET'],
+ ['name' => 'tracks#getTrackFileContent', 'url' => '/tracks/{id}', 'verb' => 'GET'],
+ ['name' => 'tracks#addTracks', 'url' => '/tracks', 'verb' => 'POST'],
+ ['name' => 'tracks#addTrackDirectory', 'url' => '/tracks-directory', 'verb' => 'POST'],
+ ['name' => 'tracks#editTrack', 'url' => '/tracks/{id}', 'verb' => 'PUT'],
+ ['name' => 'tracks#deleteTrack', 'url' => '/tracks/{id}', 'verb' => 'DELETE'],
+ ['name' => 'tracks#deleteTracks', 'url' => '/tracks', 'verb' => 'DELETE'],
]
];
diff --git a/css/images/marker-icon.svg b/css/images/marker-icon.svg
new file mode 100644
index 000000000..fca9d379a
--- /dev/null
+++ b/css/images/marker-icon.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/css/style.css b/css/style.css
index a3d381abe..c7c139f65 100644
--- a/css/style.css
+++ b/css/style.css
@@ -329,6 +329,14 @@ tr.selected td {
mask: url('images/star-circle.svg') no-repeat 50% 50%;
mask-size: 15px;
}
+.trackWaypoint {
+ height: 25px !important;
+ width: 25px !important;
+ -webkit-mask: url('images/marker-icon.svg') no-repeat 50% 50%;
+ -webkit-mask-size: 25px;
+ mask: url('images/marker-icon.svg') no-repeat 50% 50%;
+ mask-size: 25px;
+}
.line-enabled {
background: var(--color-background-dark);
}
@@ -356,6 +364,8 @@ table.editFavorite input[type=text] {
#togglePhotosButton button,
#toggleContactsButton button,
#toggleFavoritesButton button,
+#toggleTracksButton button,
+.toggleTrackButton button,
.toggleCategoryButton button {
opacity: 1 !important;
}
@@ -363,6 +373,8 @@ table.editFavorite input[type=text] {
#navigation-favorites > a,
#navigation-photos > a,
#navigation-contacts > a,
+#navigation-tracks > a,
+.track-line > a,
.category-line > a {
opacity: 1 !important;
}
@@ -373,7 +385,16 @@ table.editFavorite input[type=text] {
font-weight: 900;
text-decoration: inherit;
position: absolute;
- left: 18px;
+ left: 16px;
+}
+#navigation-tracks > a::before {
+ content: "\f201";
+ font-family: 'Font Awesome\ 5 Free';
+ font-style: normal;
+ font-weight: 900;
+ text-decoration: inherit;
+ position: absolute;
+ left: 16px;
}
.ui-autocomplete {
z-index: 10000 !important;
@@ -415,3 +436,12 @@ table.editFavorite input[type=text] {
.app-navigation-entry-menu {
z-index: 10001;
}
+#colorinput {
+ opacity: 0;
+ width: 0px;
+ height: 0px;
+ min-height: 0px;
+ padding: 0px;
+ margin: 0px;
+ border: 0px;
+}
diff --git a/js/script.js b/js/script.js
index 6848cd145..e7c332ad0 100644
--- a/js/script.js
+++ b/js/script.js
@@ -8,6 +8,8 @@
mapController.map.photosController = photosController;
contactsController.initLayer(mapController.map);
mapController.map.contactsController = contactsController;
+ tracksController.initController(mapController.map);
+ tracksController.getTracks();
// once controllers have been set/initialized, we can restore option values from server
optionsController.restoreOptions();
@@ -67,6 +69,7 @@
var optionsController = {
optionValues: {},
enabledFavoriteCategories: [],
+ enabledTracks: [],
saveOptionValues: function (optionValues) {
var req = {
options: optionValues
@@ -131,6 +134,23 @@
if (optionsValues.hasOwnProperty('routingEnabled') && optionsValues.routingEnabled === 'true') {
routingController.toggleRouting();
}
+ if (!optionsValues.hasOwnProperty('tracksEnabled') || optionsValues.tracksEnabled === 'true') {
+ tracksController.toggleTracks();
+ }
+ if (!optionsValues.hasOwnProperty('trackListShow') || optionsValues.trackListShow === 'true') {
+ tracksController.toggleTrackList();
+ }
+ if (optionsValues.hasOwnProperty('enabledTracks')
+ && optionsValues.enabledTracks
+ && optionsValues.enabledTracks !== '')
+ {
+ that.enabledTracks = optionsValues.enabledTracks.split('|').map(function (x) {
+ return parseInt(x);
+ });
+ if (tracksController.trackListLoaded) {
+ tracksController.restoreTracksState(that.enabledTracks);
+ }
+ }
// save tile layer when changed
// do it after restore, otherwise restoring triggers save
@@ -486,6 +506,7 @@
contactsController.updateTimeFilterEnd(that.valueEnd);
}
favoritesController.updateFilterDisplay();
+ tracksController.updateFilterDisplay();
that.onUpdateCallbackBlock = false;
if (unencoded[0] < that.min || unencoded[1] > that.max || positions[1] - positions[0] < 10) {
@@ -547,11 +568,13 @@
var maxs = [];
var rawMins = [
favoritesController.firstDate,
+ tracksController.firstDate,
photosController.photoMarkersOldest,
contactsController.contactMarkersOldest
];
var rawMaxs = [
favoritesController.lastDate,
+ tracksController.lastDate,
photosController.photoMarkersNewest,
contactsController.contactMarkersNewest
];
@@ -598,6 +621,7 @@
var photosController = new PhotosController(optionsController, timeFilterController);
var contactsController = new ContactsController(optionsController, timeFilterController);
var favoritesController = new FavoritesController(optionsController, timeFilterController);
+ var tracksController = new TracksController(optionsController, timeFilterController);
timeFilterController.connect();
diff --git a/js/tracksController.js b/js/tracksController.js
new file mode 100644
index 000000000..1d1cebf30
--- /dev/null
+++ b/js/tracksController.js
@@ -0,0 +1,802 @@
+function TracksController(optionsController, timeFilterController) {
+ this.optionsController = optionsController;
+ this.timeFilterController = timeFilterController;
+
+ this.mainLayer = null;
+ // indexed by track id
+ // those actually added to map, those which get toggled
+ this.mapTrackLayers = {};
+ // layers which actually contain lines/waypoints, those which get filtered
+ this.trackLayers = {};
+ this.trackColors = {};
+ this.trackDivIcon = {};
+
+ this.firstDate = null;
+ this.lastDate = null;
+
+ // used by optionsController to know if tracks loading
+ // was done before or after option restoration
+ this.trackListLoaded = false;
+}
+
+TracksController.prototype = {
+
+ // set up favorites-related UI stuff
+ initController : function(map) {
+ this.map = map;
+ this.mainLayer = L.featureGroup();
+ var that = this;
+ // UI events
+ // click on menu buttons
+ $('body').on('click', '.tracksMenuButton, .trackMenuButton', function(e) {
+ var wasOpen = $(this).parent().parent().parent().find('>.app-navigation-entry-menu').hasClass('open');
+ $('.app-navigation-entry-menu.open').removeClass('open');
+ if (!wasOpen) {
+ $(this).parent().parent().parent().find('>.app-navigation-entry-menu').addClass('open');
+ }
+ });
+ // click on a track name : zoom to bounds
+ $('body').on('click', '.track-line .track-name', function(e) {
+ var id = $(this).parent().attr('track');
+ that.zoomOnTrack(id);
+ });
+ // toggle a track
+ $('body').on('click', '.toggleTrackButton', function(e) {
+ var id = $(this).parent().parent().parent().attr('track');
+ that.toggleTrack(id, true);
+ });
+ // remove a track
+ $('body').on('click', '.removeTrack', function(e) {
+ var id = parseInt($(this).parent().parent().parent().parent().attr('track'));
+ that.removeTrackDB(id);
+ });
+ // remove all tracks
+ $('body').on('click', '#remove-all-tracks', function(e) {
+ that.removeAllTracksDB();
+ });
+ // show/hide all tracks
+ $('body').on('click', '#select-all-tracks', function(e) {
+ that.showAllTracks();
+ var trackStringList = Object.keys(that.trackLayers).join('|');
+ that.optionsController.saveOptionValues({enabledTracks: trackStringList});
+ that.optionsController.enabledTracks = trackStringList;
+ that.optionsController.saveOptionValues({tracksEnabled: that.map.hasLayer(that.mainLayer)});
+ });
+ $('body').on('click', '#select-no-tracks', function(e) {
+ that.hideAllTracks();
+ var trackStringList = '';
+ that.optionsController.saveOptionValues({enabledTracks: trackStringList});
+ that.optionsController.enabledTracks = trackStringList;
+ that.optionsController.saveOptionValues({tracksEnabled: that.map.hasLayer(that.mainLayer)});
+ });
+ // click on + button
+ $('body').on('click', '#addTrackButton', function(e) {
+ OC.dialogs.filepicker(
+ t('maps', 'Load gpx file'),
+ function(targetPath) {
+ that.addTracksDB(targetPath);
+ },
+ true,
+ 'application/gpx+xml',
+ true
+ );
+ });
+ // click on add directory button
+ $('body').on('click', '#add-track-folder', function(e) {
+ OC.dialogs.filepicker(
+ t('maps', 'Load gpx files from directory'),
+ function(targetPath) {
+ that.addTrackDirectoryDB(targetPath || '/');
+ },
+ false,
+ 'httpd/unix-directory',
+ true
+ );
+ });
+ // toggle tracks
+ $('body').on('click', '#toggleTracksButton', function(e) {
+ that.toggleTracks();
+ that.optionsController.saveOptionValues({tracksEnabled: that.map.hasLayer(that.mainLayer)});
+ that.updateMyFirstLastDates();
+ });
+ // expand track list
+ $('body').on('click', '#navigation-tracks > a', function(e) {
+ that.toggleTrackList();
+ that.optionsController.saveOptionValues({trackListShow: $('#navigation-tracks').hasClass('open')});
+ });
+ $('body').on('click', '#navigation-tracks', function(e) {
+ if (e.target.tagName === 'LI' && $(e.target).attr('id') === 'navigation-tracks') {
+ that.toggleTrackList();
+ that.optionsController.saveOptionValues({trackListShow: $('#navigation-tracks').hasClass('open')});
+ }
+ });
+ $('body').on('click', '.changeTrackColor', function(e) {
+ var id = $(this).parent().parent().parent().parent().attr('track');
+ that.askChangeTrackColor(id);
+ });
+ $('body').on('change', '#colorinput', function(e) {
+ that.okColor();
+ });
+ },
+
+ // expand or fold categories in sidebar
+ toggleTrackList: function() {
+ $('#navigation-tracks').toggleClass('open');
+ },
+
+ // toggle tracks general layer on map and save state in user options
+ toggleTracks: function() {
+ if (this.map.hasLayer(this.mainLayer)) {
+ this.map.removeLayer(this.mainLayer);
+ // color of the eye
+ $('#toggleTracksButton button').addClass('icon-toggle').attr('style', '');
+ }
+ else {
+ this.map.addLayer(this.mainLayer);
+ // color of the eye
+ var color = OCA.Theming.color.replace('#', '');
+ var imgurl = OC.generateUrl('/svg/core/actions/toggle?color='+color);
+ $('#toggleTracksButton button').removeClass('icon-toggle').css('background-image', 'url('+imgurl+')');
+ }
+ },
+
+ // add/remove markers from layers considering current filter values
+ updateFilterDisplay: function() {
+ var startFilter = this.timeFilterController.valueBegin;
+ var endFilter = this.timeFilterController.valueEnd;
+
+ var id, layer, i, date;
+ for (id in this.trackLayers) {
+ date = this.trackLayers[id].date;
+ // if it was not filtered, check if it should be removed
+ if (this.mapTrackLayers[id].hasLayer(this.trackLayers[id])) {
+ if (date && (date < startFilter || date > endFilter)) {
+ this.mapTrackLayers[id].removeLayer(this.trackLayers[id]);
+ }
+ }
+ // if it was filtered, check if it should be added
+ else {
+ if (date && (date >= startFilter && date <= endFilter)) {
+ this.mapTrackLayers[id].addLayer(this.trackLayers[id]);
+ }
+ }
+ }
+ },
+
+ updateMyFirstLastDates: function(pageLoad=false) {
+ if (!this.map.hasLayer(this.mainLayer)) {
+ this.firstDate = null;
+ this.lastDate = null;
+ return;
+ }
+
+ var id;
+
+ // we update dates only if nothing is currently loading
+ for (id in this.mapTrackLayers) {
+ if (this.mainLayer.hasLayer(this.mapTrackLayers[id]) && !this.trackLayers[id].loaded) {
+ return;
+ }
+ }
+
+ var initMinDate = Math.floor(Date.now() / 1000) + 1000000
+ var initMaxDate = 0;
+
+ var first = initMinDate;
+ var last = initMaxDate;
+ for (id in this.mapTrackLayers) {
+ if (this.mainLayer.hasLayer(this.mapTrackLayers[id]) && this.trackLayers[id].loaded && this.trackLayers[id].date) {
+ if (this.trackLayers[id].date < first) {
+ first = this.trackLayers[id].date;
+ }
+ if (this.trackLayers[id].date > last) {
+ last = this.trackLayers[id].date;
+ }
+ }
+ }
+ if (first !== initMinDate
+ && last !== initMaxDate) {
+ this.firstDate = first;
+ this.lastDate = last;
+ }
+ else {
+ this.firstDate = null;
+ this.lastDate = null;
+ }
+ if (pageLoad) {
+ this.timeFilterController.updateSliderRangeFromController();
+ this.timeFilterController.setSliderToMaxInterval();
+ }
+ },
+
+ saveEnabledTracks: function(additionalIds=[]) {
+ var trackList = [];
+ var layer;
+ for (var id in this.mapTrackLayers) {
+ layer = this.mapTrackLayers[id];
+ if (this.mainLayer.hasLayer(layer)) {
+ trackList.push(id);
+ }
+ }
+ for (var i=0; i < additionalIds.length; i++) {
+ trackList.push(additionalIds[i]);
+ }
+ var trackStringList = trackList.join('|');
+ this.optionsController.saveOptionValues({enabledTracks: trackStringList});
+ // this is used when tracks are loaded again
+ this.optionsController.enabledTracks = trackList;
+ },
+
+ restoreTracksState: function(enabledTrackList) {
+ var id;
+ for (var i=0; i < enabledTrackList.length; i++) {
+ id = enabledTrackList[i];
+ if (this.mapTrackLayers.hasOwnProperty(id)) {
+ this.toggleTrack(id, false, true);
+ }
+ }
+ },
+
+ showAllTracks: function() {
+ if (!this.map.hasLayer(this.mainLayer)) {
+ this.toggleTracks();
+ }
+ for (var id in this.mapTrackLayers) {
+ if (!this.mainLayer.hasLayer(this.mapTrackLayers[id])) {
+ this.toggleTrack(id);
+ }
+ }
+ this.updateMyFirstLastDates();
+ },
+
+ hideAllTracks: function() {
+ for (var id in this.mapTrackLayers) {
+ if (this.mainLayer.hasLayer(this.mapTrackLayers[id])) {
+ this.toggleTrack(id);
+ }
+ }
+ this.updateMyFirstLastDates();
+ },
+
+ removeTrackDB: function(id) {
+ var that = this;
+ $('#track-list > li[track="'+id+'"]').addClass('icon-loading-small');
+ var req = {};
+ var url = OC.generateUrl('/apps/maps/tracks/'+id);
+ $.ajax({
+ type: 'DELETE',
+ url: url,
+ data: req,
+ async: true
+ }).done(function (response) {
+ that.removeTrackMap(id);
+ that.saveEnabledTracks();
+ }).always(function (response) {
+ $('#track-list > li[track="'+id+'"]').removeClass('icon-loading-small');
+ }).fail(function() {
+ OC.Notification.showTemporary(t('maps', 'Failed to remove track'));
+ });
+ },
+
+ removeAllTracksDB: function() {
+ var that = this;
+ $('#navigation-tracks').addClass('icon-loading-small');
+ var req = {
+ ids: Object.keys(this.trackLayers)
+ };
+ var url = OC.generateUrl('/apps/maps/tracks');
+ $.ajax({
+ type: 'DELETE',
+ url: url,
+ data: req,
+ async: true
+ }).done(function (response) {
+ for (var id in that.trackLayers) {
+ that.removeTrackMap(id);
+ }
+ that.saveEnabledTracks();
+ }).always(function (response) {
+ $('#navigation-tracks').removeClass('icon-loading-small');
+ }).fail(function() {
+ OC.Notification.showTemporary(t('maps', 'Failed to remove track'));
+ });
+ },
+
+ removeTrackMap: function(id) {
+ this.mainLayer.removeLayer(this.mapTrackLayers[id]);
+ this.mapTrackLayers[id].removeLayer(this.trackLayers[id]);
+ delete this.mapTrackLayers[id];
+ delete this.trackLayers[id];
+ delete this.track[id];
+
+ $('style[track='+id+']').remove();
+
+ $('#track-list > li[track="'+id+'"]').fadeOut('slow', function() {
+ $(this).remove();
+ });
+ },
+
+ addTrackDirectoryDB: function(path) {
+ var that = this;
+ $('#navigation-tracks').addClass('icon-loading-small');
+ var req = {
+ path: path
+ };
+ var url = OC.generateUrl('/apps/maps/tracks-directory');
+ $.ajax({
+ type: 'POST',
+ url: url,
+ data: req,
+ async: true
+ }).done(function (response) {
+ // show main layer if needed
+ if (!that.map.hasLayer(that.mainLayer)) {
+ that.toggleTracks();
+ }
+ var ids = [];
+ for (var i=0; i < response.length; i++) {
+ that.addTrackMap(response[i], true);
+ ids.push(response[i].id);
+ }
+ that.saveEnabledTracks(ids);
+ that.optionsController.saveOptionValues({tracksEnabled: true});
+ }).always(function (response) {
+ $('#navigation-tracks').removeClass('icon-loading-small');
+ }).fail(function() {
+ OC.Notification.showTemporary(t('maps', 'Failed to add track directory'));
+ });
+ },
+
+ addTracksDB: function(pathList) {
+ var that = this;
+ $('#navigation-tracks').addClass('icon-loading-small');
+ var req = {
+ pathList: pathList
+ };
+ var url = OC.generateUrl('/apps/maps/tracks');
+ $.ajax({
+ type: 'POST',
+ url: url,
+ data: req,
+ async: true
+ }).done(function (response) {
+ // show main layer if needed
+ if (!that.map.hasLayer(that.mainLayer)) {
+ that.toggleTracks();
+ }
+ var ids = [];
+ for (var i=0; i < response.length; i++) {
+ that.addTrackMap(response[i], true);
+ ids.push(response[i].id);
+ }
+ that.saveEnabledTracks(ids);
+ that.optionsController.saveOptionValues({tracksEnabled: true});
+ }).always(function (response) {
+ $('#navigation-tracks').removeClass('icon-loading-small');
+ }).fail(function() {
+ OC.Notification.showTemporary(t('maps', 'Failed to add tracks'));
+ });
+ },
+
+ addTrackMap: function(track, show=false, pageLoad=false) {
+ // color
+ var color = track.color || OCA.Theming.color;
+ this.trackColors[track.id] = color;
+ this.trackDivIcon[track.id] = L.divIcon({
+ iconAnchor: [12, 25],
+ className: 'trackWaypoint trackWaypoint-'+track.id,
+ html: ''
+ });
+
+ this.mapTrackLayers[track.id] = L.featureGroup();
+ this.trackLayers[track.id] = L.featureGroup();
+ this.trackLayers[track.id].loaded = false;
+ this.mapTrackLayers[track.id].addLayer(this.trackLayers[track.id]);
+
+ var name = track.file_name;
+
+ // side menu entry
+ var imgurl = OC.generateUrl('/svg/core/actions/address?color='+color.replace('#', ''));
+ var li = '' +
+ ' '+name+'' +
+ ' ' +
+ ' ' +
+ '';
+
+ var beforeThis = null;
+ var nameLower = name.toLowerCase();
+ $('#track-list > li').each(function() {
+ trackName = $(this).attr('name');
+ if (nameLower.localeCompare(trackName) < 0) {
+ beforeThis = $(this);
+ return false;
+ }
+ });
+ if (beforeThis !== null) {
+ $(li).insertBefore(beforeThis);
+ }
+ else {
+ $('#track-list').append(li);
+ }
+
+ // enable if in saved options or if it should be enabled for another reason
+ if (show || this.optionsController.enabledTracks.indexOf(track.id) !== -1) {
+ this.toggleTrack(track.id, false, pageLoad);
+ }
+ },
+
+ getTracks: function() {
+ var that = this;
+ $('#navigation-tracks').addClass('icon-loading-small');
+ var req = {};
+ var url = OC.generateUrl('/apps/maps/tracks');
+ $.ajax({
+ type: 'GET',
+ url: url,
+ data: req,
+ async: true
+ }).done(function (response) {
+ var i, track;
+ for (i=0; i < response.length; i++) {
+ track = response[i];
+ that.addTrackMap(track, false, true);
+ }
+ that.trackListLoaded = true;
+ }).always(function (response) {
+ $('#navigation-tracks').removeClass('icon-loading-small');
+ }).fail(function() {
+ OC.Notification.showTemporary(t('maps', 'Failed to load tracks'));
+ });
+ },
+
+ toggleTrack: function(id, save=false, pageLoad=false) {
+ var trackLayer = this.trackLayers[id];
+ if (!trackLayer.loaded) {
+ this.loadTrack(id, save, pageLoad);
+ }
+ this.toggleMapTrackLayer(id);
+ if (save) {
+ this.saveEnabledTracks();
+ this.updateMyFirstLastDates();
+ }
+ },
+
+ toggleMapTrackLayer: function(id) {
+ var mapTrackLayer = this.mapTrackLayers[id];
+ var eyeButton = $('#track-list > li[track="'+id+'"] .toggleTrackButton button');
+ // hide track
+ if (this.mainLayer.hasLayer(mapTrackLayer)) {
+ this.mainLayer.removeLayer(mapTrackLayer);
+ // color of the eye
+ eyeButton.addClass('icon-toggle').attr('style', '');
+ }
+ // show track
+ else {
+ this.mainLayer.addLayer(mapTrackLayer);
+ // color of the eye
+ var color = OCA.Theming.color.replace('#', '');
+ var imgurl = OC.generateUrl('/svg/core/actions/toggle?color='+color);
+ eyeButton.removeClass('icon-toggle').css('background-image', 'url('+imgurl+')');
+ }
+ },
+
+ loadTrack: function(id, save=false, pageLoad=false) {
+ var that = this;
+ $('#track-list > li[track="'+id+'"]').addClass('icon-loading-small');
+ var req = {};
+ var url = OC.generateUrl('/apps/maps/tracks/'+id);
+ $.ajax({
+ type: 'GET',
+ url: url,
+ data: req,
+ async: true
+ }).done(function (response) {
+ that.processGpx(id, response, that.trackLayers[id]);
+ that.trackLayers[id].loaded = true;
+ that.updateMyFirstLastDates(pageLoad);
+ }).always(function (response) {
+ $('#track-list > li[track="'+id+'"]').removeClass('icon-loading-small');
+ }).fail(function() {
+ OC.Notification.showTemporary(t('maps', 'Failed to load track content'));
+ });
+ },
+
+ processGpx: function(id, gpx, layerGroup) {
+ var that = this;
+ var color;
+ var coloredTooltipClass;
+ var rgbc;
+
+ var gpxp = $.parseXML(gpx.replace(/version="1.1"/, 'version="1.0"'));
+ var gpxx = $(gpxp).find('gpx');
+
+ // count the number of lines and point
+ var nbPoints = gpxx.find('>wpt').length;
+ var nbLines = gpxx.find('>trk').length + gpxx.find('>rte').length;
+
+ color = this.trackColors[id];
+ this.setTrackCss(id, color);
+ coloredTooltipClass = 'tooltip' + id;
+
+ var weight = 4;
+
+ var fileDesc = gpxx.find('>metadata>desc').text();
+
+ var minTrackDate = Math.floor(Date.now() / 1000) + 1000000;
+ var date;
+
+ gpxx.find('wpt').each(function() {
+ date = that.addWaypoint(id, $(this), layerGroup, coloredTooltipClass);
+ minTrackDate = (date < minTrackDate) ? date : minTrackDate;
+ });
+
+ gpxx.find('trk').each(function() {
+ name = $(this).find('>name').text();
+ cmt = $(this).find('>cmt').text();
+ desc = $(this).find('>desc').text();
+ linkText = $(this).find('link text').text();
+ linkUrl = $(this).find('link').attr('href');
+ $(this).find('trkseg').each(function() {
+ date = that.addLine(id, $(this).find('trkpt'), layerGroup, weight, color, name, cmt, desc, linkText, linkUrl, coloredTooltipClass);
+ minTrackDate = (date < minTrackDate) ? date : minTrackDate;
+ });
+ });
+
+ // ROUTES
+ gpxx.find('rte').each(function() {
+ name = $(this).find('>name').text();
+ cmt = $(this).find('>cmt').text();
+ desc = $(this).find('>desc').text();
+ linkText = $(this).find('link text').text();
+ linkUrl = $(this).find('link').attr('href');
+ date = that.addLine(id, $(this).find('rtept'), layerGroup, weight, color, name, cmt, desc, linkText, linkUrl, coloredTooltipClass);
+ minTrackDate = (date < minTrackDate) ? date : minTrackDate;
+ });
+
+ layerGroup.date = minTrackDate;
+ },
+
+ addWaypoint: function(id, elem, layerGroup, coloredTooltipClass) {
+ var lat = elem.attr('lat');
+ var lon = elem.attr('lon');
+ var name = elem.find('name').text();
+ var cmt = elem.find('cmt').text();
+ var desc = elem.find('desc').text();
+ var sym = elem.find('sym').text();
+ var ele = elem.find('ele').text();
+ var time = elem.find('time').text();
+ var linkText = elem.find('link text').text();
+ var linkUrl = elem.find('link').attr('href');
+
+ var date = null;
+ if (time) {
+ date = Date.parse(time)/1000;
+ }
+
+ var mm = L.marker(
+ [lat, lon],
+ {
+ icon: this.trackDivIcon[id]
+ }
+ );
+ mm.bindTooltip(brify(name, 20), {className: coloredTooltipClass});
+
+ var popupText = '' + escapeHTML(name) + '
' +
+ t('maps', 'Track')+ ' : ' + escapeHTML(id) + '
';
+ if (linkText && linkUrl) {
+ popupText = popupText +
+ t('maps', 'Link') + ' : '+ escapeHTML(linkText) + '
';
+ }
+ if (ele !== '') {
+ popupText = popupText + t('maps', 'Elevation')+ ' : ' +
+ escapeHTML(ele) + 'm
';
+ }
+ var popupText = popupText + t('maps', 'Latitude') + ' : '+ lat + '
' +
+ t('maps', 'Longitude') + ' : '+ lon + '
';
+ if (cmt !== '') {
+ popupText = popupText +
+ t('maps', 'Comment') + ' : '+ escapeHTML(cmt) + '
';
+ }
+ if (desc !== '') {
+ popupText = popupText +
+ t('maps', 'Description') + ' : '+ escapeHTML(desc) + '
';
+ }
+ if (sym !== '') {
+ popupText = popupText +
+ t('maps', 'Symbol name') + ' : '+ sym;
+ }
+ mm.bindPopup(popupText);
+ layerGroup.addLayer(mm);
+ return date;
+ },
+
+ addLine: function(id, points, layerGroup, weight, color, name, cmt, desc, linkText, linkUrl, coloredTooltipClass) {
+ var lat, lon, ele, time;
+ var latlngs = [];
+ // get first date
+ var date = null;
+ if (points.length > 0) {
+ var p = points.first();
+ time = p.find('time').text();
+ if (time) {
+ date = Date.parse(time)/1000;
+ }
+ }
+ // build line
+ points.each(function() {
+ lat = $(this).attr('lat');
+ lon = $(this).attr('lon');
+ if (!lat || !lon) {
+ return;
+ }
+ ele = $(this).find('ele').text();
+ time = $(this).find('time').text();
+ if (ele !== '') {
+ latlngs.push([lat, lon, ele]);
+ }
+ else{
+ latlngs.push([lat, lon]);
+ }
+ });
+ var l = L.polyline(latlngs, {
+ weight: weight,
+ opacity : 1,
+ className: 'poly'+id,
+ });
+ var popupText = 'Track '+id+'
';
+ if (cmt !== '') {
+ popupText = popupText + '' + t('maps', 'Comment') +
+ '
' +
+ '' +
+ escapeHTML(cmt) + '
';
+ }
+ if (desc !== '') {
+ popupText = popupText + 'Description
' +
+ '' +
+ escapeHTML(desc) + '
';
+ }
+ linkHTML = '';
+ if (linkText && linkUrl) {
+ linkHTML = '' + escapeHTML(linkText) + '';
+ }
+ popupText = popupText.replace('' + escapeHTML(name) + '',
+ '' + escapeHTML(name) + ' (' + linkHTML + ')');
+ l.bindPopup(
+ popupText,
+ {
+ autoPan: true,
+ autoClose: true,
+ closeOnClick: true
+ }
+ );
+ var tooltipText = id;
+ if (id !== name) {
+ tooltipText = tooltipText + '
' + escapeHTML(name);
+ }
+ l.bindTooltip(tooltipText, {sticky: true, className: coloredTooltipClass});
+ // border layout
+ var bl;
+ bl = L.polyline(latlngs,
+ {opacity:1, weight: parseInt(weight * 1.6), color: 'black'});
+ bl.bindPopup(
+ popupText,
+ {
+ autoPan: true,
+ autoClose: true,
+ closeOnClick: true
+ }
+ );
+ layerGroup.addLayer(bl);
+ layerGroup.addLayer(l);
+ bl.on('mouseover', function() {
+ layerGroup.bringToFront();
+ });
+ bl.on('mouseout', function() {
+ });
+ bl.bindTooltip(tooltipText, {sticky: true, className: coloredTooltipClass});
+
+ l.on('mouseover', function() {
+ layerGroup.bringToFront();
+ });
+ l.on('mouseout', function() {
+ });
+
+ return date;
+ },
+
+ zoomOnTrack: function(id) {
+ if (this.mainLayer.hasLayer(this.mapTrackLayers[id])) {
+ this.map.fitBounds(this.mapTrackLayers[id].getBounds(), {padding: [30, 30]});
+ }
+ },
+
+ askChangeTrackColor: function(id) {
+ $('#trackcolor').attr('track', id);
+ var currentColor = this.trackColors[id];
+ $('#colorinput').val(currentColor);
+ $('#colorinput').click();
+ },
+
+ okColor: function() {
+ var color = $('#colorinput').val();
+ var id = $('#trackcolor').attr('track');
+ this.trackColors[id] = color;
+ this.changeTrackColor(id, color);
+ },
+
+ changeTrackColor: function(id, color) {
+ var that = this;
+ $('#track-list > li[track="'+id+'"]').addClass('icon-loading-small');
+ var req = {
+ color: color
+ };
+ var url = OC.generateUrl('/apps/maps/tracks/'+id);
+ $.ajax({
+ type: 'PUT',
+ url: url,
+ data: req,
+ async: true
+ }).done(function (response) {
+ var imgurl = OC.generateUrl('/svg/core/actions/address?color='+color.replace('#', ''));
+ $('#track-list > li[track='+id+'] .track-name').attr('style', 'background-image: url('+imgurl+')');
+
+ that.setTrackCss(id, color);
+ }).always(function (response) {
+ $('#track-list > li[track="'+id+'"]').removeClass('icon-loading-small');
+ }).fail(function() {
+ OC.Notification.showTemporary(t('maps', 'Failed to change track color'));
+ });
+ },
+
+ setTrackCss: function(id, color) {
+ $('style[track='+id+']').remove();
+
+ var rgbc = hexToRgb(color);
+ var textcolor = 'black';
+ if (rgbc.r + rgbc.g + rgbc.b < 3 * 80) {
+ textcolor = 'white';
+ }
+ $('').appendTo('body');
+ },
+
+}
diff --git a/js/utils.js b/js/utils.js
index 1d5552925..4858633ee 100644
--- a/js/utils.js
+++ b/js/utils.js
@@ -3,6 +3,10 @@ function basename(str) {
return base;
}
+function dirname(path) {
+ return path.replace(/\\/g,'/').replace(/\/[^\/]*$/, '');;
+}
+
function Timer(callback, mydelay) {
var timerId, start, remaining = mydelay;
@@ -93,3 +97,23 @@ Date.prototype.toIsoString = function() {
':' + pad(tzo % 60);
}
+function brify(str, linesize) {
+ var res = '';
+ var words = str.split(' ');
+ var cpt = 0;
+ var toAdd = '';
+ for (var i=0; i';
+ toAdd = words[i] + ' ';
+ cpt = words[i].length + 1;
+ }
+ }
+ res += toAdd;
+ return res;
+}
+
diff --git a/lib/Controller/TracksController.php b/lib/Controller/TracksController.php
new file mode 100644
index 000000000..7e90dfd72
--- /dev/null
+++ b/lib/Controller/TracksController.php
@@ -0,0 +1,216 @@
+
+ * @copyright Julien Veyssier 2019
+ */
+
+namespace OCA\Maps\Controller;
+
+use OCP\App\IAppManager;
+
+use OCP\IURLGenerator;
+use OCP\IConfig;
+use \OCP\IL10N;
+
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\RedirectResponse;
+
+use OCP\AppFramework\Http\ContentSecurityPolicy;
+
+use OCP\IRequest;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\ApiController;
+use OCP\Constants;
+use OCP\Share;
+
+use OCA\Maps\Service\TracksService;
+
+function endswith($string, $test) {
+ $strlen = strlen($string);
+ $testlen = strlen($test);
+ if ($testlen > $strlen) return false;
+ return substr_compare($string, $test, $strlen - $testlen, $testlen) === 0;
+}
+
+class TracksController extends Controller {
+
+ private $userId;
+ private $userfolder;
+ private $config;
+ private $appVersion;
+ private $shareManager;
+ private $userManager;
+ private $groupManager;
+ private $dbconnection;
+ private $dbtype;
+ private $dbdblquotes;
+ private $trans;
+ private $logger;
+ private $tracksService;
+ protected $appName;
+
+ public function __construct($AppName, IRequest $request, $UserId,
+ $userfolder, $config, $shareManager,
+ IAppManager $appManager, $userManager,
+ $groupManager, IL10N $trans, $logger, TracksService $tracksService){
+ parent::__construct($AppName, $request);
+ $this->tracksService = $tracksService;
+ $this->logger = $logger;
+ $this->appName = $AppName;
+ $this->appVersion = $config->getAppValue('maps', 'installed_version');
+ $this->userId = $UserId;
+ $this->userManager = $userManager;
+ $this->groupManager = $groupManager;
+ $this->trans = $trans;
+ $this->dbtype = $config->getSystemValue('dbtype');
+ $this->config = $config;
+ $this->dbconnection = \OC::$server->getDatabaseConnection();
+ if ($UserId !== '' and $userfolder !== null){
+ $this->userfolder = $userfolder;
+ }
+ $this->shareManager = $shareManager;
+ }
+
+ /**
+ * @NoAdminRequired
+ */
+ public function getTracks() {
+ $tracks = $this->tracksService->getTracksFromDB($this->userId);
+ $existingTracks = [];
+ foreach ($tracks as $track) {
+ $res = $this->userfolder->getById($track['file_id']);
+ if (is_array($res) and count($res) > 0) {
+ $trackFile = $res[0];
+ if ($trackFile->getType() === \OCP\Files\FileInfo::TYPE_FILE) {
+ $track['file_name'] = $trackFile->getName();
+ array_push($existingTracks, $track);
+ }
+ else {
+ $this->deleteTrack($track['id']);
+ }
+ }
+ else {
+ $this->deleteTrack($track['id']);
+ }
+ }
+ return new DataResponse($existingTracks);
+ }
+
+ /**
+ * @NoAdminRequired
+ */
+ public function getTrackFileContent($id) {
+ $track = $this->tracksService->getTrackFromDB($id);
+ $res = $this->userfolder->getById($track['file_id']);
+ if (is_array($res) and count($res) > 0) {
+ $trackFile = $res[0];
+ if ($trackFile->getType() === \OCP\Files\FileInfo::TYPE_FILE) {
+ $trackContent = $trackFile->getContent();
+ return new DataResponse($trackContent);
+ }
+ else {
+ return new DataResponse('bad file type', 400);
+ }
+ }
+ else {
+ return new DataResponse('file not found', 400);
+ }
+ }
+
+ /**
+ * @NoAdminRequired
+ */
+ public function addTracks($pathList) {
+ $tracks = [];
+ foreach ($pathList as $path) {
+ if ($path && strlen($path) > 0) {
+ $cleanpath = str_replace(array('../', '..\\'), '', $path);
+ if ($this->userfolder->nodeExists($cleanpath)) {
+ $trackFile = $this->userfolder->get($cleanpath);
+ if ($trackFile->getType() === \OCP\Files\FileInfo::TYPE_FILE) {
+ $trackFileId = $trackFile->getId();
+ $trackId = $this->tracksService->addTrackToDB($this->userId, $trackFileId);
+ $track = $this->tracksService->getTrackFromDB($trackId);
+ $track['file_name'] = $trackFile->getName();
+ array_push($tracks, $track);
+ }
+ }
+ }
+ }
+ return new DataResponse($tracks);
+ }
+
+ /**
+ * @NoAdminRequired
+ */
+ public function addTrackDirectory($path) {
+ $tracks = [];
+ if ($path && strlen($path) > 0) {
+ $cleanpath = str_replace(array('../', '..\\'), '', $path);
+ if ($this->userfolder->nodeExists($cleanpath)) {
+ $dir = $this->userfolder->get($cleanpath);
+ if ($dir->getType() === \OCP\Files\FileInfo::TYPE_FOLDER) {
+ // find all gpx files
+ foreach ($dir->searchByMime('application/gpx+xml') as $node) {
+ if ($node->getParent()->getId() === $dir->getId() and
+ $node->getType() === \OCP\Files\FileInfo::TYPE_FILE
+ ) {
+ $trackFileId = $node->getId();
+ $trackId = $this->tracksService->addTrackToDB($this->userId, $trackFileId);
+ $track = $this->tracksService->getTrackFromDB($trackId);
+ $track['file_name'] = $node->getName();
+ array_push($tracks, $track);
+ }
+ }
+ }
+ }
+ }
+ return new DataResponse($tracks);
+ }
+
+ /**
+ * @NoAdminRequired
+ */
+ public function editTrack($id, $color) {
+ $track = $this->tracksService->getTrackFromDB($id, $this->userId);
+ if ($track !== null) {
+ $this->tracksService->editTrackInDB($id, $color);
+ return new DataResponse('EDITED');
+ }
+ else {
+ return new DataResponse('no such track', 400);
+ }
+ }
+
+ /**
+ * @NoAdminRequired
+ */
+ public function deleteTrack($id) {
+ $track = $this->tracksService->getTrackFromDB($id, $this->userId);
+ if ($track !== null) {
+ $this->tracksService->deleteTrackFromDB($id);
+ return new DataResponse('DELETED');
+ }
+ else {
+ return new DataResponse('no such track', 400);
+ }
+ }
+
+ /**
+ * @NoAdminRequired
+ */
+ public function deleteTracks($ids) {
+ if (is_array($ids) && count($ids) > 0) {
+ $this->tracksService->deleteTracksFromDB($ids, $this->userId);
+ }
+ return new DataResponse('DELETED');
+ }
+
+}
diff --git a/lib/Service/TracksService.php b/lib/Service/TracksService.php
new file mode 100644
index 000000000..4b5669a1c
--- /dev/null
+++ b/lib/Service/TracksService.php
@@ -0,0 +1,138 @@
+l10n = $l10n;
+ $this->logger = $logger;
+ $this->qb = \OC::$server->getDatabaseConnection()->getQueryBuilder();
+ }
+
+ /**
+ * @param string $userId
+ */
+ public function getTracksFromDB($userId) {
+ $tracks = [];
+ $qb = $this->qb;
+ $qb->select('id', 'file_id', 'color')
+ ->from('maps_tracks', 't')
+ ->where(
+ $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
+ );
+ $req = $qb->execute();
+
+ while ($row = $req->fetch()) {
+ array_push($tracks, [
+ 'id' => intval($row['id']),
+ 'file_id' => intval($row['file_id']),
+ 'color' => $row['color']
+ ]);
+ }
+ $req->closeCursor();
+ $qb = $qb->resetQueryParts();
+ return $tracks;
+ }
+
+ public function getTrackFromDB($id, $userId=null) {
+ $track = null;
+ $qb = $this->qb;
+ $qb->select('id', 'file_id', 'color')
+ ->from('maps_tracks', 't')
+ ->where(
+ $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
+ );
+ if ($userId !== null) {
+ $qb->andWhere(
+ $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
+ );
+ }
+ $req = $qb->execute();
+
+ while ($row = $req->fetch()) {
+ $track = [
+ 'id' => intval($row['id']),
+ 'file_id' => intval($row['file_id']),
+ 'color' => $row['color']
+ ];
+ break;
+ }
+ $req->closeCursor();
+ $qb = $qb->resetQueryParts();
+ return $track;
+ }
+
+ public function addTrackToDB($userId, $fileId) {
+ $qb = $this->qb;
+ $qb->insert('maps_tracks')
+ ->values([
+ 'user_id' => $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR),
+ 'file_id' => $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)
+ ]);
+ $req = $qb->execute();
+ $trackId = $qb->getLastInsertId();
+ $qb = $qb->resetQueryParts();
+ return $trackId;
+ }
+
+ public function editTrackInDB($id, $color) {
+ $qb = $this->qb;
+ $qb->update('maps_tracks')
+ ->set('color', $qb->createNamedParameter($color, IQueryBuilder::PARAM_STR))
+ ->where(
+ $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
+ );
+ $req = $qb->execute();
+ $qb = $qb->resetQueryParts();
+ }
+
+ public function deleteTrackFromDB($id) {
+ $qb = $this->qb;
+ $qb->delete('maps_tracks')
+ ->where(
+ $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
+ );
+ $req = $qb->execute();
+ $qb = $qb->resetQueryParts();
+ }
+
+ public function deleteTracksFromDB($ids, $userId) {
+ $qb = $this->qb;
+ $qb->delete('maps_tracks')
+ ->where(
+ $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
+ );
+ if (count($ids) > 0) {
+ $or = $qb->expr()->orx();
+ foreach ($ids as $id) {
+ $or->add($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
+ }
+ $qb->andWhere($or);
+ }
+ else {
+ return;
+ }
+ $req = $qb->execute();
+ $qb = $qb->resetQueryParts();
+ }
+
+}
diff --git a/templates/index.php b/templates/index.php
index 544693d1e..36b3e898f 100644
--- a/templates/index.php
+++ b/templates/index.php
@@ -5,6 +5,7 @@
script('maps', 'photosController');
script('maps', 'contactsController');
script('maps', 'favoritesController');
+script('maps', 'tracksController');
script('maps', 'script');
?>
diff --git a/templates/navigation/index.php b/templates/navigation/index.php
index f2342f7ab..44119ac97 100644
--- a/templates/navigation/index.php
+++ b/templates/navigation/index.php
@@ -74,7 +74,6 @@
-
t('Photos')); ?>
@@ -127,4 +126,53 @@
+
+ t('Tracks')); ?>
+
+
+
+
+
+
+