<commit_before>var setting = require('../settings.js');
var format = require('../format.js');
var Flickr = require('../adapters/flickr.js');
var Enum = require('../enum.js');
var log = require('winston');
var key = 'map';
/**
 * @enum {number}
 * @const
 */
var point = {
	longitude: 0,
	latitude: 1,
	elevation: 2,
	time: 3,
	speed: 4
};

/**
 * @enum {number}
 * @const
 */
exports.point = point;

/**
 * @type {GeoJSON}
 */
exports.json = null;

/**
 * @param {Post} post
 * @param {function(GeoJSON)} callback
 */
exports.fromPost = function(post, callback)
{
	if (!post.photosLoaded)
	{

	}

	callback(geo);
};

/**
 *
 * @param slug
 * @param callback
 */
exports.loadGPX = function(slug, callback)
{
	var db = require('../adapters/hash.js');
	db.getObject(key, slug, callback);
};

/**
 * @param {Post} post
 * @param {function(Boolean)} callback
 */
exports.makeGPX = function(post, callback)
{
	addPhotos({ type: 'FeatureCollection', features: [] }, post, callback);
};

/**
 * @param {String} gpx
 * @param {Post} post
 * @param {function(Boolean)} callback
 */
exports.saveGPX = function(gpx, post, callback)
{
	/** @type {GeoJSON} */
	var geo = parseGPX(gpx);
	if (geo == null) { callback(false); return; }
	addPhotos(geo, post, callback);
};

/**
 * Add photos to the map
 * @param {GeoJSON|Object} geo
 * @param {Post} post
 * @param {function(Boolean)} callback
 */
function addPhotos(geo, post, callback)
{
	post.getPhotos(function()
	{
		var compress = require('zlib');
		var db = require('../adapters/hash.js');

		for (var i = 0; i < post.photos.length; i++)
		{
			if (post.photos[i].latitude)
			{
				geo.features.push(parsePhoto(post.photos[i]));
			}
		}

		compress.gzip(JSON.stringify(geo), function(err, buffer)
		{
			db.add(key, post.slug,
			{
				'buffer': buffer.toString('hex'),
				'eTag': post.slug + '_map_' + (new Date()).getTime().toString()
			}, callback);
		});
	});
}

// Distance -------------------------------------------------------------------

/**
 * Total distance between points in a line
 * @param {Array} points
 */
function lineLength(points)
{
	var length = 0;
	var lastPoint = null;

	for (var i = 0; i < points.length; i++)
	{
		if (lastPoint) { length += distance(lastPoint, points[i]); }
		lastPoint = points[i];
	}
	return length;
}

/**
 * Distance in miles between geographic points
 * South latitudes are negative, east longitudes are positive
 * @param {number[]} p1 [longitude, latitude, elevation, time]
 * @param {number[]} p2
 * @return {number}
 * @see http://stackoverflow.com/questions/3694380/calculating-distance-between-two-points-using-latitude-longitude-what-am-i-doi
 * @see http://www.geodatasource.com/developers/javascript
 */
function distance(p1, p2)
{
	if (sameLocation(p1, p2)) { return 0; }

	var theta = p1[point.longitude] - p2[point.longitude];
	var d = Math.sin(deg2rad(p1[point.latitude])) * Math.sin(deg2rad(p2[point.latitude]))
		  + Math.cos(deg2rad(p1[point.latitude])) * Math.cos(deg2rad(p2[point.latitude])) * Math.cos(deg2rad(theta));

	d = Math.acos(d);
	d = rad2deg(d);
	d = d * 60 * 1.1515;    // miles

	return d;
}

/**
 * Whether two points are at the same location (disregarding elevation)
 * @param {number[]} p1
 * @param {number[]} p2
 * @return {Boolean}
 */
function sameLocation(p1, p2)
{
	return p1[point.latitude] == p2[point.latitude]
		&& p1[point.longitude] == p2[point.longitude];
}
function deg2rad(deg) { return (deg * Math.PI / 180.0); }
function rad2deg(rad) {	return (rad * 180.0 / Math.PI); }

// GPX to GeoJSON -------------------------------------------------------------
// https://github.com/mapbox/togeojson

/**
 * Convert GPX to GeoJSON
 * @param {String} gpx
 * @return {GeoJSON}
 */
function parseGPX(gpx)
{
	var DOM = require('xmldom').DOMParser;
	/** @type {Document} */
	var xml = null;

	try
	{
		xml = new DOM().parseFromString(gpx);
	}
	catch (ex)
	{
		log.error(ex.toString());
		return null;
	}
	var i, f;
	var tracks = xml.getElementsByTagName('trk');
	var routes = xml.getElementsByTagName('rte');
	var waypoints = xml.getElementsByTagName('wpt');
	/** @type {GeoJSON} */
	var geo = {
		type: 'FeatureCollection',
		features: []
	};

	for (i = 0; i < tracks.length; i++)
	{
		f = parseTrack(tracks[i]);
		if (f != null) { geo.features.push(f); }
	}
	for (i = 0; i < routes.length; i++)
	{
		f = parseRoute(routes[i]);
		if (f != null) { geo.features.push(f); }
	}
	for (i = 0; i < waypoints.length; i++)
	{
		f = parsePoint(waypoints[i]);
		if (f != null) { geo.features.push(f); }
	}
	return geo;
}

/**
 * First child or null
 * @param {Document|Node} node
 * @param {String} tag
 * @returns {Node}
 */
function firstNode(node, tag)
{
	var n = node.getElementsByTagName(tag);
	return n.length ? n[0] : null;
}

/**
 * @param {Node|Element} dom
 * @param {String} name
 * @returns {Number}
 */
function attrf(dom, name) { return parseFloat(dom.getAttribute(name)); }

/**
 * Node content
 * @param {Node} node
 * @returns {string}
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Node.normalize
 */
function nodeValue(node)
{
	if (node && node.normalize) { node.normalize(); }
	return node && node.firstChild && node.firstChild.nodeValue;
}

/**
 * Return location as [longitude, latitude, elevation, time, speed]
 * A degree of latitude is approximately 69 miles
 * A degree of longitude is about 69 miles at the equater, 0 at the poles
 * @param {Node|Element} node
 * @returns {Number[]}
 * @see http://nationalatlas.gov/articles/mapping/a_latlong.html
 */
function parseLocation(node)
{
	var location = [attrf(node, 'lon'), attrf(node, 'lat')];    // decimal degrees
	var elevation = firstNode(node, 'ele');                     // meters
	var t = firstNode(node, 'time');                            // UTC

	// exclude points close to home
	if (distance(location, setting.map.privacyCenter) < setting.map.privacyMiles) { return null; }

	if (elevation)
	{
		var m = parseFloat(nodeValue(elevation));
		location.push(Math.round(m * 3.28084));     // convert meters to whole feet
	}
	if (t)
	{
		var d = new Date(nodeValue(t));
		location.push(d.getTime());
	}
	// empty speed
	location.push(0);

	return location;
}

/**
 * @param {Node|Element} node
 * @returns {GeoJSON.Feature}
 */
function parseTrack(node)
{
	var segments = node.getElementsByTagName('trkseg');
	var track = [];
	var stat = { topSpeed: 0, avgSpeed: 0, duration: 0, distance: 0 };
	var count = 0, total = 0, s = 0, last;

	for (var i = 0; i < segments.length; i++)
	{
		track.push(parseLine(segments[i], 'trkpt'));
	}

	if (track.length == 0 || track[0].length == 0) { return null; }

	last = track[track.length - 1];

	// milliseconds between first and last point converted to hours
	//stat.duration = format.hoursAndMinutes((last[last.length - 1][point.time] - track[0][0][point.time]) / Enum.time.hour);
	stat.duration = (last[last.length - 1][point.time] - track[0][0][point.time]) / Enum.time.hour;

	// get combined max and average speeds for track segments
	for (i = 0; i < track.length; i++)
	{
		for (var j = 1; j < track[i].length; j++)
		{
			count++;
			s = track[i][j][point.speed];
			if (s > stat.topSpeed) { stat.topSpeed = parseFloat(s.toFixed(1)); }
			track[i][j] = (track[i][j]).slice(0,3);   // remove time and speed
			total += s;
		}
		stat.distance += lineLength(track[i]);
		track[i] = simplifyTrack(track[i]);
	}

	stat.avgSpeed = parseFloat((total / count).toFixed(1));
	stat.distance = parseFloat(stat.distance.toFixed(2));

	return {
		type: 'Feature',
		properties: getProperties(node, stat),
		geometry: {
			type: track.length === 1 ? 'LineString' : 'MultiLineString',
			coordinates: track.length === 1 ? track[0] : track
		}
	};
}

/**
 * @param {Node} node
 * @returns {GeoJSON.Feature}
 */
function parsePoint(node)
{
	var p = getProperties(node);

	p.sym = nodeValue(firstNode(node, 'sym'));

	return {
		type: 'Feature',
		properties: p,
		geometry: {
			type: 'Point',
			coordinates: parseLocation(node)
		}
	};
}

/**
 * Convert Flickr photo to GeoJSON feature
 * @param {Flickr.PhotoSummary} photo
 * @returns {GeoJSON.Feature}
 */
function parsePhoto(photo)
{
	return {
		type: 'Feature',
		geometry: {
			type: 'Point',
			coordinates: [photo.longitude, photo.latitude]
		},
		properties: {
			id: photo.id,
			title: photo.title,
			preview: photo[Flickr.size.medium640]
		}
	};
}

/**
 * @param {Node|Element} node
 * @param {String} name
 * @returns {Array} Array of point arrays
 */
function parseLine(node, name)
{
	var points = node.getElementsByTagName(name);
	var line = [];
	var t, d, p;

	for (var i = 0; i < points.length; i++)
	{
		p = parseLocation(points[i]);
		if (p != null) { line.push(p); }
	}

	// add speed to each point
	for (i = 1; i < line.length; i++)
	{
		t = line[i][point.time] - line[i - 1][point.time];                  // milliseconds
		d = distance(line[i], line[i - 1]);                                 // miles
		line[i][point.speed] = (t > 0 && d > 0) ? d/(t/Enum.time.hour) : 0; // miles per hour
	}

	return line;
}

/**
 * Create route object
 * @param {Node} node
 * @returns {GeoJSON.Feature}
 */
function parseRoute(node)
{
	return {
		type: 'Feature',
		properties: getProperties(node),
		geometry: {
			type: 'LineString',
			coordinates: parseLine(node, 'rtept')
		}
	};
}

/**
 * Properties of a GPX node
 * @param {Node} node
 * @param {Object} [extras] Object literal of additional properties to set
 * @returns {Object}
 */
function getProperties(node, extras)
{
	var names = ['name', 'desc', 'author', 'copyright', 'link', 'time', 'keywords'];
	var properties = (extras) ? extras : {};

	for (var i = 0; i < names.length; i++)
	{
		var value = nodeValue(firstNode(node, names[i]));
		if (value) { properties[names[i]] = value; }
	}
	return properties;
}

// Simplify -------------------------------------------------------------------

/**
 * Simplification using Douglas-Peucker algorithm with recursion elimination
 * @param {Array} points
 * @returns {Array}
 */
function simplifyTrack(points)
{
	var len = points.length,
		keep = new Uint8Array(len),
		// convert tolerance in feet to tolerance in geographic degrees
		tolerance = setting.map.maxDeviationFeet / Enum.distance.equator,
		first = 0,
		last = len - 1,
		stack = [],
		newPoints = [],
		i, maxDistance, distance, index;

	keep[first] = keep[last] = 1;   // keep the end-points

	while (last)
	{
		maxDistance = 0;

		for (i = first + 1; i < last; i++)
		{
			distance = pointLineDistance(points[i], points[first], points[last]);

			if (distance > maxDistance)
			{
				index = i;
				maxDistance = distance;
			}
		}

		if (maxDistance > tolerance)
		{
			keep[index] = 1;    // keep the deviant point
			stack.push(first, index, index, last);
		}

		last = stack.pop();
		first = stack.pop();
	}

	for (i = 0; i < len; i++) {	if (keep[i]) { newPoints.push(points[i]); }	}

	return newPoints;
}

/**
 * Shortest distance from a point to a segment
 * @param {number[]} p
 * @param {number[]} p1
 * @param {number[]} p2
 * @returns {number}
 */
function pointLineDistance(p, p1, p2)
{
	var x = p1[0],
		y = p1[1],
		dx = p2[0] - x,
		dy = p2[1] - y;

	if (dx !== 0 || dy !== 0)
	{
		var t = ((p[0] - x) * dx + (p[1] - y) * dy) / (dx * dx + dy * dy);

		if (t > 1)
		{
			x = p2[0];
			y = p2[1];
		}
		else if (t > 0)
		{
			x += dx * t;
			y += dy * t;
		}
	}

	dx = p[0] - x;
	dy = p[1] - y;

	return dx * dx + dy * dy;
}<commit_msg>fix acos on distance calc<commit_after>var setting = require('../settings.js');
var format = require('../format.js');
var Flickr = require('../adapters/flickr.js');
var Enum = require('../enum.js');
var log = require('winston');
var key = 'map';
/**
 * @enum {number}
 * @const
 */
var point = {
	longitude: 0,
	latitude: 1,
	elevation: 2,
	time: 3,
	speed: 4
};

/**
 * @enum {number}
 * @const
 */
exports.point = point;

/**
 * @type {GeoJSON}
 */
exports.json = null;

/**
 * @param {Post} post
 * @param {function(GeoJSON)} callback
 */
exports.fromPost = function(post, callback)
{
	if (!post.photosLoaded)
	{

	}

	callback(geo);
};

/**
 *
 * @param slug
 * @param callback
 */
exports.loadGPX = function(slug, callback)
{
	var db = require('../adapters/hash.js');
	db.getObject(key, slug, callback);
};

/**
 * @param {Post} post
 * @param {function(Boolean)} callback
 */
exports.makeGPX = function(post, callback)
{
	addPhotos({ type: 'FeatureCollection', features: [] }, post, callback);
};

/**
 * @param {String} gpx
 * @param {Post} post
 * @param {function(Boolean)} callback
 */
exports.saveGPX = function(gpx, post, callback)
{
	/** @type {GeoJSON} */
	var geo = parseGPX(gpx);
	if (geo == null) { callback(false); return; }
	addPhotos(geo, post, callback);
};

/**
 * Add photos to the map
 * @param {GeoJSON|Object} geo
 * @param {Post} post
 * @param {function(Boolean)} callback
 */
function addPhotos(geo, post, callback)
{
	post.getPhotos(function()
	{
		var compress = require('zlib');
		var db = require('../adapters/hash.js');

		for (var i = 0; i < post.photos.length; i++)
		{
			if (post.photos[i].latitude)
			{
				geo.features.push(parsePhoto(post.photos[i]));
			}
		}

		compress.gzip(JSON.stringify(geo), function(err, buffer)
		{
			db.add(key, post.slug,
			{
				'buffer': buffer.toString('hex'),
				'eTag': post.slug + '_map_' + (new Date()).getTime().toString()
			}, callback);
		});
	});
}

// Distance -------------------------------------------------------------------

/**
 * Total distance between points in a line
 * @param {Array} points
 */
function lineLength(points)
{
	var length = 0;
	var lastPoint = null;

	for (var i = 0; i < points.length; i++)
	{
		if (lastPoint) { length += distance(lastPoint, points[i]); }
		lastPoint = points[i];
	}
	return length;
}

/**
 * Distance in miles between geographic points
 * South latitudes are negative, east longitudes are positive
 * @param {number[]} p1 [longitude, latitude, elevation, time]
 * @param {number[]} p2
 * @return {number}
 * @see http://stackoverflow.com/questions/3694380/calculating-distance-between-two-points-using-latitude-longitude-what-am-i-doi
 * @see http://www.geodatasource.com/developers/javascript
 */
function distance(p1, p2)
{
	if (sameLocation(p1, p2)) { return 0; }

	var theta = p1[point.longitude] - p2[point.longitude];
	var d = Math.sin(deg2rad(p1[point.latitude])) * Math.sin(deg2rad(p2[point.latitude]))
		  + Math.cos(deg2rad(p1[point.latitude])) * Math.cos(deg2rad(p2[point.latitude])) * Math.cos(deg2rad(theta));

	if (d >= -1 && d <= 1)
	{
		d = Math.acos(d);
		d = rad2deg(d);
		d = d * 60 * 1.1515;    // miles
	}
	else
	{
		d = 0;
	}
	return d;
}

/**
 * Whether two points are at the same location (disregarding elevation)
 * @param {number[]} p1
 * @param {number[]} p2
 * @return {Boolean}
 */
function sameLocation(p1, p2)
{
	return p1[point.latitude] == p2[point.latitude]
		&& p1[point.longitude] == p2[point.longitude];
}
function deg2rad(deg) { return (deg * Math.PI / 180.0); }
function rad2deg(rad) {	return (rad * 180.0 / Math.PI); }

// GPX to GeoJSON -------------------------------------------------------------
// https://github.com/mapbox/togeojson

/**
 * Convert GPX to GeoJSON
 * @param {String} gpx
 * @return {GeoJSON}
 */
function parseGPX(gpx)
{
	var DOM = require('xmldom').DOMParser;
	/** @type {Document} */
	var xml = null;

	try
	{
		xml = new DOM().parseFromString(gpx);
	}
	catch (ex)
	{
		log.error(ex.toString());
		return null;
	}
	var i, f;
	var tracks = xml.getElementsByTagName('trk');
	var routes = xml.getElementsByTagName('rte');
	var waypoints = xml.getElementsByTagName('wpt');
	/** @type {GeoJSON} */
	var geo = {
		type: 'FeatureCollection',
		features: []
	};

	for (i = 0; i < tracks.length; i++)
	{
		f = parseTrack(tracks[i]);
		if (f != null) { geo.features.push(f); }
	}
	for (i = 0; i < routes.length; i++)
	{
		f = parseRoute(routes[i]);
		if (f != null) { geo.features.push(f); }
	}
	for (i = 0; i < waypoints.length; i++)
	{
		f = parsePoint(waypoints[i]);
		if (f != null) { geo.features.push(f); }
	}
	return geo;
}

/**
 * First child or null
 * @param {Document|Node} node
 * @param {String} tag
 * @returns {Node}
 */
function firstNode(node, tag)
{
	var n = node.getElementsByTagName(tag);
	return n.length ? n[0] : null;
}

/**
 * @param {Node|Element} dom
 * @param {String} name
 * @returns {Number}
 */
function attrf(dom, name) { return parseFloat(dom.getAttribute(name)); }

/**
 * Node content
 * @param {Node} node
 * @returns {string}
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Node.normalize
 */
function nodeValue(node)
{
	if (node && node.normalize) { node.normalize(); }
	return node && node.firstChild && node.firstChild.nodeValue;
}

/**
 * Return location as [longitude, latitude, elevation, time, speed]
 * A degree of latitude is approximately 69 miles
 * A degree of longitude is about 69 miles at the equater, 0 at the poles
 * @param {Node|Element} node
 * @returns {Number[]}
 * @see http://nationalatlas.gov/articles/mapping/a_latlong.html
 */
function parseLocation(node)
{
	var location = [attrf(node, 'lon'), attrf(node, 'lat')];    // decimal degrees
	var elevation = firstNode(node, 'ele');                     // meters
	var t = firstNode(node, 'time');                            // UTC

	// exclude points close to home
	if (distance(location, setting.map.privacyCenter) < setting.map.privacyMiles) { return null; }

	if (elevation)
	{
		var m = parseFloat(nodeValue(elevation));
		location.push(Math.round(m * 3.28084));     // convert meters to whole feet
	}
	if (t)
	{
		var d = new Date(nodeValue(t));
		location.push(d.getTime());
	}
	// empty speed
	location.push(0);

	return location;
}

/**
 * @param {Node|Element} node
 * @returns {GeoJSON.Feature}
 */
function parseTrack(node)
{
	var segments = node.getElementsByTagName('trkseg');
	var track = [];
	var stat = { topSpeed: 0, avgSpeed: 0, duration: 0, distance: 0 };
	var count = 0, total = 0, s = 0, last;

	for (var i = 0; i < segments.length; i++)
	{
		track.push(parseLine(segments[i], 'trkpt'));
	}

	if (track.length == 0 || track[0].length == 0) { return null; }

	last = track[track.length - 1];

	// milliseconds between first and last point converted to hours
	//stat.duration = format.hoursAndMinutes((last[last.length - 1][point.time] - track[0][0][point.time]) / Enum.time.hour);
	stat.duration = (last[last.length - 1][point.time] - track[0][0][point.time]) / Enum.time.hour;

	// get combined max and average speeds for track segments
	for (i = 0; i < track.length; i++)
	{
		for (var j = 1; j < track[i].length; j++)
		{
			count++;
			s = track[i][j][point.speed];
			if (s > stat.topSpeed) { stat.topSpeed = parseFloat(s.toFixed(1)); }
			track[i][j] = (track[i][j]).slice(0,3);   // remove time and speed
			total += s;
		}
		stat.distance += lineLength(track[i]);
		track[i] = simplifyTrack(track[i]);
	}

	stat.avgSpeed = parseFloat((total / count).toFixed(1));
	stat.distance = parseFloat(stat.distance.toFixed(2));

	return {
		type: 'Feature',
		properties: getProperties(node, stat),
		geometry: {
			type: track.length === 1 ? 'LineString' : 'MultiLineString',
			coordinates: track.length === 1 ? track[0] : track
		}
	};
}

/**
 * @param {Node} node
 * @returns {GeoJSON.Feature}
 */
function parsePoint(node)
{
	var p = getProperties(node);

	p.sym = nodeValue(firstNode(node, 'sym'));

	return {
		type: 'Feature',
		properties: p,
		geometry: {
			type: 'Point',
			coordinates: parseLocation(node)
		}
	};
}

/**
 * Convert Flickr photo to GeoJSON feature
 * @param {Flickr.PhotoSummary} photo
 * @returns {GeoJSON.Feature}
 */
function parsePhoto(photo)
{
	return {
		type: 'Feature',
		geometry: {
			type: 'Point',
			coordinates: [photo.longitude, photo.latitude]
		},
		properties: {
			id: photo.id,
			title: photo.title,
			preview: photo[Flickr.size.medium640]
		}
	};
}

/**
 * @param {Node|Element} node
 * @param {String} name
 * @returns {Array} Array of point arrays
 */
function parseLine(node, name)
{
	var points = node.getElementsByTagName(name);
	var line = [];
	var t, d, p;

	for (var i = 0; i < points.length; i++)
	{
		p = parseLocation(points[i]);
		if (p != null) { line.push(p); }
	}

	// add speed to each point
	for (i = 1; i < line.length; i++)
	{
		t = line[i][point.time] - line[i - 1][point.time];                  // milliseconds
		d = distance(line[i], line[i - 1]);                                 // miles
		line[i][point.speed] = (t > 0 && d > 0) ? d/(t/Enum.time.hour) : 0; // miles per hour
	}

	return line;
}

/**
 * Create route object
 * @param {Node} node
 * @returns {GeoJSON.Feature}
 */
function parseRoute(node)
{
	return {
		type: 'Feature',
		properties: getProperties(node),
		geometry: {
			type: 'LineString',
			coordinates: parseLine(node, 'rtept')
		}
	};
}

/**
 * Properties of a GPX node
 * @param {Node} node
 * @param {Object} [extras] Object literal of additional properties to set
 * @returns {Object}
 */
function getProperties(node, extras)
{
	var names = ['name', 'desc', 'author', 'copyright', 'link', 'time', 'keywords'];
	var properties = (extras) ? extras : {};

	for (var i = 0; i < names.length; i++)
	{
		var value = nodeValue(firstNode(node, names[i]));
		if (value) { properties[names[i]] = value; }
	}
	return properties;
}

// Simplify -------------------------------------------------------------------

/**
 * Simplification using Douglas-Peucker algorithm with recursion elimination
 * @param {Array} points
 * @returns {Array}
 */
function simplifyTrack(points)
{
	var len = points.length,
		keep = new Uint8Array(len),
		// convert tolerance in feet to tolerance in geographic degrees
		tolerance = setting.map.maxDeviationFeet / Enum.distance.equator,
		first = 0,
		last = len - 1,
		stack = [],
		newPoints = [],
		i, maxDistance, distance, index;

	keep[first] = keep[last] = 1;   // keep the end-points

	while (last)
	{
		maxDistance = 0;

		for (i = first + 1; i < last; i++)
		{
			distance = pointLineDistance(points[i], points[first], points[last]);

			if (distance > maxDistance)
			{
				index = i;
				maxDistance = distance;
			}
		}

		if (maxDistance > tolerance)
		{
			keep[index] = 1;    // keep the deviant point
			stack.push(first, index, index, last);
		}

		last = stack.pop();
		first = stack.pop();
	}

	for (i = 0; i < len; i++) {	if (keep[i]) { newPoints.push(points[i]); }	}

	return newPoints;
}

/**
 * Shortest distance from a point to a segment
 * @param {number[]} p
 * @param {number[]} p1
 * @param {number[]} p2
 * @returns {number}
 */
function pointLineDistance(p, p1, p2)
{
	var x = p1[0],
		y = p1[1],
		dx = p2[0] - x,
		dy = p2[1] - y;

	if (dx !== 0 || dy !== 0)
	{
		var t = ((p[0] - x) * dx + (p[1] - y) * dy) / (dx * dx + dy * dy);

		if (t > 1)
		{
			x = p2[0];
			y = p2[1];
		}
		else if (t > 0)
		{
			x += dx * t;
			y += dy * t;
		}
	}

	dx = p[0] - x;
	dy = p[1] - y;

	return dx * dx + dy * dy;
}
{'fraction_non_alphanumeric': 0.19493339157021183, 'fraction_numerical': 0.01288490936885783, 'mean_word_length': 3.076923076923077, 'pattern_counts': {'":': 0, '<': 32, '<?xml version=': 0, '>': 18, 'https://': 4, 'lorem ipsum': 0, 'www.': 2, 'xml': 12}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7202577', 'n_tokens_mistral': 8917, 'n_tokens_neox': 8224, 'n_words': 2076}