├── .gitignore ├── .htaccess ├── LICENSE ├── Locory.php ├── LocoryDbMigrator.php ├── LocoryDemoDataGenerator.php ├── README.md ├── bower.json ├── build.bat ├── build_tools └── 7za.exe ├── composer.json ├── config-dist.php ├── data ├── .gitignore ├── demotrack1.csv ├── demotrack2.csv ├── demotrack3.csv └── demotrack4.csv ├── index.php ├── locory.js ├── locory.phpproj ├── locory.png ├── locory.sln ├── publication_assets └── mainpage.png ├── robots.txt ├── style.css ├── version.txt └── views ├── dashboard.js ├── dashboard.php ├── layout.php ├── login.js └── login.php /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studo 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | 106 | # MightyMoose 107 | *.mm.* 108 | AutoTest.Net/ 109 | 110 | # Web workbench (sass) 111 | .sass-cache/ 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.[Pp]ublish.xml 131 | *.azurePubxml 132 | # TODO: Comment the next line if you want to checkin your web deploy settings 133 | # but database connection strings (with potential passwords) will be unencrypted 134 | *.pubxml 135 | *.publishproj 136 | 137 | # NuGet Packages 138 | *.nupkg 139 | # The packages folder can be ignored because of Package Restore 140 | **/packages/* 141 | # except build/, which is used as an MSBuild target. 142 | !**/packages/build/ 143 | # Uncomment if necessary however generally it will be regenerated when needed 144 | #!**/packages/repositories.config 145 | 146 | # Windows Azure Build Output 147 | csx/ 148 | *.build.csdef 149 | 150 | # Windows Store app package directory 151 | AppPackages/ 152 | 153 | # Others 154 | *.[Cc]ache 155 | ClientBin/ 156 | [Ss]tyle[Cc]op.* 157 | ~$* 158 | *~ 159 | *.dbmdl 160 | *.dbproj.schemaview 161 | *.pfx 162 | *.publishsettings 163 | node_modules/ 164 | bower_components/ 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file 170 | # to a newer Visual Studio version. Backup files are not needed, 171 | # because we have git ;-) 172 | _UpgradeReport_Files/ 173 | Backup*/ 174 | UpgradeLog*.XML 175 | UpgradeLog*.htm 176 | 177 | # SQL Server files 178 | *.mdf 179 | *.ldf 180 | 181 | # Business Intelligence projects 182 | *.rdl.data 183 | *.bim.layout 184 | *.bim_*.settings 185 | 186 | # Microsoft Fakes 187 | FakesAssemblies/ 188 | 189 | # Node.js Tools for Visual Studio 190 | .ntvs_analysis.dat 191 | 192 | # Visual Studio 6 build log 193 | *.plg 194 | 195 | # Visual Studio 6 workspace options file 196 | *.opt 197 | 198 | /components 199 | /vendor 200 | /.release 201 | /config.php 202 | /composer.phar 203 | /composer.lock 204 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteCond %{REQUEST_FILENAME} !-f 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteRule ^ index.php [QSA,L] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Bernd Bestel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Locory.php: -------------------------------------------------------------------------------- 1 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 23 | 24 | if ($doMigrations === true) 25 | { 26 | self::$DbConnection->exec("CREATE TABLE IF NOT EXISTS migrations (migration SMALLINT NOT NULL, execution_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (migration))"); 27 | LocoryDbMigrator::MigrateDb(self::$DbConnection); 28 | 29 | if (self::IsDemoInstallation()) 30 | { 31 | LocoryDemoDataGenerator::PopulateDemoData(self::$DbConnection); 32 | } 33 | } 34 | } 35 | 36 | return self::$DbConnection; 37 | } 38 | 39 | public static function AddLocationPoint($time, $latitude, $longitude, $accuracy) 40 | { 41 | $db = self::GetDbConnection(); 42 | 43 | $statement = $db->prepare('INSERT INTO locationpoints (time, latitude, longitude, accuracy) VALUES (:time, :latitude, :longitude, :accuracy)'); 44 | $statement->bindValue(':time', $time); 45 | $statement->bindValue(':latitude', $latitude); 46 | $statement->bindValue(':longitude', $longitude); 47 | $statement->bindValue(':accuracy', $accuracy); 48 | 49 | $statement->execute(); 50 | } 51 | 52 | public static function AddCsvData($csvString) 53 | { 54 | $lines = explode(PHP_EOL, $csvString); 55 | 56 | foreach ($lines as $line) 57 | { 58 | if (!empty($line)) 59 | { 60 | $parsedLine = str_getcsv($line); 61 | self::AddLocationPoint($parsedLine[0], $parsedLine[1], $parsedLine[2], $parsedLine[3]); 62 | } 63 | } 64 | } 65 | 66 | public static function GetLocationPoints($from, $to) 67 | { 68 | $db = self::GetDbConnection(); 69 | 70 | $statement = $db->prepare('SELECT * FROM locationpoints WHERE time >= :from AND time <= :to'); 71 | $statement->bindValue(':from', $from); 72 | $statement->bindValue(':to', $to); 73 | $statement->execute(); 74 | 75 | $rows = array(); 76 | while ($row = $statement->fetch(PDO::FETCH_ASSOC)) 77 | { 78 | $rows[] = $row; 79 | } 80 | 81 | return $rows; 82 | } 83 | 84 | public static function GetLocationPointStatistics($from, $to) 85 | { 86 | $db = self::GetDbConnection(); 87 | 88 | $statement = $db->prepare('SELECT MIN(accuracy) AS AccuracyMin, MAX(accuracy) AS AccuracyMax, AVG(accuracy) AS AccuracyAverage, SUM(distance_to_point_before) AS Distance FROM locationpoints WHERE time >= :from AND time <= :to'); 89 | $statement->bindValue(':from', $from); 90 | $statement->bindValue(':to', $to); 91 | $statement->execute(); 92 | 93 | return $statement->fetch(PDO::FETCH_ASSOC); 94 | } 95 | 96 | public static function CalculateLocationPointDistances() 97 | { 98 | $db = self::GetDbConnection(); 99 | $distanceCalculator = new Vincenty(); 100 | 101 | $statementNotCalculatedRows = $db->prepare('SELECT id, time, latitude, longitude FROM locationpoints WHERE distance_to_point_before IS NULL ORDER BY time'); 102 | $statementNotCalculatedRows->execute(); 103 | 104 | $idPreviousRow = null; 105 | while ($row = $statementNotCalculatedRows->fetch(PDO::FETCH_ASSOC)) 106 | { 107 | if ($idPreviousRow == null) 108 | { 109 | //Try to get the row before, should only happen once when starting a new calculation 110 | 111 | $statementRowBefore = $db->prepare('SELECT id, latitude, longitude FROM locationpoints WHERE time < :time ORDER BY time DESC LIMIT 1'); 112 | $statementRowBefore->bindValue(':time', $row['time']); 113 | $statementRowBefore->execute(); 114 | $rowBefore = $statementRowBefore->fetch(PDO::FETCH_ASSOC); 115 | 116 | $idPreviousRow = $rowBefore['id']; 117 | $latitudePreviousRow = $rowBefore['latitude']; 118 | $longitudePreviousRow = $rowBefore['longitude']; 119 | } 120 | 121 | if ($idPreviousRow != null) 122 | { 123 | $coordinatePrevious = new Coordinate($latitudePreviousRow, $longitudePreviousRow); 124 | $coordinateCurrent = new Coordinate($row['latitude'], $row['longitude']); 125 | $distance = $distanceCalculator->getDistance($coordinatePrevious, $coordinateCurrent); 126 | 127 | $statementUpdate = $db->prepare('UPDATE locationpoints SET distance_to_point_before = :distance WHERE id = :id'); 128 | $statementUpdate->bindValue(':distance', $distance); 129 | $statementUpdate->bindValue(':id', $row['id']); 130 | $statementUpdate->execute(); 131 | } 132 | 133 | $idPreviousRow = $row['id']; 134 | $latitudePreviousRow = $row['latitude']; 135 | $longitudePreviousRow = $row['longitude']; 136 | } 137 | } 138 | 139 | /** 140 | * @return boolean 141 | */ 142 | public static function IsDemoInstallation() 143 | { 144 | return file_exists(__DIR__ . '/data/demo.txt'); 145 | } 146 | 147 | private static $InstalledVersion; 148 | /** 149 | * @return string 150 | */ 151 | public static function GetInstalledVersion() 152 | { 153 | if (self::$InstalledVersion == null) 154 | { 155 | self::$InstalledVersion = file_get_contents(__DIR__ . '/version.txt'); 156 | } 157 | 158 | return self::$InstalledVersion; 159 | } 160 | 161 | /** 162 | * @return boolean 163 | */ 164 | public static function IsValidSession($sessionKey) 165 | { 166 | if ($sessionKey === null || empty($sessionKey)) 167 | { 168 | return false; 169 | } 170 | else 171 | { 172 | return file_exists(__DIR__ . "/data/sessions/$sessionKey.txt"); 173 | } 174 | } 175 | 176 | /** 177 | * @return string 178 | */ 179 | public static function CreateSession() 180 | { 181 | if (!file_exists(__DIR__ . '/data/sessions')) 182 | { 183 | mkdir(__DIR__ . '/data/sessions'); 184 | } 185 | 186 | $now = time(); 187 | foreach (new FilesystemIterator(__DIR__ . '/data/sessions') as $file) 188 | { 189 | if ($now - $file->getCTime() >= 2678400) //31 days 190 | { 191 | unlink(__DIR__ . '/data/sessions/' . $file->getFilename()); 192 | } 193 | } 194 | 195 | $newSessionKey = uniqid() . uniqid() . uniqid(); 196 | file_put_contents(__DIR__ . "/data/sessions/$newSessionKey.txt", ''); 197 | return $newSessionKey; 198 | } 199 | 200 | public static function RemoveSession($sessionKey) 201 | { 202 | unlink(__DIR__ . "/data/sessions/$sessionKey.txt"); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /LocoryDbMigrator.php: -------------------------------------------------------------------------------- 1 | query('SELECT COUNT(*) FROM migrations WHERE migration = ' . $migrationId)->fetchColumn(); 25 | if (intval($rowCount) === 0) 26 | { 27 | $pdo->exec(utf8_encode($sql)); 28 | $statement = $pdo->prepare('INSERT INTO migrations (migration) VALUES (:id)'); 29 | $statement->bindValue(':id', $migrationId); 30 | $statement->execute(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LocoryDemoDataGenerator.php: -------------------------------------------------------------------------------- 1 | query('SELECT COUNT(*) FROM migrations WHERE migration = -1')->fetchColumn(); 8 | if (intval($rowCount) === 0) 9 | { 10 | $sql = " 11 | INSERT INTO migrations (migration) VALUES (-1); 12 | "; 13 | $pdo->exec(utf8_encode($sql)); 14 | 15 | $csvTrack1 = file_get_contents(__DIR__ . '/data/demotrack1.csv'); 16 | $csvTrack2 = file_get_contents(__DIR__ . '/data/demotrack2.csv'); 17 | $csvTrack3 = file_get_contents(__DIR__ . '/data/demotrack3.csv'); 18 | $csvTrack4 = file_get_contents(__DIR__ . '/data/demotrack4.csv'); 19 | 20 | $startDateForTrack1 = new DateTime(); 21 | $startDateForTrack1->modify('-4 day'); 22 | $startDateForTrack1->setTime(0, 0, 0); 23 | 24 | $startDateForTrack2 = new DateTime(); 25 | $startDateForTrack2->modify('-3 day'); 26 | $startDateForTrack2->setTime(0, 0, 0); 27 | 28 | $startDateForTrack3 = new DateTime(); 29 | $startDateForTrack3->modify('-2 day'); 30 | $startDateForTrack3->setTime(0, 0, 0); 31 | 32 | $startDateForTrack4 = new DateTime(); 33 | $startDateForTrack4->modify('-1 day'); 34 | $startDateForTrack4->setTime(0, 0, 0); 35 | 36 | $linesTrack1 = explode(PHP_EOL, $csvTrack1); 37 | foreach ($linesTrack1 as $line) 38 | { 39 | $startDateForTrack1->modify('+1 second'); 40 | $line = str_replace('TIME', $startDateForTrack1->format('Y-m-d H:i:s'), $line); 41 | $parsedLine = str_getcsv($line); 42 | Locory::AddLocationPoint($parsedLine[0], $parsedLine[1], $parsedLine[2], $parsedLine[3]); 43 | } 44 | 45 | $linesTrack2 = explode(PHP_EOL, $csvTrack2); 46 | foreach ($linesTrack2 as $line) 47 | { 48 | $startDateForTrack2->modify('+1 second'); 49 | $line = str_replace('TIME', $startDateForTrack2->format('Y-m-d H:i:s'), $line); 50 | $parsedLine = str_getcsv($line); 51 | Locory::AddLocationPoint($parsedLine[0], $parsedLine[1], $parsedLine[2], $parsedLine[3]); 52 | } 53 | 54 | $linesTrack3 = explode(PHP_EOL, $csvTrack3); 55 | foreach ($linesTrack3 as $line) 56 | { 57 | $startDateForTrack3->modify('+1 second'); 58 | $line = str_replace('TIME', $startDateForTrack3->format('Y-m-d H:i:s'), $line); 59 | $parsedLine = str_getcsv($line); 60 | Locory::AddLocationPoint($parsedLine[0], $parsedLine[1], $parsedLine[2], $parsedLine[3]); 61 | } 62 | 63 | $linesTrack4 = explode(PHP_EOL, $csvTrack4); 64 | foreach ($linesTrack4 as $line) 65 | { 66 | $startDateForTrack4->modify('+1 second'); 67 | $line = str_replace('TIME', $startDateForTrack4->format('Y-m-d H:i:s'), $line); 68 | $parsedLine = str_getcsv($line); 69 | Locory::AddLocationPoint($parsedLine[0], $parsedLine[1], $parsedLine[2], $parsedLine[3]); 70 | } 71 | 72 | Locory::CalculateLocationPointDistances(); 73 | } 74 | } 75 | 76 | public static function RecreateDemo() 77 | { 78 | $db = Locory::GetDbConnection(); 79 | $db->exec('TRUNCATE TABLE migrations'); 80 | $db->exec('TRUNCATE TABLE locationpoints'); 81 | 82 | $db = Locory::GetDbConnection(true); 83 | self::PopulateDemoData($db); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # locory 2 | A private / self-hosted location history solution 3 | 4 | ## Motivation 5 | I love to have a location history, but because such data is too private to let this do others for you, I searched for a self hosted solution, found nothing that fitted my needs completely, so this is locory. 6 | 7 | # Give it a try 8 | Public demo of the latest version → [https://locory-demo.berrnd.xyz](https://locory-demo.berrnd.xyz) 9 | 10 | ## How to install 11 | Just unpack the [latest release](https://github.com/berrnd/locory/releases/latest) on your PHP enabled webserver, copy `config-dist.php` to `data/config.php`, edit it to your needs, create the new database on the server you configured, ensure that the `data` directory is writable and you're ready to go. Alternatively clone this repository and install composer dependencies manually. 12 | 13 | If you use nginx as your webserver, please include `try_files $uri /index.php;` in your location block. 14 | 15 | ## What currently is possible 16 | - It provides an API to save location data 17 | - It gives you a simple web interface to show your location history in custom time ranges, calculates the (linear) distance, and so on 18 | 19 | ### Get data in 20 | Make a POST to `https://locory/api/add/csv`, the body has to be one or multiple lines in the format `,,,`. 21 | I personally do this with [Automagic](https://play.google.com/store/apps/details?id=ch.gridvision.ppam.androidautomagic) on my smartphone. 22 | 23 | ### Calculate distance between location points 24 | This can be a long running task, so this is not done at the time of import - just setup a cron job for example every hour. 25 | 26 | `*/60 * * * * www-data php //index.php /cli/calculate/distance GET` 27 | 28 | ### Show your location history 29 | Just browse to the your install URL and login with the configured credentials 30 | 31 | ## Screenshots 32 | ![Main page](https://github.com/berrnd/locory/raw/master/publication_assets/mainpage.png "Main page") 33 | 34 | ## Todo 35 | The analysis features are very basic for now, there are much more great things, heatmaps, "where spent I most of my time", and so on... 36 | 37 | ## License 38 | The MIT License (MIT) 39 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asp.net", 3 | "private": true, 4 | "dependencies": { 5 | "bootstrap": "^3.3.7", 6 | "font-awesome": "^4.7.0", 7 | "moment": "^2.18.1", 8 | "jquery-timeago": "^1.6.1", 9 | "leaflet": "^1.1.0", 10 | "bootstrap-daterangepicker": "^2.1.25" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | set projectPath=%~dp0 2 | if %projectPath:~-1%==\ set projectPath=%projectPath:~0,-1% 3 | 4 | set releasePath=%projectPath%\.release 5 | mkdir "%releasePath%" 6 | 7 | for /f "tokens=*" %%a in ('type version.txt') do set version=%%a 8 | 9 | del "%releasePath%\locory_%version%.zip" 10 | "build_tools\7za.exe" a -r "%releasePath%\locory_%version%.zip" "%projectPath%\*" -xr!.* -xr!build_tools -xr!build.bat -xr!composer.json -xr!composer.lock -xr!composer.phar -xr!locory.phpproj -xr!locory.phpproj.user -xr!locory.sln -xr!bower.json -xr!publication_assets 11 | "build_tools\7za.exe" a -r "%releasePath%\locory_%version%.zip" "%projectPath%\.htaccess" 12 | "build_tools\7za.exe" d "%releasePath%\locory_%version%.zip" data\*.* data\sessions 13 | -------------------------------------------------------------------------------- /build_tools/7za.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berrnd/locory/dd427f01c2e9e34024f81e45d9c8dfb4cd86e426/build_tools/7za.exe -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "slim/slim": "^3.8", 4 | "slim/php-view": "^2.2", 5 | "mjaschen/phpgeo": "^1.3", 6 | "pavlakis/slim-cli": "^1.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /config-dist.php: -------------------------------------------------------------------------------- 1 | [ 19 | 'displayErrorDetails' => true, 20 | 'determineRouteBeforeAppMiddleware' => true 21 | ], 22 | ])); 23 | $container = $app->getContainer(); 24 | $container['renderer'] = new PhpRenderer('./views'); 25 | } 26 | 27 | if (PHP_SAPI === 'cli') 28 | { 29 | $app->add(new \pavlakis\cli\CliRequest()); 30 | } 31 | 32 | if (!Locory::IsDemoInstallation() && PHP_SAPI !== 'cli') 33 | { 34 | $sessionMiddleware = function(Request $request, Response $response, callable $next) 35 | { 36 | $route = $request->getAttribute('route'); 37 | $routeName = $route->getName(); 38 | 39 | if (!Locory::IsValidSession($_COOKIE['locory_session']) && $routeName !== 'login') 40 | { 41 | //Allow also authentication by headers 42 | if ($request->hasHeader('Locory-Username') && $request->hasHeader('Locory-Password')) 43 | { 44 | if ($request->getHeaderLine('Locory-Username') === HTTP_USER && $request->getHeaderLine('Locory-Password') === HTTP_PASSWORD) 45 | { 46 | $response = $next($request, $response); 47 | } 48 | } 49 | else 50 | { 51 | $response = $response->withRedirect('/login'); 52 | } 53 | } 54 | else 55 | { 56 | $response = $next($request, $response); 57 | } 58 | 59 | return $response; 60 | }; 61 | 62 | $app->add($sessionMiddleware); 63 | } 64 | 65 | $app->get('/login', function(Request $request, Response $response) 66 | { 67 | return $this->renderer->render($response, '/layout.php', [ 68 | 'title' => 'Login', 69 | 'contentPage' => 'login.php' 70 | ]); 71 | })->setName('login'); 72 | 73 | $app->post('/login', function(Request $request, Response $response) 74 | { 75 | $postParams = $request->getParsedBody(); 76 | if (isset($postParams['username']) && isset($postParams['password'])) 77 | { 78 | if ($postParams['username'] === HTTP_USER && $postParams['password'] === HTTP_PASSWORD) 79 | { 80 | $sessionKey = Locory::CreateSession(); 81 | setcookie('locory_session', $sessionKey, time()+2592000); //30 days 82 | 83 | return $response->withRedirect('/'); 84 | } 85 | else 86 | { 87 | return $response->withRedirect('/login?invalid=true'); 88 | } 89 | } 90 | else 91 | { 92 | return $response->withRedirect('/login?invalid=true'); 93 | } 94 | })->setName('login'); 95 | 96 | $app->get('/logout', function(Request $request, Response $response) 97 | { 98 | Locory::RemoveSession($_COOKIE['locory_session']); 99 | return $response->withRedirect('/'); 100 | }); 101 | 102 | $app->get('/', function(Request $request, Response $response) 103 | { 104 | Locory::GetDbConnection(true); //For database schema migration 105 | 106 | return $this->renderer->render($response, '/layout.php', [ 107 | 'title' => 'Dashboard', 108 | 'contentPage' => 'dashboard.php' 109 | ]); 110 | }); 111 | 112 | $app->group('/api', function() 113 | { 114 | $this->post('/add/csv', function(Request $request, Response $response) 115 | { 116 | Locory::AddCsvData($request->getBody()->getContents()); 117 | echo json_encode(array('success' => true)); 118 | }); 119 | 120 | $this->get('/get/locationpoints/{from}/{to}', function(Request $request, Response $response, $args) 121 | { 122 | echo json_encode(Locory::GetLocationPoints($args['from'] . ' 00:00:00', $args['to'] . ' 23:59:59')); 123 | }); 124 | 125 | $this->get('/get/statistics/{from}/{to}', function(Request $request, Response $response, $args) 126 | { 127 | echo json_encode(Locory::GetLocationPointStatistics($args['from'] . ' 00:00:00', $args['to'] . ' 23:59:59')); 128 | }); 129 | })->add(function($request, $response, $next) 130 | { 131 | $response = $next($request, $response); 132 | return $response->withHeader('Content-Type', 'application/json'); 133 | }); 134 | 135 | $app->group('/cli', function() 136 | { 137 | $this->get('/calculate/distance', function(Request $request, Response $response) 138 | { 139 | Locory::CalculateLocationPointDistances(); 140 | }); 141 | 142 | $this->get('/recreatedemo', function(Request $request, Response $response) 143 | { 144 | if (Locory::IsDemoInstallation()) 145 | { 146 | LocoryDemoDataGenerator::RecreateDemo(); 147 | } 148 | }); 149 | })->add(function($request, $response, $next) 150 | { 151 | $response = $next($request, $response); 152 | 153 | if (PHP_SAPI !== 'cli') 154 | { 155 | echo 'Please call this only from CLI'; 156 | return $response->withHeader('Content-Type', 'text/plain')->withStatus(400); 157 | } 158 | 159 | return $response->withHeader('Content-Type', 'text/plain'); 160 | }); 161 | 162 | $app->run(); 163 | -------------------------------------------------------------------------------- /locory.js: -------------------------------------------------------------------------------- 1 | var Locory = {}; 2 | 3 | $(function() 4 | { 5 | var menuItem = $('.nav').find("[data-nav-for-page='" + Locory.ContentPage + "']"); 6 | menuItem.addClass('active'); 7 | 8 | Locory.DefaultFrom = moment().subtract(1, "days"); 9 | Locory.DefaultTo = moment().subtract(1, "days"); 10 | 11 | $.timeago.settings.allowFuture = true; 12 | $('time.timeago').timeago(); 13 | }); 14 | 15 | Locory.FetchJson = function(url, success, error) 16 | { 17 | var xhr = new XMLHttpRequest(); 18 | 19 | xhr.onreadystatechange = function() 20 | { 21 | if (xhr.readyState === XMLHttpRequest.DONE) 22 | { 23 | if (xhr.status === 200) 24 | { 25 | if (success) 26 | { 27 | success(JSON.parse(xhr.responseText)); 28 | } 29 | } 30 | else 31 | { 32 | if (error) 33 | { 34 | error(xhr); 35 | } 36 | } 37 | } 38 | }; 39 | 40 | xhr.open("GET", url, true); 41 | xhr.send(); 42 | } 43 | 44 | Locory.SetupMap = function(mapElementId) 45 | { 46 | Locory.Map = L.map(mapElementId); 47 | 48 | L.tileLayer("https://osm-tile-cache.berrnd.org/{z}/{x}/{y}.png", { 49 | attribution: 'Map data © OpenStreetMap contributors', 50 | maxZoom: 18 51 | }).addTo(Locory.Map); 52 | 53 | Locory.LocationPointsLayer = new L.FeatureGroup(); 54 | Locory.Map.addLayer(Locory.LocationPointsLayer); 55 | } 56 | 57 | Locory.GetUriParam = function(key) 58 | { 59 | var currentUri = decodeURIComponent(window.location.search.substring(1)); 60 | var vars = currentUri.split('&'); 61 | 62 | for (i = 0; i < vars.length; i++) 63 | { 64 | var currentParam = vars[i].split('='); 65 | 66 | if (currentParam[0] === key) 67 | { 68 | return currentParam[1] === undefined ? true : currentParam[1]; 69 | } 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /locory.phpproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | locory 6 | 174b96d0-7d4f-446a-b86e-ed5d7ffac534 7 | Library 8 | 9 | 10 | {A0786B88-2ADB-4C21-ABE8-AA2D79766269} 11 | locory 12 | 13 | 14 | true 15 | 16 | 17 | false 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /locory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berrnd/locory/dd427f01c2e9e34024f81e45d9c8dfb4cd86e426/locory.png -------------------------------------------------------------------------------- /locory.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{A0786B88-2ADB-4C21-ABE8-AA2D79766269}") = "locory", "locory.phpproj", "{174B96D0-7D4F-446A-B86E-ED5D7FFAC534}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {174B96D0-7D4F-446A-B86E-ED5D7FFAC534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {174B96D0-7D4F-446A-B86E-ED5D7FFAC534}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | EndGlobal 21 | -------------------------------------------------------------------------------- /publication_assets/mainpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berrnd/locory/dd427f01c2e9e34024f81e45d9c8dfb4cd86e426/publication_assets/mainpage.png -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | } 4 | 5 | .navbar-fixed-top { 6 | border: 0; 7 | } 8 | 9 | .sidebar { 10 | display: none; 11 | } 12 | 13 | @media (min-width: 768px) { 14 | .sidebar { 15 | position: fixed; 16 | top: 51px; 17 | bottom: 0; 18 | left: 0; 19 | z-index: 1000; 20 | display: block; 21 | padding: 20px; 22 | overflow-x: hidden; 23 | overflow-y: auto; 24 | background-color: #f5f5f5; 25 | border-right: 1px solid #5e5e5e; 26 | min-width: 210px; 27 | max-width: 260px; 28 | } 29 | 30 | #navbar-mobile { 31 | display: none !important; 32 | } 33 | } 34 | 35 | .nav-sidebar { 36 | margin-right: -21px; 37 | margin-bottom: 20px; 38 | margin-left: -20px; 39 | } 40 | 41 | .nav-sidebar > li > a { 42 | padding-right: 20px; 43 | padding-left: 20px; 44 | } 45 | 46 | .nav-sidebar > .active > a, 47 | .nav-sidebar > .active > a:hover, 48 | .nav-sidebar > .active > a:focus { 49 | color: #fff; 50 | background-color: #5e5e5e; 51 | } 52 | 53 | .main { 54 | padding: 20px; 55 | } 56 | 57 | @media (min-width: 768px) { 58 | .main { 59 | padding-right: 40px; 60 | padding-left: 40px; 61 | } 62 | } 63 | 64 | .main .page-header { 65 | margin-top: 0; 66 | } 67 | 68 | .nav-copyright { 69 | position: absolute; 70 | bottom: 0; 71 | width: 100%; 72 | color: #b3b3b1; 73 | font-size: 0.85em; 74 | text-align: center; 75 | } 76 | 77 | .discrete-link { 78 | color: inherit; 79 | } 80 | 81 | .navbar-fixed-top { 82 | border-bottom: solid; 83 | border-color: #5e5e5e; 84 | } 85 | 86 | .navbar-brand { 87 | font-weight: bold; 88 | letter-spacing: -2px; 89 | font-size: 2.2em; 90 | } 91 | 92 | .table td.fit-content, 93 | .table th.fit-content { 94 | white-space: nowrap; 95 | width: 1%; 96 | } 97 | 98 | .dataTables_info, 99 | .dataTables_length, 100 | .dataTables_filter { 101 | font-style: italic; 102 | } 103 | 104 | .timeago-contextual { 105 | font-style: italic; 106 | font-size: 0.8em; 107 | } 108 | 109 | .disabled, 110 | .no-real-button { 111 | pointer-events: none; 112 | } 113 | 114 | .warning-bg { 115 | background-color: #fcf8e3 !important; 116 | } 117 | 118 | .error-bg { 119 | background-color: #f2dede !important; 120 | } 121 | 122 | .info-bg { 123 | background-color: #afd9ee !important; 124 | } 125 | 126 | .discrete-content-separator { 127 | padding-top: 5px; 128 | padding-bottom: 5px; 129 | } 130 | 131 | .discrete-content-separator-2x { 132 | padding-top: 10px; 133 | padding-bottom: 10px; 134 | } 135 | 136 | #map { 137 | height: 750px; 138 | } 139 | 140 | #daterange { 141 | cursor: pointer; 142 | width: 200px; 143 | } 144 | 145 | #daterange-navigation { 146 | padding-top: 5px; 147 | width: 200px; 148 | margin-left: 60px; 149 | } 150 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 1.0.0 -------------------------------------------------------------------------------- /views/dashboard.js: -------------------------------------------------------------------------------- 1 | Locory.SetupMap("map"); 2 | 3 | function ReloadMap(from, to) 4 | { 5 | if (moment.isMoment(from) && moment.isMoment(to)) 6 | { 7 | Locory.Map.removeLayer(Locory.LocationPointsLayer); 8 | Locory.LocationPointsLayer = new L.FeatureGroup(); 9 | Locory.Map.addLayer(Locory.LocationPointsLayer); 10 | 11 | Locory.FetchJson("/api/get/locationpoints/" + from.format("YYYY-MM-DD") + "/" + to.format("YYYY-MM-DD"), 12 | function(points) 13 | { 14 | if (points.length > 0) 15 | { 16 | for (point of points) 17 | { 18 | L.marker([point.latitude, point.longitude]).addTo(Locory.LocationPointsLayer); 19 | } 20 | 21 | Locory.Map.fitBounds(Locory.LocationPointsLayer.getBounds()); 22 | document.getElementById("summary-location-points").innerText = points.length; 23 | 24 | Locory.FetchJson("/api/get/statistics/" + from.format("YYYY-MM-DD") + "/" + to.format("YYYY-MM-DD"), 25 | function(statistics) 26 | { 27 | document.getElementById("summary-accuracy-min").innerText = parseFloat(statistics.AccuracyMin).toFixed(0); 28 | document.getElementById("summary-accuracy-max").innerText = parseFloat(statistics.AccuracyMax).toFixed(0); 29 | document.getElementById("summary-accuracy-average").innerText = parseFloat(statistics.AccuracyAverage).toFixed(0); 30 | document.getElementById("summary-distance").innerText = (parseFloat(statistics.Distance) / 1000).toFixed(1); 31 | }, 32 | function(xhr) 33 | { 34 | console.error(xhr); 35 | } 36 | ); 37 | 38 | $("#datainfo").removeClass("hide"); 39 | $("#nodatainfo").addClass("hide"); 40 | } 41 | else 42 | { 43 | $("#datainfo").addClass("hide"); 44 | $("#nodatainfo").removeClass("hide"); 45 | } 46 | }, 47 | function(xhr) 48 | { 49 | console.error(xhr); 50 | } 51 | ); 52 | } 53 | } 54 | 55 | $(function() 56 | { 57 | function SetDateRangeDisplay(start, end) 58 | { 59 | $("#daterange span").html(start.format("YYYY-MM-DD") + " - " + end.format("YYYY-MM-DD")); 60 | } 61 | 62 | function MoveDateRange(forward) 63 | { 64 | picker = $("#daterange").data("daterangepicker"); 65 | var days = moment.duration(picker.endDate.diff(picker.startDate)).asDays().toFixed(0); 66 | 67 | if (forward === false) 68 | { 69 | days = days * -1; 70 | } 71 | 72 | var newStartDate = picker.startDate.add(days, "days"); 73 | var newEndDate = picker.endDate.add(days, "days"); 74 | 75 | SetupDateRangePicker(newStartDate, newEndDate); 76 | ReloadMap(newStartDate, newEndDate); 77 | } 78 | 79 | function SetupDateRangePicker(from, to) 80 | { 81 | $("#daterange").daterangepicker( 82 | { 83 | startDate: from, 84 | endDate: to, 85 | showWeekNumbers: true, 86 | alwaysShowCalendars: true, 87 | showDropdowns: true, 88 | opens: "center", 89 | locale: 90 | { 91 | format: "YYYY-MM-DD", 92 | firstDay: 1 93 | }, 94 | ranges: 95 | { 96 | "Today": [moment(), moment()], 97 | "Yesterday": [moment().subtract(1, "days"), moment().subtract(1, "days")], 98 | "Last 7 Days": [moment().subtract(6, "days"), moment()], 99 | "Last 30 Days": [moment().subtract(29, "days"), moment()], 100 | "This Month": [moment().startOf("month"), moment().endOf("month")], 101 | "Last Month": [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")] 102 | } 103 | }, SetDateRangeDisplay); 104 | 105 | $("#daterange").on("apply.daterangepicker", function(ev, picker) 106 | { 107 | ReloadMap(picker.startDate, picker.endDate); 108 | }); 109 | 110 | SetDateRangeDisplay(from, to); 111 | } 112 | 113 | $("#daterange-forward").click(function() 114 | { 115 | MoveDateRange(true); 116 | }); 117 | 118 | $("#daterange-backward").click(function() 119 | { 120 | MoveDateRange(false); 121 | }); 122 | 123 | SetupDateRangePicker(Locory.DefaultFrom, Locory.DefaultTo); 124 | ReloadMap(Locory.DefaultFrom, Locory.DefaultTo); 125 | }); 126 | -------------------------------------------------------------------------------- /views/dashboard.php: -------------------------------------------------------------------------------- 1 |
2 | 3 |

  location points,  km total distance - accuracy varies between  m and  m (average  m)No data found in the given date range

4 | 5 |
6 |   7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /views/layout.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <?php echo $title; ?> | locory 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 62 | 63 |
64 |
65 | 66 | 90 | 91 | 92 | 93 | 94 |
95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /views/login.js: -------------------------------------------------------------------------------- 1 | $(function() 2 | { 3 | $(".logout-button").hide(); 4 | 5 | $("#username").focus(); 6 | 7 | if (Locory.GetUriParam("invalid") === "true") 8 | { 9 | $("#login-error").text("Invalid credentials, please try again."); 10 | $("#login-error").show(); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /views/login.php: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Login

4 | 5 |
6 | 7 |
8 | 9 | 10 |
11 |
12 | 13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 | 23 |
24 | --------------------------------------------------------------------------------