Commit 25b42a52 authored by Theopolisme's avatar Theopolisme
Browse files

Redirect to new domain

parent 95cfc71e
Loading
Loading
Loading
Loading

.gitignore

deleted100644 → 0
+0 −2
Original line number Diff line number Diff line
.DS_Store
node_modules

README.md

deleted100644 → 0
+0 −34
Original line number Diff line number Diff line
# location-history-visualizer

**Available online: [theopolis.me/location-history-visualizer](http://theopolis.me/location-history-visualizer)**

A tool for visualizing your complete, consolidated, collected Google [Location History](https://google.com/locationhistory).

It works directly in your web browser – no software to download, no packages to install. **Everyone deserves to know what data is being collected about them, without having to fiddle with cryptic pieces of software.**

*location-history-visualizer* takes raw Google Takeout output and produces a heatmap of all of your location data over time, overlaid on an interactive map.

## Packages used
* [leaflet.js](http://leafletjs.com/), for rendering the interactive map
* [leaflet.heat](https://github.com/Leaflet/Leaflet.heat), for rendering the heatmap overlay
* [filestream](https://github.com/DamonOehlman/filestream), for dealing with gigantic Google Takeout files
* [oboe.js](http://oboejs.com), for processing said gigantic files
* [browserify](http://browserify.org/), for helping filestream  work properly in the browser


## License

Copyright (C) 2014 Theopolisme <theopolismewiki@gmail.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

index.css

deleted100644 → 0
+0 −210
Original line number Diff line number Diff line
body {
    padding: 0;
    margin: 0;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 14px;
}

html, body, #map {
    height: 100%;
    width: 100%;
}

body.working {
	cursor: progress;
}

.visualizer {
  height: 100%;
  width: 100%;
  position: relative;
  overflow: hidden;
}

.hidden {
	display: none !important;
}

#forkme img {
  z-index: 99999;
}

#map {
	z-index: 0;
}

.container {
	display: table;
	position: absolute;
	height: 100%;
	width: 100%;
	z-index: 9999;
}

.map-active .container {
	/* so that clicks go through to the map! */
	pointer-events: none;
}

.content {
	display: table-cell;
	vertical-align: middle;
}

.content-box {
	margin: 0 auto;
	width: 50%;
	min-width: 300px;
	max-width: 600px;
	background-color: rgba(255,255,255,0.9);
	padding: 10px;
	border-radius: 10px;
}

h2 {
	text-align: center
}

.note {
  padding: 10px;
  text-align: center;
  background-color: #ffeb3b;
}

.credit {
	font-size: 0.8em;
	text-align: center;
}

.whats-new {
  font-size: 0.9em;
  text-align: center;
}

#working {
	text-align: center;
}

#done {
	cursor: pointer;
}

#controls {
  z-index: 99999;
  position: absolute;
  overflow: hidden;
  bottom: -200px;
  /* We set a specific height here so that revealing only the top part of the
  div will always reveal the *correct* top part. When we expand it later (on hover),
  we set height back to auto, so that all the content is shown correctly. Slightly
  hacky, yes, but it appears to be working in all browsers. A Good Thing. */
  height: 171px;
  left: 0px;
  margin: 0px 10px;
  padding: 3px 16px 5px;
  background-color: rgba(255,255,255,0.7);
  transition: bottom 0.5s;
}

.map-active #controls {
  bottom: -145px;
}

#controls:hover {
  bottom: 0px;
  left: 0px;
  height: auto;
  background-color: rgba(255,255,255,0.9);
}

#controls:hover .title span {
  opacity: 0;
}

#controls .title {
  text-align: center;
  font-weight: bold;
  line-height: 1.4;
  font-size: 13px;
  padding-bottom: 3px;
}

#controls .title span {
  display: block;
  line-height: 0.9;
  font-weight: lighter;
  font-size: 11px;
  transition: all 0.5s ease-in-out;
}

#controls .control-block {
    clear: both;
}

#controls .control-block input {
  float: right;
  margin-left: 10px;
}

#controls .actions {
  text-align: center;
}

#controls .support {
  border-top: 1px dashed black;
  margin-top: 10px;
  padding-top: 10px;
  font-size: 0.8em;
  text-align: center;
}

#controls .support div {
  padding-bottom: 5px;
}

/* http://tobiasahlin.com/spinkit/ */

.spinner {
  margin: 20px auto 0;
  width: 70px;
  text-align: center;
}

.spinner > div {
  width: 18px;
  height: 18px;
  background-color: #333;

  border-radius: 100%;
  display: inline-block;
  -webkit-animation: bouncedelay 1.4s infinite ease-in-out;
  animation: bouncedelay 1.4s infinite ease-in-out;
  /* Prevent first frame from flickering when animation starts */
  -webkit-animation-fill-mode: both;
  animation-fill-mode: both;
}

.spinner .bounce1 {
  -webkit-animation-delay: -0.32s;
  animation-delay: -0.32s;
}

.spinner .bounce2 {
  -webkit-animation-delay: -0.16s;
  animation-delay: -0.16s;
}

@-webkit-keyframes bouncedelay {
  0%, 80%, 100% { -webkit-transform: scale(0.0) }
  40% { -webkit-transform: scale(1.0) }
}

@keyframes bouncedelay {
  0%, 80%, 100% { 
    transform: scale(0.0);
    -webkit-transform: scale(0.0);
  } 40% { 
    transform: scale(1.0);
    -webkit-transform: scale(1.0);
  }
}
+3 −104
Original line number Diff line number Diff line
<!DOCTYPE html>
<html>
<head>
    <title>location-history-visualizer</title>
    <link rel="stylesheet" href="index.css"/>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.3/leaflet.css"/>
    <title>Location History Visualizer</title>
    <meta http-equiv="refresh" content="0; url=https://locationhistoryvisualizer.com/heatmap/">
<link rel="canonical" href="https://locationhistoryvisualizer.com/heatmap/" />
</head>
<body>
<div class="visualizer">
    <!-- Fork me on GitHub -->
    <a id="forkme" href="https://github.com/theopolisme/location-history-visualizer" target="_blank"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://camo.githubusercontent.com/52760788cde945287fbb584134c4cbc2bc36f904/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f77686974655f6666666666662e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_white_ffffff.png"></a>

    <!-- Shown before the heatmap is displayed -->
    <div class="container">
        <div class="content">
            <!-- Intro, before data has been uploaded -->
            <div id="intro" class="content-box">
                <h2>Location History Visualizer</h2>
                <div class="note">
                    <strong>Note:</strong> This tool only generates heatmaps. If you're looking for minute-by-minute analysis of your Location History, including accuracy information, location-based search, advanced filtering and measurement tools, and more, check out <strong><a href="https://theopatt.com/lhv-pro">Location History Visualizer Pro</a></strong>.
                </div>
                <p>Welcome to <b>location-history-visualizer</b>, a tool for visualizing your collected Google <a href="https://google.com/locationhistory" target="_blank">Location History</a> data with heatmaps. <i>Don't worry&mdash;all processing and visualization happens directly on your computer, so rest assured that nobody is able to access your Location History but you... and Google, of course.</i> ^.^</p>
                <p>To start off, you'll need to go to <a href="https://google.com/takeout" target="_blank">Google Takeout</a> to download your Location History data: on that page, deselect everything except Location History by clicking "Select none" and then reselecting "Location History". Then hit "Next" and, finally, click "Create archive". Once the archive has been created, click "Download". Unzip the downloaded file, and open the "Location History" folder within. <b>Then, drag and drop <i>LocationHistory.json</i> from inside that folder onto this page.</b> Let the visualization begin!</p>
                <p class="fallback">Alternatively, select your <b>LocationHistory.json</b> file directly: <input name="file" type="file" id="file"></input></p>
                <hr>
                <p><i>Experimental (even faster!) import method:</i> Instead of going through Google Takeout, simply browse to the <a href="https://maps.google.com/locationhistory/kml?startTime=0&endTime=9000000000000" target="_blank">Google Location History KML API endpoint</a>. A file (<b>history-12-31-1969.kml</b>) will be downloaded &ndash; just drag and drop it onto this page, and we're off! <i>Note:</i> This uses a non-public Google API and as such may cease to work at any point. Your mileage may vary.</p>
                <p class="credit">A project by <a href="https://theopolis.me" target="_blank">@theopolisme</a>. Made in 2014 in Memphis, Tennessee (updated 2015).</p>
            </div>

            <!-- Shown in interim while processing is in progress -->
            <div id="working" class="content-box hidden">
                <h2>Processing data...</h2>
                <div class="spinner">
                    <div class="bounce1"></div>
                    <div class="bounce2"></div>
                    <div class="bounce3"></div>
                </div>
                <p><span id="currentStatus">Waking up...</span></p>
                <p>This will take a while... sit back, get a cup of tea or something.</p>
                <p><i>Why does it take so long?</i> Depending on how long Google's been tracking your location, you may have hundreds of thousands of [latitude, longitude] pairs, every one of which must be loaded, analyzed, and plotted. That's a lot of dots.</p>
            </div>

            <!-- Content displayed once processing complete -->
            <div id="done" class="content-box hidden">
                <h2>Render complete!</h2>
                <p>Successfully processed <span id="numberProcessed"></span> data points to generate the heatmap. Click inside this box to dismiss it and start exploring...</p>
                <p><i>Zoom</i> by scrolling, double-clicking, or using the buttons in the upper lefthand corner. <i>Navigate</i> by clicking and dragging. Hover over the menu in the lower lefthand corner to customize the heatmap rendering.</p>
                <p class="credit">A project by <a href="http://theopolis.me" target="_blank">@theopolisme</a>. Made in 2014 in Memphis, Tennessee.</p>
            </div>
        </div>
    </div>

    <!-- Appear when heatmap is displayed -->
    <div id="controls">
        <div class="title">
            location-history-visualizer
            <span>(hover for additional controls)</span>
        </div>
        <div class="control-block">
            <label for="radius">Point radius</label>
            <input type="range" min="1" max="30" id="radius" class="control">
        </div>
        <div class="control-block">
            <label for="blur">Blur radius</label>
            <input type="range" min="1" max="25" id="blur" class="control">
        </div>
        <div class="control-block">
            <label for="heatOpacity">Heat opacity</label>
            <input type="range" min="0" max="1" step="0.05" id="heatOpacity" class="control">
        </div>
        <div class="control-block">
            <label for="tileOpacity">Map opacity</label>
            <input type="range" min="0" max="1" step="0.05" id="tileOpacity" class="control">
        </div>
        <div class="control-block actions">
            <button id="reset">Reset to defaults</button>
        </div>
        <div class="support">
            <div>Find this useful? Buy me a cup of tea :)</div>
            <form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_blank">
                <input type="hidden" name="cmd" value="_s-xclick">
                <input type="hidden" name="hosted_button_id" value="AGUH8EGVDADM8">
                <input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
                <img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
            </form>
        </div>
    </div>

    <!-- The map, populated by leaflet.js -->
    <div id="map"></div>
</div>

<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.3/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/3.8.4/dropzone.min.js"></script>
<script src="lib/leaflet.heat.min.js"></script>
<script src="lib/prettysize.js"></script>
<script src="index.js"></script>

<!-- Google Analytics -->
<script>
  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

  ga('create', 'UA-55418641-1', 'auto');
  ga('send', 'pageview');
</script>
</body>
</html>

index.js

deleted100644 → 0
+0 −191
Original line number Diff line number Diff line
( function ( $, L, prettySize ) {
	var map, heat,
		heatOptions = {
			tileOpacity: 1,
			heatOpacity: 1,
			radius: 25,
			blur: 15
		};

	// Start at the beginning
	stageOne();

	function stageOne () {
		var dropzone;

		// Initialize the map
		map = L.map( 'map' ).setView([0,0], 2);
		L.tileLayer( 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
			attribution: 'location-history-visualizer is open source and available <a href="https://github.com/theopolisme/location-history-visualizer">on GitHub</a>. Map data &copy; <a href="https://openstreetmap.org">OpenStreetMap</a> contributors.',
			maxZoom: 18,
			minZoom: 2
		} ).addTo( map );

		// Initialize the dropzone
		dropzone = new Dropzone( document.body, {
			url: '/',
			previewsContainer: document.createElement( 'div' ), // >> /dev/null
			clickable: false,
			accept: function ( file, done ) {
				stageTwo( file );
				dropzone.disable(); // Your job is done, buddy
			}
		} );

		// For mobile browsers, allow direct file selection as well
		$( '#file' ).change( function () {
			stageTwo( this.files[0] );
			dropzone.disable();
		} );
	}

	function stageTwo ( file ) {
		heat = L.heatLayer( [], heatOptions ).addTo( map );

		// First, change tabs
		$( 'body' ).addClass( 'working' );
		$( '#intro' ).addClass( 'hidden' );
		$( '#working' ).removeClass( 'hidden' );

		// Now start working!
		processFile( file );

		function status ( message ) {
			$( '#currentStatus' ).text( message );
		}

		function processFile ( file ) {
			var fileSize = prettySize( file.size ),
				reader = new FileReader();

			status( 'Preparing to import file (' + fileSize + ')...' );

			function getLocationDataFromJson ( data ) {
				var SCALAR_E7 = 0.0000001, // Since Google Takeout stores latlngs as integers
					locations = JSON.parse( data ).locations;

				if ( !locations || locations.length === 0 ) {
					throw new ReferenceError( 'No location data found.' );
				}

				return locations.map( function ( location ) {
					return [ location.latitudeE7 * SCALAR_E7, location.longitudeE7 * SCALAR_E7 ];
				} );
			}

			function getLocationDataFromKml ( data ) {
				var KML_DATA_REGEXP = /<when>(.*?)<\/when>\s*<gx:coord>(\S*)\s(\S*)\s(\S*)<\/gx:coord>/g,
					locations = [],
					match = KML_DATA_REGEXP.exec( data );

				// match
				//  [1] ISO 8601 timestamp
				//  [2] longitude
				//  [3] latitude
				//  [4] altitude (not currently provided by Location History)

				while ( match !== null ) {
					locations.push( [ Number( match[3] ), Number( match[2] ) ] );
					match = KML_DATA_REGEXP.exec( data );
				}

				return locations;
			}

			reader.onprogress = function ( e ) {
				var percentLoaded = Math.round( ( e.loaded / e.total ) * 100 );
				status( percentLoaded + '% of ' + fileSize + ' loaded...' );
			};

			reader.onload = function ( e ) {
				var latlngs;

				status( 'Generating map...' );

				try {
					if ( /\.kml$/i.test( file.name ) ) {
						latlngs = getLocationDataFromKml( e.target.result );
					} else {
						latlngs = getLocationDataFromJson( e.target.result );
					}
				} catch ( ex ) {
					status( 'Something went wrong generating your map. Ensure you\'re uploading a Google Takeout JSON file that contains location data and try again, or create an issue on GitHub if the problem persists. (error: ' + ex.message + ')' );
					return;
				}

				heat._latlngs = latlngs;

				heat.redraw();
				stageThree( /* numberProcessed */ latlngs.length );
			};

			reader.onerror = function () {
				status( 'Something went wrong reading your JSON file. Ensure you\'re uploading a "direct-from-Google" JSON file and try again, or create an issue on GitHub if the problem persists. (error: ' + reader.error + ')' );
			};

			reader.readAsText( file );
		}
	}

	function stageThree ( numberProcessed ) {
		var $done = $( '#done' );

		// Change tabs :D
		$( 'body' ).removeClass( 'working' );
		$( '#working' ).addClass( 'hidden' );
		$done.removeClass( 'hidden' );

		// Update count
		$( '#numberProcessed' ).text( numberProcessed.toLocaleString() );

		// Fade away when clicked
		$done.one( 'click', function () {
			$( 'body' ).addClass( 'map-active' );
			$done.fadeOut();
			activateControls();
		} );

		function activateControls () {
			var $tileLayer = $( '.leaflet-tile-pane' ),
				$heatmapLayer = $( '.leaflet-heatmap-layer' ),
				originalHeatOptions = $.extend( {}, heatOptions ); // for reset

			// Update values of the dom elements
			function updateInputs () {
				var option;
				for ( option in heatOptions ) {
					if ( heatOptions.hasOwnProperty( option ) ) {
						document.getElementById( option ).value = heatOptions[option];
					}
				}
			}

			updateInputs();

			$( '.control' ).change( function () {
				switch ( this.id ) {
					case 'tileOpacity':
						$tileLayer.css( 'opacity', this.value );
						break;
					case 'heatOpacity':
						$heatmapLayer.css( 'opacity', this.value );
						break;
					default:
						heatOptions[ this.id ] = Number( this.value );
						heat.setOptions( heatOptions );
						break;
				}
			} );

			$( '#reset' ).click( function () {
				$.extend( heatOptions, originalHeatOptions );
				updateInputs();
				heat.setOptions( heatOptions );
				// Reset opacity too
				$heatmapLayer.css( 'opacity', originalHeatOptions.heatOpacity );
				$tileLayer.css( 'opacity', originalHeatOptions.tileOpacity );
			} );
		}
	}

}( jQuery, L, prettySize ) );
Loading