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') + + '

    ' + + ''; + } + if (desc !== '') { + popupText = popupText + '

    Description

    ' + + ''; + } + 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 @@ - + + + +