<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { GeoJSON, Tooltip } from 'react-leaflet';
import ElevationChartActivePoint from 'fm3/components/ElevationChartActivePoint';
import RichMarker, { createMarkerIcon } from 'fm3/components/RichMarker';
import { getMapLeafletElement } from 'fm3/leafletElementHolder';
import turfLineSlice from '@turf/line-slice';
import turfLineDistance from '@turf/line-distance';
import LeafletHotline from 'leaflet-hotline'; // eslint-disable-line

import { distance, smoothElevations } from 'fm3/geoutils';

const oneDecimalDigitNumberFormat = Intl.NumberFormat('sk', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
const timeFormat = new Intl.DateTimeFormat('sk', { hour: 'numeric', minute: '2-digit' });

class TrackViewerResult extends React.Component {
  static propTypes = {
    trackGeojson: PropTypes.shape({}),
    startPoints: PropTypes.arrayOf(PropTypes.shape({
      lat: PropTypes.number.isRequired,
      lon: PropTypes.number.isRequired,
      startTime: PropTypes.string,
    })),
    finishPoints: PropTypes.arrayOf(PropTypes.shape({
      lat: PropTypes.number.isRequired,
      lon: PropTypes.number.isRequired,
      lengthInKm: PropTypes.number.isRequired,
      finishTime: PropTypes.string,
    })),
    displayingElevationChart: PropTypes.bool,
    colorizeTrackBy: PropTypes.oneOf(['elevation', 'steepness']),
    eleSmoothingFactor: PropTypes.number.isRequired,
  }

  state = {
    infoLat: undefined,
    infoLon: undefined,
    infoDistanceKm: undefined,
  }

  componentWillReceiveProps(nextProps) {
    const userTurnedOnEleProfile = !this.props.displayingElevationChart && nextProps.displayingElevationChart;
    const userTurnedOffEleProfile = this.props.displayingElevationChart && !nextProps.displayingElevationChart;
    const userToggledColorizeTrackBy = this.props.colorizeTrackBy !== nextProps.colorizeTrackBy;
    if (userTurnedOnEleProfile) {
      this.showColorizedEleTrackOnMap(nextProps.colorizeTrackBy);
    } else if (userTurnedOffEleProfile) {
      this.removeColorizedEleTrackFromMap();
    } else if (userToggledColorizeTrackBy) {
      this.removeColorizedEleTrackFromMap();
      this.showColorizedEleTrackOnMap(nextProps.colorizeTrackBy);
    }
  }

  componentWillUnmount() {
    this.removeColorizedEleTrackFromMap();
  }
  // we keep here only business logic which needs access to the layer (otherwise use trackViewerLogic)
  onEachFeature = (feature, layer) => {
    if (feature.geometry.type === 'Point' && feature.properties.name) {
      layer.bindTooltip(feature.properties.name, { direction: 'right', className: 'compact' });
    }

    if (feature.geometry.type === 'LineString') {
      layer.on('click', (e) => { this.showInfoPoint(e, feature); });
      layer.on('mouseover', (e) => { this.showInfoPoint(e, feature); });
      layer.on('mouseout', () => {
        this.setState({ infoLat: undefined, infoLon: undefined, infoDistanceKm: undefined });
      });
    }
  };

  colorLineDataForElevation = () => {
    const firstRealFeature = this.props.trackGeojson.features[0]; // eslint-disable-line
    const latLonSmoothEles = smoothElevations(firstRealFeature, this.props.eleSmoothingFactor);
    const eles = latLonSmoothEles.map(lonLatEle => lonLatEle[2]);
    const maxEle = Math.max(...eles);
    const minEle = Math.min(...eles);
    return latLonSmoothEles.map((latLonEle) => {
      const color = (latLonEle[2] - minEle) / (maxEle - minEle);
      return [latLonEle[0], latLonEle[1], color];
    });
  }

  colorLineDataForSteepness = () => {
    const firstRealFeature = this.props.trackGeojson.features[0]; // eslint-disable-line
    const latLonSmoothEles = smoothElevations(firstRealFeature, this.props.eleSmoothingFactor);
    let prevLatLonEle = latLonSmoothEles[0];
    return latLonSmoothEles.map((latLonEle) => {
      const lat = latLonEle[0];
      const lon = latLonEle[1];
      const ele = latLonEle[2];
      const d = distance(lat, lon, prevLatLonEle[0], prevLatLonEle[1]);
      let angle = 0;
      if (d > 0) {
        angle = (ele - prevLatLonEle[2]) / d;
      }
      prevLatLonEle = latLonEle;
      const color = angle / 0.5 + 0.5;
      return [lat, lon, color];
    });
  }

  showColorizedEleTrackOnMap = (colorizeTrackBy) => {
    let colorLineData;
    let palette;
    if (colorizeTrackBy === 'elevation') {
      colorLineData = this.colorLineDataForElevation();
      palette = { 0.0: 'green', 0.5: 'yellow', 1.0: 'red' };
    } else if (colorizeTrackBy === 'steepness') {
      colorLineData = this.colorLineDataForSteepness();
      palette = { 0.0: 'green', 0.5: 'white', 1.0: 'red' };
    }
    const line = L.hotline(colorLineData, { weight: 4, palette });
    line.isColorizedElePath = true;
    line.addTo(getMapLeafletElement());
  }

  removeColorizedEleTrackFromMap = () => {
    const map = getMapLeafletElement();
    map.eachLayer((layer) => {
      if (layer.isColorizedElePath) {
        map.removeLayer(layer);
      }
    });
  }

  showInfoPoint = (e, feature) => {
    const infoLat = e.latlng.lat;
    const infoLon = e.latlng.lng;
    const infoDistanceKm = this.computeInfoDistanceKm(infoLat, infoLon, feature);
    this.setState({ infoLat, infoLon, infoDistanceKm });
  }

  computeInfoDistanceKm = (infoLat, infoLon, geojsonLineString) => {
    const p1 = {
      type: 'Feature',
      geometry: { type: 'Point', coordinates: geojsonLineString.geometry.coordinates[0] },
    };
    const p2 = {
      type: 'Feature',
      geometry: { type: 'Point', coordinates: [infoLon, infoLat] },
    };
    const s = turfLineSlice(p1, p2, geojsonLineString);
    return turfLineDistance(s);
  }

  pointToLayer = (geoJsonPoint, latlng) => L.marker(latlng, { icon: createMarkerIcon() })

  render() {
    const { trackGeojson, startPoints, finishPoints, displayingElevationChart } = this.props;
    const keyToAssureProperRefresh = (JSON.stringify(trackGeojson) + displayingElevationChart).length; // otherwise GeoJSON will still display the first data
    return trackGeojson && (

        <GeoJSON
          data={trackGeojson}
          key={keyToAssureProperRefresh}
          onEachFeature={this.onEachFeature}
          style={{ weight: 6, opacity: displayingElevationChart ? 0 : 0.85 }}
          pointToLayer={this.pointToLayer}
        />

        {startPoints.map((p, i) => (
          <RichMarker
            faIcon="play"
            key={i}
            faIconLeftPadding="2px"
            color="#409a40"
            interactive={false}
            position={L.latLng(p.lat, p.lon)}
          >
            { p.startTime &&
              <Tooltip offset={new L.Point(9, -25)} direction="right" permanent>
                {timeFormat.format(new Date(p.startTime))}
              </Tooltip> }
          </RichMarker>
        ))}
        {finishPoints.map((p, i) => (
          <RichMarker
            faIcon="stop"
            key={i}
            faIconLeftPadding="2px"
            color="#d9534f"
            interactive={false}
            position={L.latLng(p.lat, p.lon)}
          >
            <Tooltip offset={new L.Point(9, -25)} direction="right" permanent>

                {p.finishTime ? timeFormat.format(new Date(p.finishTime)) : ''}
                {p.finishTime ? ', ' : ''}
                {oneDecimalDigitNumberFormat.format(p.lengthInKm)} km
            </Tooltip>
          </RichMarker>
        ))}

        {this.state.infoLat &&
        <RichMarker
          faIcon="info"
          faIconLeftPadding="2px"
          color="grey"
          interactive={false}
          position={L.latLng(this.state.infoLat, this.state.infoLon)}
        >
          <Tooltip className="compact" offset={new L.Point(9, -25)} direction="right" permanent>

              {oneDecimalDigitNumberFormat.format(this.state.infoDistanceKm)} km

          </Tooltip>
        </RichMarker>}

        <ElevationChartActivePoint />

    );
  }
}

export default connect(
  state => ({
    trackGeojson: state.trackViewer.trackGeojson,
    startPoints: state.trackViewer.startPoints,
    finishPoints: state.trackViewer.finishPoints,
    displayingElevationChart: state.elevationChart.trackGeojson !== null,
    colorizeTrackBy: state.trackViewer.colorizeTrackBy,
    eleSmoothingFactor: state.trackViewer.eleSmoothingFactor,
  }),
)(TrackViewerResult);
<commit_msg>fixed showing elevation chart<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { GeoJSON, Tooltip } from 'react-leaflet';
import ElevationChartActivePoint from 'fm3/components/ElevationChartActivePoint';
import RichMarker, { createMarkerIcon } from 'fm3/components/RichMarker';
import { getMapLeafletElement } from 'fm3/leafletElementHolder';
import turfLineSlice from '@turf/line-slice';
import turfLineDistance from '@turf/line-distance';
import LeafletHotline from 'leaflet-hotline'; // eslint-disable-line

import { distance, smoothElevations } from 'fm3/geoutils';

const oneDecimalDigitNumberFormat = Intl.NumberFormat('sk', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
const timeFormat = new Intl.DateTimeFormat('sk', { hour: 'numeric', minute: '2-digit' });

class TrackViewerResult extends React.Component {
  static propTypes = {
    trackGeojson: PropTypes.shape({}),
    startPoints: PropTypes.arrayOf(PropTypes.shape({
      lat: PropTypes.number.isRequired,
      lon: PropTypes.number.isRequired,
      startTime: PropTypes.string,
    })),
    finishPoints: PropTypes.arrayOf(PropTypes.shape({
      lat: PropTypes.number.isRequired,
      lon: PropTypes.number.isRequired,
      lengthInKm: PropTypes.number.isRequired,
      finishTime: PropTypes.string,
    })),
    displayingElevationChart: PropTypes.bool,
    colorizeTrackBy: PropTypes.oneOf(['elevation', 'steepness']),
    eleSmoothingFactor: PropTypes.number.isRequired,
  }

  state = {
    infoLat: undefined,
    infoLon: undefined,
    infoDistanceKm: undefined,
  }

  componentWillReceiveProps(nextProps) {
    if (!nextProps.trackGeojson) {
      return;
    }

    const userTurnedOnEleProfile = !this.props.displayingElevationChart && nextProps.displayingElevationChart;
    const userTurnedOffEleProfile = this.props.displayingElevationChart && !nextProps.displayingElevationChart;
    const userToggledColorizeTrackBy = this.props.colorizeTrackBy !== nextProps.colorizeTrackBy;
    if (userTurnedOnEleProfile) {
      this.showColorizedEleTrackOnMap(nextProps.colorizeTrackBy);
    } else if (userTurnedOffEleProfile) {
      this.removeColorizedEleTrackFromMap();
    } else if (userToggledColorizeTrackBy) {
      this.removeColorizedEleTrackFromMap();
      this.showColorizedEleTrackOnMap(nextProps.colorizeTrackBy);
    }
  }

  componentWillUnmount() {
    this.removeColorizedEleTrackFromMap();
  }
  // we keep here only business logic which needs access to the layer (otherwise use trackViewerLogic)
  onEachFeature = (feature, layer) => {
    if (feature.geometry.type === 'Point' && feature.properties.name) {
      layer.bindTooltip(feature.properties.name, { direction: 'right', className: 'compact' });
    }

    if (feature.geometry.type === 'LineString') {
      layer.on('click', (e) => { this.showInfoPoint(e, feature); });
      layer.on('mouseover', (e) => { this.showInfoPoint(e, feature); });
      layer.on('mouseout', () => {
        this.setState({ infoLat: undefined, infoLon: undefined, infoDistanceKm: undefined });
      });
    }
  };

  colorLineDataForElevation = () => {
    const firstRealFeature = this.props.trackGeojson.features[0]; // eslint-disable-line
    const latLonSmoothEles = smoothElevations(firstRealFeature, this.props.eleSmoothingFactor);
    const eles = latLonSmoothEles.map(lonLatEle => lonLatEle[2]);
    const maxEle = Math.max(...eles);
    const minEle = Math.min(...eles);
    return latLonSmoothEles.map((latLonEle) => {
      const color = (latLonEle[2] - minEle) / (maxEle - minEle);
      return [latLonEle[0], latLonEle[1], color || 0];
    });
  }

  colorLineDataForSteepness = () => {
    const firstRealFeature = this.props.trackGeojson.features[0]; // eslint-disable-line
    const latLonSmoothEles = smoothElevations(firstRealFeature, this.props.eleSmoothingFactor);
    let prevLatLonEle = latLonSmoothEles[0];
    return latLonSmoothEles.map((latLonEle) => {
      const lat = latLonEle[0];
      const lon = latLonEle[1];
      const ele = latLonEle[2];
      const d = distance(lat, lon, prevLatLonEle[0], prevLatLonEle[1]);
      let angle = 0;
      if (d > 0) {
        angle = (ele - prevLatLonEle[2]) / d;
      }
      prevLatLonEle = latLonEle;
      const color = angle / 0.5 + 0.5;
      return [lat, lon, color || 0];
    });
  }

  showColorizedEleTrackOnMap = (colorizeTrackBy) => {
    let colorLineData;
    let palette;
    if (colorizeTrackBy === 'elevation') {
      colorLineData = this.colorLineDataForElevation();
      palette = { 0.0: 'green', 0.5: 'yellow', 1.0: 'red' };
    } else if (colorizeTrackBy === 'steepness') {
      colorLineData = this.colorLineDataForSteepness();
      palette = { 0.0: 'green', 0.5: 'white', 1.0: 'red' };
    }
    const line = L.hotline(colorLineData, { weight: 4, palette });
    line.isColorizedElePath = true;
    line.addTo(getMapLeafletElement());
  }

  removeColorizedEleTrackFromMap = () => {
    const map = getMapLeafletElement();
    map.eachLayer((layer) => {
      if (layer.isColorizedElePath) {
        map.removeLayer(layer);
      }
    });
  }

  showInfoPoint = (e, feature) => {
    const infoLat = e.latlng.lat;
    const infoLon = e.latlng.lng;
    const infoDistanceKm = this.computeInfoDistanceKm(infoLat, infoLon, feature);
    this.setState({ infoLat, infoLon, infoDistanceKm });
  }

  computeInfoDistanceKm = (infoLat, infoLon, geojsonLineString) => {
    const p1 = {
      type: 'Feature',
      geometry: { type: 'Point', coordinates: geojsonLineString.geometry.coordinates[0] },
    };
    const p2 = {
      type: 'Feature',
      geometry: { type: 'Point', coordinates: [infoLon, infoLat] },
    };
    const s = turfLineSlice(p1, p2, geojsonLineString);
    return turfLineDistance(s);
  }

  pointToLayer = (geoJsonPoint, latlng) => L.marker(latlng, { icon: createMarkerIcon() })

  render() {
    const { trackGeojson, startPoints, finishPoints, displayingElevationChart } = this.props;
    const keyToAssureProperRefresh = (JSON.stringify(trackGeojson) + displayingElevationChart).length; // otherwise GeoJSON will still display the first data
    return trackGeojson && (

        <GeoJSON
          data={trackGeojson}
          key={keyToAssureProperRefresh}
          onEachFeature={this.onEachFeature}
          style={{ weight: 6, opacity: displayingElevationChart ? 0 : 0.85 }}
          pointToLayer={this.pointToLayer}
        />

        {startPoints.map((p, i) => (
          <RichMarker
            faIcon="play"
            key={i}
            faIconLeftPadding="2px"
            color="#409a40"
            interactive={false}
            position={L.latLng(p.lat, p.lon)}
          >
            { p.startTime &&
              <Tooltip offset={new L.Point(9, -25)} direction="right" permanent>
                {timeFormat.format(new Date(p.startTime))}
              </Tooltip> }
          </RichMarker>
        ))}
        {finishPoints.map((p, i) => (
          <RichMarker
            faIcon="stop"
            key={i}
            faIconLeftPadding="2px"
            color="#d9534f"
            interactive={false}
            position={L.latLng(p.lat, p.lon)}
          >
            <Tooltip offset={new L.Point(9, -25)} direction="right" permanent>

                {p.finishTime ? timeFormat.format(new Date(p.finishTime)) : ''}
                {p.finishTime ? ', ' : ''}
                {oneDecimalDigitNumberFormat.format(p.lengthInKm)} km
            </Tooltip>
          </RichMarker>
        ))}

        {this.state.infoLat &&
        <RichMarker
          faIcon="info"
          faIconLeftPadding="2px"
          color="grey"
          interactive={false}
          position={L.latLng(this.state.infoLat, this.state.infoLon)}
        >
          <Tooltip className="compact" offset={new L.Point(9, -25)} direction="right" permanent>

              {oneDecimalDigitNumberFormat.format(this.state.infoDistanceKm)} km

          </Tooltip>
        </RichMarker>}

        <ElevationChartActivePoint />

    );
  }
}

export default connect(
  state => ({
    trackGeojson: state.trackViewer.trackGeojson,
    startPoints: state.trackViewer.startPoints,
    finishPoints: state.trackViewer.finishPoints,
    displayingElevationChart: state.elevationChart.trackGeojson !== null,
    colorizeTrackBy: state.trackViewer.colorizeTrackBy,
    eleSmoothingFactor: state.trackViewer.eleSmoothingFactor,
  }),
)(TrackViewerResult);
<|endoftext|>


<commit_before>// math utility functions

// Aliases for sylvester functions to more familiar names, also adding a sqrMagnitude function
Vector.prototype.sqrMagnitude = function() {
	// inner product with itself is sqrmagnitude
	return this.dot(this);
}
Vector.prototype.magnitude = Vector.prototype.modulus;
Vector.prototype.normalize = Vector.prototype.toUnitVector;

// Quaternion type
function Quaternion(inQv, inQs) {
	this.qv = inQv || Vector.Zero(3);
	this.qs = inQs || 0.0;
}
Quaternion.prototype = Vector.prototype;
Quaternion.prototype = {
	copy : function() {
		return new Quaternion(this.qv, this.qs);
	},
	normalize : function() {
		// normalize the quaternion in place and return the normalized quaternion
		var m = this.magnitude();
		this.qv = this.qv.x(1.0 / m);
		this.qs = this.qs / m;
		return this;
	},
	normalized : function() {
		// return the normalized quaternion without modifying the original
		var newQuat = this.copy();
		return newQuat.normalize();
	},
	sqrMagnitude : function() {
		return this.qv.dot(this.qv) + (this.qs * this.qs);
	},
	magnitude : function() {
		return Math.sqrt(this.sqrMagnitude());
	},
	mul : function(other) {
		// multiply 2 quaternions
		// [(psqv + qspv + pv cross qv), (psqs - pv dot qv)]
		// this is p, other is q
		var newQv = other.qv.x(this.qs).add(this.qv.x(other.qs)).add(this.qv.cross(other.qv));
		var newQs = this.qs * other.qv - this.qv.dot(other.qv);
		return new Quaternion(newQv, newQs);
	},
	multiply : this.mul,
	x : this.mul,
	scale : function(k) {
		// multiply by a scalar
		return new Quaternion(this.qv.mul(k), this.qs * k);
	},
	asVector : function() {
		var tmp = new Vector(this.qv);
		tmp.elements.push(qs);
	},
	fromVector : function(vec) {
		// [qv qs]
		this.qv = new Vector(vec.e(0), vec.e(1), vec.e(2));
		this.qs = vec.e(3);
	},
	q0 : function(val) {
		if (val) {
			this.qv[0] = val;
		}
		return this.qv[0];
	},
	q1 : function(val) {
		if (val) {
			this.qv[1] = val;
		}
		return this.qv[1];
	},
	q2 : function(val) {
		if (val) {
			this.qv[2] = val;
		}
		return this.qv[2];
	},
	q3 : function(val) {
		if (val) {
			this.qs = val;
		}
		return this.qs;
	},
	slerp : function(inQuat, beta) {
		// TODO
	},
	conjugate : function() {
		return new Quaternion(this.qv * -1, this.qs);
	},
	inverse : function() {
		var qstar = this.conjugate();
		return qstar.scale(this.sqrMagnitude());
	},
	rotate : function(v) {
		// rotate a vector v using this quaternion
		// TODO
	},
}

function QuaternionToEuler(quat) {
	var q0 = quat.q0(),
		q1 = quat.q1(),
		q2 = quat.q2(),
		q3 = quat.q3();
	return new Vector([
		Math.atan2((2 * (q0 * q1 + q2 * q3)), (1 - 2(q1 * q1 + q2 * q2))),
		Math.asin(2 * (q0 * q2 - q3 * q1)),
		Math.atan2(2 * (q0 * q3 + q1 * q2), 1 - 2 * (q2 * q2 + q3 * q3))
	]);
}

function EulerToQuaternion(euler) {
	var x = euler[0] / 2;
	var y = euler[1] / 2;
	var z = euler[2] / 2;

	var q = [
		Math.cos(x) * Math.cos(y) * Math.cos(z) + Math.sin(x) * Math.sin(y) + Math.sin(z),
		Math.sin(x) * Math.cos(y) * Math.cos(z) - Math.cos(x) * Math.sin(y) + Math.sin(z),
		Math.cos(x) * Math.sin(y) * Math.cos(z) - Math.sin(x) * Math.cos(y) + Math.sin(z),
		Math.sin(x) * Math.cos(y) * Math.sin(z) - Math.sin(x) * Math.sin(y) + Math.cos(z)
	];
	var quat = new Quaternion();
	quat.fromVector(q);
	return quat;
}

function QuaternionToMatrix(quat) {
	var x = quat.q0();
	var y = quat.q1();
	var z = quat.q2();
	var w = quat.q3();

	return new Matrix(	[1 - 2*y*y - 2*z*z, 2*x*y + 2*z*w, 2*x*z - 2*y*w],
						[2*x*y - 2*z*w, 1 - 2*x*x - 2*z*z, 2*y*z + 2*x*w],
						[2*x*z - 2*y*w, 2*y*z - 2*x*w, 1 - 2*x*x - 2*y*y]);
}

function MatrixToQuaternion(mat) {
	// adapted from Game Engine Architecture by Jason Gregory
	var q = [0, 0, 0, 0];
	var trace = mat.e(0,0) + mat.e(1,1) + mat.e(2,2);

	// check diagonal
	if (trace > 0.0) {
		var s = Math.sqrt(trace + 1.0);
		q[3] = s * 0.5;

		var t = 0.5 / s;
		q[0] = (mat.e(2,1) - mat.e(1,2)) * t;
		q[1] = (mat.e(0,2) - mat.e(2,0)) * t;
		q[2] = (mat.e(1,0) - mat.e(0,1)) * t;
	}
	else {
		// diagonal is negative
		var i = 0;
		if (mat.e(1,1) > mat.e(0,0)) {
			i = 1;
		}
		if (mat.e(2,2) > mat.e(i,i)) {
			i = 2;
		}
		var NEXT = [1, 2, 0];
		var j = NEXT[i];
		var k = NEXT[j];

		var s = Math.sqrt((mat.e(i,i) - (mat.e(j,j) + mat.(k,k))) + 1.0);

		q[i] = s * 0.5;

		var t = s;
		if (s != 0.0) {
			t = 0.5 / s;
		}

		q[3] = (mat.e(k,j) - mat.e(j,k)) * t;
		q[j] = (mat.e(j,i) + mat.e(i,j)) * t;
		q[k] = (mat.e(k,i) + mat.e(i,k)) * t;
	}

	return new Quaternion(new Vector([q[0], q[1], q[2]]), q[3]);
}

function AxisAngleToQuaternion(axis, angle) {
	var qv = axis.x(Math.sin(angle / 2));
	var qs = Math.cos(angle / 2);
	return new Quaternion(qv, qs);
}

Quaternion.prototype.ToEuler = function() { return QuaternionToEuler(this); };
Quaternion.prototype.ToMatrix = function() { return QuaternionToMatrix(this); };

function TriangulatePolygon(vertArray, ccw=true) {
	// triangulate a hole-less polygon given vertex set
	// function assumes that the input is a polygon to be split into triangles and that the vertices are in CCW or CW order as opposed to vertex soup
	// code adapted from Real-Time Collision Detection by Christer Ericson, Chapter 12 - Geometrical robustness pg 498 (ear-clipping triangulation algorithm)

	var indexArray = [];

	// utility arrays to allow usage of array as a circular doubly-linked linked list
	// immediately called anonymous functions to generate the arrays
	var ll_prev = (function() {
		var retval = [];
		for (var i = 0; i < vertArray.length; ++i) {
			retval[i] = i - 1;
		}
		retval[0] = vertArray.length - 1;
		return retval;
	})();
	var ll_next = (function() {
		var retval = [];
		for (var i = 0; i < vertArray.length; ++i) {
			retval[i] = i % vertArray.length;
		}
	})();

	// set up variables for the loop
	var idx = 0;
	var nVerts = vertArray.length;
	// remove vertices until only a triangle is left
	while (nVerts > 3) {
		var isEar = true;

		// temporary triangle
		var tmp_face = new Face();
		tmp_face.indices = $V([ll_prev[idx], idx, ll_next[idx]]);
		tmp_face.normal = GetFaceNormal(vertArray[ll_prev[idx]], vertArray[idx], vertArray[ll_next[idx]]);

		// test if ear is convex
		if (TestTriangleWinding(vertArray[tmp_face.indices.e(1)].position, vertArray[tmp_face.indices.e(2)].position, vertArray[tmp_face.indices.e(3)].position, ccw)) {
			// test if there is another vertex inside this triangle
			var k = ll_next[ll_next[idx]];
			do {
				if (PointTriangleIntersection(vertArray[k].position, vertArray[ll_prev(idx)].position, vertArray[idx].position, vertArray[ll_next[idx]].position)) {
					isEar = false;
					break;
				}
				k = ll_next(k);
			} while (k != ll_prev(idx));
		}
		else {
			// triangle is incorrect winding, therefore not an ear
			isEar = false;
		}
	}
	if (isEar) {
		// add this triangle to the index array

		indexArray.push(oface);
		// remove vertex idx by changing prev/next links
		next[prev[idx]] = next[idx];
		prev[next[idx]] = prev[idx];
		--nVerts;
		// visit the previous vertex next
		idx = prev[idx];
	}
	else {
		// move on to next vertex to find ear
		idx = next[idx];
	}
	return indexArray;
}

function GetFaceNormal(a, b, c) {
	// find normal to a triangle by taking the cross product of two of the sides
	var ab = b.position.sub(a.position);
	var ac = c.position.sub(a.position);
	var onorm = ab.cross(ac);

	// the average is the face norm for lighting, but this isn't the physics normal to the face necessarily
	var avgnorm = a.normal.add(b.normal).add(c.normal).map(function(x) {return x / 3.0;});

	// test if they're pointing the same or opposite ways using the dot product
	var ndot = onorm.dot(avgnorm);

	// negate the normal if the dot product is negative (the two normals point different directions)
	return onorm.multiply(ndot < 0 ? -1 : 1);
}

function TestTriangleWinding(norm, a, b, c, ccw=true) {
	// if RH rule then it's CCW, if it's LH rule then CW
	// world and object space for OpenGL (WebGL) is RH by default
	// if we check this against the normal i think there's no need to specify specifically CCW or CW,
	// it just comes out pointing in the direction you specify or not and tells you if this is correct
	// for your coordinate system
	var ab = b.position.sub(a.position);
	var ac = c.position.sub(a.position);
	var onorm = ab.cross(ac);
	return (norm.dot(onorm) > 0);
}

function PointTriangleIntersection(pt, a, b, c, ccw=true) {

}

function VertexWindingOrder(vertArray) {
	// given a set of vertices in a mesh, get a winding order

}<commit_msg>adding slerp function, increasing the flexibility of constructor and conversion functions<commit_after>// math utility functions

// Aliases for sylvester functions to more familiar names, also adding a sqrMagnitude function
Vector.prototype.sqrMagnitude = function() {
	// inner product with itself is sqrmagnitude
	return this.dot(this);
}
Vector.prototype.magnitude = Vector.prototype.modulus;
Vector.prototype.normalize = Vector.prototype.toUnitVector;

// Quaternion type
function Quaternion(inQv, inQs) {
	if (inQv instanceof Vector) {
		if (inQv.dimensions() > 3) {
			this.qv = new Vector(inQv.elements.slice(0,3));
			this.qs = inQv.elements[4];
		}
		else {
			this.qv = inQv || Vector.Zero(3);
			this.qs = inQs || 0.0;
		}
	}
	else {
		if (inQv.length > 3) {
			// grab the first 4 of the input array as qv, qs
			// allows constructing as new Quaternion([x, y, z, w])
			this.qv = new Vector(inQv.slice(0, 3));
			this.qs = inQv[3];
		}
		else {
			// make a vector out of the input array
			this.qv = new Vector(inQv);
			this.qs = inQs;
		}
	}
}
Quaternion.prototype = Vector.prototype;
Quaternion.prototype = {
	copy : function() {
		return new Quaternion(this.qv, this.qs);
	},
	normalize : function() {
		// normalize the quaternion in place and return the normalized quaternion
		var m = this.magnitude();
		this.qv = this.qv.x(1.0 / m);
		this.qs = this.qs / m;
		return this;
	},
	normalized : function() {
		// return the normalized quaternion without modifying the original
		var newQuat = this.copy();
		return newQuat.normalize();
	},
	sqrMagnitude : function() {
		return this.qv.dot(this.qv) + (this.qs * this.qs);
	},
	magnitude : function() {
		return Math.sqrt(this.sqrMagnitude());
	},
	mul : function(other) {
		// multiply 2 quaternions
		// [(psqv + qspv + pv cross qv), (psqs - pv dot qv)]
		// this is p, other is q
		var newQv = other.qv.x(this.qs).add(this.qv.x(other.qs)).add(this.qv.cross(other.qv));
		var newQs = this.qs * other.qv - this.qv.dot(other.qv);
		return new Quaternion(newQv, newQs);
	},
	multiply : this.mul,
	x : this.mul,
	scale : function(k) {
		// multiply by a scalar
		return new Quaternion(this.qv.mul(k), this.qs * k);
	},
	asVector : function() {
		var tmp = new Vector(this.qv);
		tmp.elements.push(qs);
		return tmp;
	},
	fromVector : function(vec) {
		// [qv qs]
		if (vec instanceof Vector) {
			this.qv = new Vector([vec.e(0), vec.e(1), vec.e(2)]);
			this.qs = vec.e(3);
		}
		else {
			this.qv = new Vector([vec[0], vec[1], vec[2]]);
			this.qs = vec[3];
		}
		return this;
	},
	q0 : function(val) {
		if (val) {
			this.qv[0] = val;
		}
		return this.qv[0];
	},
	q1 : function(val) {
		if (val) {
			this.qv[1] = val;
		}
		return this.qv[1];
	},
	q2 : function(val) {
		if (val) {
			this.qv[2] = val;
		}
		return this.qv[2];
	},
	q3 : function(val) {
		if (val) {
			this.qs = val;
		}
		return this.qs;
	},
	slerp : function(inQuat, beta) {
		// TODO
		var p = this.asVector();
		var q = this.asVector();

		return new Quaternion(SLERP(p, q, beta));
	},
	conjugate : function() {
		return new Quaternion(this.qv * -1, this.qs);
	},
	inverse : function() {
		var qstar = this.conjugate();
		return qstar.scale(this.sqrMagnitude());
	},
	rotate : function(v) {
		// rotate a vector v using this quaternion
		// TODO
	},
}

function SLERP(p, q, beta) {
	var theta = Math.acos(p.dot(q));
	var wp = (Math.sin((1-beta) * theta)) / Math.sin(theta);
	var wq = Math.sin(beta * theta) / Math.sin(theta);

	return wp * p + wq * q;
}

function QuaternionToEuler(quat) {
	var q0 = quat.q0(),
		q1 = quat.q1(),
		q2 = quat.q2(),
		q3 = quat.q3();
	return new Vector([
		Math.atan2((2 * (q0 * q1 + q2 * q3)), (1 - 2(q1 * q1 + q2 * q2))),
		Math.asin(2 * (q0 * q2 - q3 * q1)),
		Math.atan2(2 * (q0 * q3 + q1 * q2), 1 - 2 * (q2 * q2 + q3 * q3))
	]);
}

function EulerToQuaternion(euler) {
	var x = euler[0] / 2;
	var y = euler[1] / 2;
	var z = euler[2] / 2;

	var q = [
		Math.cos(x) * Math.cos(y) * Math.cos(z) + Math.sin(x) * Math.sin(y) + Math.sin(z),
		Math.sin(x) * Math.cos(y) * Math.cos(z) - Math.cos(x) * Math.sin(y) + Math.sin(z),
		Math.cos(x) * Math.sin(y) * Math.cos(z) - Math.sin(x) * Math.cos(y) + Math.sin(z),
		Math.sin(x) * Math.cos(y) * Math.sin(z) - Math.sin(x) * Math.sin(y) + Math.cos(z)
	];
	return new Quaternion(q);
}

function QuaternionToMatrix(quat) {
	var x = quat.q0();
	var y = quat.q1();
	var z = quat.q2();
	var w = quat.q3();

	return new Matrix(	[1 - 2*y*y - 2*z*z, 2*x*y + 2*z*w, 2*x*z - 2*y*w],
						[2*x*y - 2*z*w, 1 - 2*x*x - 2*z*z, 2*y*z + 2*x*w],
						[2*x*z - 2*y*w, 2*y*z - 2*x*w, 1 - 2*x*x - 2*y*y]);
}

function MatrixToQuaternion(mat) {
	// adapted from Game Engine Architecture by Jason Gregory
	var q = [0, 0, 0, 0];
	var trace = mat.e(0,0) + mat.e(1,1) + mat.e(2,2);

	// check diagonal
	if (trace > 0.0) {
		var s = Math.sqrt(trace + 1.0);
		q[3] = s * 0.5;

		var t = 0.5 / s;
		q[0] = (mat.e(2,1) - mat.e(1,2)) * t;
		q[1] = (mat.e(0,2) - mat.e(2,0)) * t;
		q[2] = (mat.e(1,0) - mat.e(0,1)) * t;
	}
	else {
		// diagonal is negative
		var i = 0;
		if (mat.e(1,1) > mat.e(0,0)) {
			i = 1;
		}
		if (mat.e(2,2) > mat.e(i,i)) {
			i = 2;
		}
		var NEXT = [1, 2, 0];
		var j = NEXT[i];
		var k = NEXT[j];

		var s = Math.sqrt((mat.e(i,i) - (mat.e(j,j) + mat.(k,k))) + 1.0);

		q[i] = s * 0.5;

		var t = s;
		if (s != 0.0) {
			t = 0.5 / s;
		}

		q[3] = (mat.e(k,j) - mat.e(j,k)) * t;
		q[j] = (mat.e(j,i) + mat.e(i,j)) * t;
		q[k] = (mat.e(k,i) + mat.e(i,k)) * t;
	}

	return new Quaternion(new Vector([q[0], q[1], q[2]]), q[3]);
}

function AxisAngleToQuaternion(axis, angle) {
	var qv = axis.x(Math.sin(angle / 2));
	var qs = Math.cos(angle / 2);
	return new Quaternion(qv, qs);
}

Quaternion.prototype.ToEuler = function() { return QuaternionToEuler(this); };
Quaternion.prototype.ToMatrix = function() { return QuaternionToMatrix(this); };

function TriangulatePolygon(vertArray, ccw=true) {
	// triangulate a hole-less polygon given vertex set
	// function assumes that the input is a polygon to be split into triangles and that the vertices are in CCW or CW order as opposed to vertex soup
	// code adapted from Real-Time Collision Detection by Christer Ericson, Chapter 12 - Geometrical robustness pg 498 (ear-clipping triangulation algorithm)

	var indexArray = [];

	// utility arrays to allow usage of array as a circular doubly-linked linked list
	// immediately called anonymous functions to generate the arrays
	var ll_prev = (function() {
		var retval = [];
		for (var i = 0; i < vertArray.length; ++i) {
			retval[i] = i - 1;
		}
		retval[0] = vertArray.length - 1;
		return retval;
	})();
	var ll_next = (function() {
		var retval = [];
		for (var i = 0; i < vertArray.length; ++i) {
			retval[i] = i % vertArray.length;
		}
	})();

	// set up variables for the loop
	var idx = 0;
	var nVerts = vertArray.length;
	// remove vertices until only a triangle is left
	while (nVerts > 3) {
		var isEar = true;

		// temporary triangle
		var tmp_face = new Face();
		tmp_face.indices = $V([ll_prev[idx], idx, ll_next[idx]]);
		tmp_face.normal = GetFaceNormal(vertArray[ll_prev[idx]], vertArray[idx], vertArray[ll_next[idx]]);

		// test if ear is convex
		if (TestTriangleWinding(vertArray[tmp_face.indices.e(1)].position, vertArray[tmp_face.indices.e(2)].position, vertArray[tmp_face.indices.e(3)].position, ccw)) {
			// test if there is another vertex inside this triangle
			var k = ll_next[ll_next[idx]];
			do {
				if (PointTriangleIntersection(vertArray[k].position, vertArray[ll_prev(idx)].position, vertArray[idx].position, vertArray[ll_next[idx]].position)) {
					isEar = false;
					break;
				}
				k = ll_next(k);
			} while (k != ll_prev(idx));
		}
		else {
			// triangle is incorrect winding, therefore not an ear
			isEar = false;
		}
	}
	if (isEar) {
		// add this triangle to the index array

		indexArray.push(oface);
		// remove vertex idx by changing prev/next links
		next[prev[idx]] = next[idx];
		prev[next[idx]] = prev[idx];
		--nVerts;
		// visit the previous vertex next
		idx = prev[idx];
	}
	else {
		// move on to next vertex to find ear
		idx = next[idx];
	}
	return indexArray;
}

function GetFaceNormal(a, b, c) {
	// find normal to a triangle by taking the cross product of two of the sides
	var ab = b.position.sub(a.position);
	var ac = c.position.sub(a.position);
	var onorm = ab.cross(ac);

	// the average is the face norm for lighting, but this isn't the physics normal to the face necessarily
	var avgnorm = a.normal.add(b.normal).add(c.normal).map(function(x) {return x / 3.0;});

	// test if they're pointing the same or opposite ways using the dot product
	var ndot = onorm.dot(avgnorm);

	// negate the normal if the dot product is negative (the two normals point different directions)
	return onorm.multiply(ndot < 0 ? -1 : 1);
}

function TestTriangleWinding(norm, a, b, c, ccw=true) {
	// if RH rule then it's CCW, if it's LH rule then CW
	// world and object space for OpenGL (WebGL) is RH by default
	// if we check this against the normal i think there's no need to specify specifically CCW or CW,
	// it just comes out pointing in the direction you specify or not and tells you if this is correct
	// for your coordinate system
	var ab = b.position.sub(a.position);
	var ac = c.position.sub(a.position);
	var onorm = ab.cross(ac);
	return (norm.dot(onorm) > 0);
}

function PointTriangleIntersection(pt, a, b, c, ccw=true) {

}

function VertexWindingOrder(vertArray) {
	// given a set of vertices in a mesh, get a winding order

}<|endoftext|>


<commit_before>/* eslint-env node, mocha */

'use strict';
const request = require('supertest');
const should = require('should');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const exec = require('child_process').exec;
const Server = require('../lib/Server');
const DataStore = require('../lib/stores/DataStore');
const FileStore = require('../lib/stores/FileStore');
const File = require('../lib/models/File');
const TUS_RESUMABLE = require('../lib/constants').TUS_RESUMABLE;

const STORE_PATH = '/files';
const FILES_DIRECTORY = path.resolve(__dirname, `..${STORE_PATH}`);
const TEST_FILE_PATH = path.resolve(__dirname, 'test.mp4');
const TEST_FILE_SIZE = 960244;
const TEST_FILE_NAME = 'test_file.mp4';
const TEST_METADATA = 'some data, for you';

describe('FileStore', () => {
    let server;
    let created_file_name;
    let created_file_path;
    let deferred_file_name;
    let deferred_file_path;
    before(() => {
        const namingFunction = (req) => req.url.replace(/\//g, '-');
        server = new Server();
        server.datastore = new FileStore({
            path: STORE_PATH,
            namingFunction,
        });

        // Create the file used in getOffset
        exec(`touch ${FILES_DIRECTORY}/${TEST_FILE_NAME}`);
    });

    after(() => {
        // Remove the files directory
        exec(`rm -r ${FILES_DIRECTORY}`);
        // clear the config
        server.datastore.configstore.clear();
    });

    describe('constructor', () => {
        it('must inherit from Datastore', (done) => {
            assert.equal(server.datastore instanceof DataStore, true);
            done();
        });

        it('must have a create method', (done) => {
            server.datastore.should.have.property('create');
            done();
        });

        it('must have a write method', (done) => {
            server.datastore.should.have.property('write');
            done();
        });

        it('must have a getOffset method', (done) => {
            server.datastore.should.have.property('getOffset');
            done();
        });

        it('should create a directory for the files', (done) => {
            const stats = fs.lstatSync(FILES_DIRECTORY);
            assert.equal(stats.isDirectory(), true);
            done();
        });
    });

    describe('create', () => {
        it('should reject when the directory doesnt exist', () => {
            const file_store = new FileStore({ path: STORE_PATH });
            file_store.directory = 'some_new_path';
            return file_store.create(new File(1))
                    .should.be.rejected();
        });

        it('should resolve when the directory exists', () => {
            const file_store = new FileStore({ path: STORE_PATH });
            return file_store.create(new File('name.mp4'))
                    .should.be.fulfilled();
        });

        it('should return the file name', (done) => {
            let req = { headers: { 'upload-length': 1000 }, url: '/example/files' };
            filestore.create(req).then((file_id) => {
                assert.equal(file_id, '-example-files');
                done();
            });
        });
    });

    describe('write', () => {
        it('should reject write streams that cant be opened', () => {
            return server.datastore.write()
                    .should.be.rejectedWith(500);
        });

        it('should open a stream and resolve the new offset', (done) => {
            const file_store = new FileStore({ path: STORE_PATH });
            const write_stream = fs.createReadStream(TEST_FILE_PATH);
            write_stream.once('open', () => {
                file_store.write(write_stream, TEST_FILE_NAME, 0)
                .then((offset) => {
                    assert.equal(offset, TEST_FILE_SIZE);
                    return done();
                })
                .catch(done);
            });
        });
    });

    describe('getOffset', () => {
        it('should reject non-existant files', () => {
            const file_store = new FileStore({ path: STORE_PATH });
            return file_store.getOffset('doesnt_exist')
                    .should.be.rejectedWith(404);
        });

        it('should resolve the stats for existant files', () => {
            const file_store = new FileStore({ path: STORE_PATH });
            return file_store.getOffset(TEST_FILE_NAME)
                    .should.be.fulfilledWith(fs.statSync(`${FILES_DIRECTORY}/${TEST_FILE_NAME}`));
        });
    });

    describe('POST', () => {
        it('should create a file in the provided directory', (done) => {
            request(server.listen())
            .post(STORE_PATH)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .set('Upload-Length', 960244)
            .set('Upload-Metadata', TEST_METADATA)
            .expect(201)
            .end((err, res) => {
                created_file_name = res.headers.location.split('/').pop();
                created_file_path = path.resolve(FILES_DIRECTORY, created_file_name);
                assert.equal(fs.existsSync(created_file_path), true);
                done();
            });
        });

        it('should create a file with deferred length', (done) => {
            request(server.listen())
            .post(STORE_PATH)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .set('Upload-Defer-Length', 1)
            .expect(201)
            .end((err, res) => {
                deferred_file_name = res.headers.location.split('/').pop();
                deferred_file_path = path.resolve(FILES_DIRECTORY, deferred_file_name);
                assert.equal(fs.existsSync(deferred_file_path), true);
                done();
            });
        });

        it('should 404 paths without a file id', (done) => {
            request(server.listen())
            .head(`${STORE_PATH}/`)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .expect(404, done);
        });
    });

    describe('HEAD', () => {
        it('should return the starting offset and length', (done) => {
            request(server.listen())
            .head(`${STORE_PATH}/${created_file_name}`)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .expect(200)
            .end((err, res) => {
                assert.equal(res.headers['upload-offset'], 0);
                assert.equal(res.headers['upload-length'], TEST_FILE_SIZE);
                done();
            });
        });

        it('should return the updated offset and length', (done) => {
            const source = fs.createReadStream(TEST_FILE_PATH);
            const dest = fs.createWriteStream(created_file_path);
            source.pipe(dest);
            source.on('end', () => {
                request(server.listen())
                .head(`${STORE_PATH}/${created_file_name}`)
                .set('Tus-Resumable', TUS_RESUMABLE)
                .expect(200)
                .end((err, res) => {
                    assert.equal(res.headers['upload-offset'], TEST_FILE_SIZE);
                    assert.equal(res.headers['upload-length'], TEST_FILE_SIZE);
                    done();
                });
            });
        });

        it('should return the defer header for deferred files', (done) => {
            request(server.listen())
            .head(`${STORE_PATH}/${deferred_file_path}`)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .expect(200)
            .end((err, res) => {
                assert.equal(res.headers['upload-defer-length'], 1);
                assert.equal(res.headers['upload-length'], undefined);
                done();
            });
        });

        it('should return the upload metatata', (done) => {
            request(server.listen())
            .head(`${STORE_PATH}/${created_file_path}`)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .expect(200)
            .end((err, res) => {
                assert.equal(res.headers['upload-metadata'], TEST_METADATA);
                done();
            });
        });

        it('should 404 files that dont exist', (done) => {
            request(server.listen())
            .head(`${STORE_PATH}/a_file_that_doesnt_exist`)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .expect(404, done);
        });
    });
});
<commit_msg>Edits for create method<commit_after>/* eslint-env node, mocha */

'use strict';
const request = require('supertest');
const should = require('should');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const exec = require('child_process').exec;
const Server = require('../lib/Server');
const DataStore = require('../lib/stores/DataStore');
const FileStore = require('../lib/stores/FileStore');
const File = require('../lib/models/File');
const TUS_RESUMABLE = require('../lib/constants').TUS_RESUMABLE;

const STORE_PATH = '/files';
const FILES_DIRECTORY = path.resolve(__dirname, `..${STORE_PATH}`);
const TEST_FILE_PATH = path.resolve(__dirname, 'test.mp4');
const TEST_FILE_SIZE = 960244;
const TEST_FILE_NAME = 'test_file.mp4';
const TEST_METADATA = 'some data, for you';

describe.only('FileStore', () => {
    let server;
    let created_file_name;
    let created_file_path;
    let deferred_file_name;
    let deferred_file_path;

    before((done) => {
        server = new Server();
        server.datastore = new FileStore({
            path: STORE_PATH,
        });

        // Create the file used in getOffset
        exec(`touch ${FILES_DIRECTORY}/${TEST_FILE_NAME}`, (err) => {
            if (err) {
                return done(err);
            }

            return done();
        });
    });

    after((done) => {
        // Remove the files directory
        exec(`rm -r ${FILES_DIRECTORY}`, (err) => {
            if (err) {
                return done(err);
            }

            // clear the config
            server.datastore.configstore.clear();
            return done();
        });
    });

    describe('constructor', () => {
        it('must inherit from Datastore', (done) => {
            assert.equal(server.datastore instanceof DataStore, true);
            done();
        });

        it('must have a create method', (done) => {
            server.datastore.should.have.property('create');
            done();
        });

        it('must have a write method', (done) => {
            server.datastore.should.have.property('write');
            done();
        });

        it('must have a getOffset method', (done) => {
            server.datastore.should.have.property('getOffset');
            done();
        });

        it('should create a directory for the files', (done) => {
            const stats = fs.lstatSync(FILES_DIRECTORY);
            assert.equal(stats.isDirectory(), true);
            done();
        });
    });

    describe('create', () => {
        const req = { headers: { 'upload-length': 1000 }, url: STORE_PATH };

        it('should reject when the directory doesnt exist', () => {
            const file_store = new FileStore({ path: STORE_PATH });
            file_store.directory = 'some_new_path';
            return file_store.create(req)
                    .should.be.rejected();
        });

        it('should resolve when the directory exists', () => {
            const file_store = new FileStore({ path: STORE_PATH });
            return file_store.create(req)
                    .should.be.fulfilled();
        });

        it('should resolve to the File model', (done) => {
            const file_store = new FileStore({ path: STORE_PATH });
            file_store.create(req)
                .then((newFile) => {
                    assert.equal(newFile instanceof File, true);
                    return done();
                })
                .catch(done);
        });

        it('should use custom naming function when provided', (done) => {
            const namingFunction = (incomingReq) => incomingReq.url.replace(/\//g, '-');
            const file_store = new FileStore({ path: STORE_PATH, namingFunction });
            file_store.create(req)
                .then((newFile) => {
                    assert.equal(newFile.id, '-files');
                    return done();
                })
                .catch(done);
        });
    });

    describe('write', () => {
        it('should reject write streams that cant be opened', () => {
            return server.datastore.write()
                    .should.be.rejectedWith(500);
        });

        it('should open a stream and resolve the new offset', (done) => {
            const file_store = new FileStore({ path: STORE_PATH, directory: FILES_DIRECTORY });
            const write_stream = fs.createReadStream(TEST_FILE_PATH);
            write_stream.once('open', () => {
                file_store.write(write_stream, TEST_FILE_NAME, 0)
                .then((offset) => {
                    assert.equal(offset, TEST_FILE_SIZE);
                    return done();
                })
                .catch(done);
            });
        });
    });

    describe('getOffset', () => {
        it('should reject non-existant files', () => {
            const file_store = new FileStore({ path: STORE_PATH });
            return file_store.getOffset('doesnt_exist')
                    .should.be.rejectedWith(404);
        });

        it('should resolve the stats for existant files', () => {
            const file_store = new FileStore({ path: STORE_PATH });
            return file_store.getOffset(TEST_FILE_NAME)
                    .should.be.fulfilledWith(fs.statSync(`${FILES_DIRECTORY}/${TEST_FILE_NAME}`));
        });
    });

    describe('POST', () => {
        it('should create a file in the provided directory', (done) => {
            request(server.listen())
            .post(STORE_PATH)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .set('Upload-Length', 960244)
            .set('Upload-Metadata', TEST_METADATA)
            .expect(201)
            .end((err, res) => {
                created_file_name = res.headers.location.split('/').pop();
                created_file_path = path.resolve(FILES_DIRECTORY, created_file_name);
                assert.equal(fs.existsSync(created_file_path), true);
                done();
            });
        });

        it('should create a file with deferred length', (done) => {
            request(server.listen())
            .post(STORE_PATH)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .set('Upload-Defer-Length', 1)
            .expect(201)
            .end((err, res) => {
                deferred_file_name = res.headers.location.split('/').pop();
                deferred_file_path = path.resolve(FILES_DIRECTORY, deferred_file_name);
                assert.equal(fs.existsSync(deferred_file_path), true);
                done();
            });
        });

        it('should 404 paths without a file id', (done) => {
            request(server.listen())
            .head(`${STORE_PATH}/`)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .expect(404, done);
        });
    });

    describe('HEAD', () => {
        it('should return the starting offset and length', (done) => {
            request(server.listen())
            .head(`${STORE_PATH}/${created_file_name}`)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .expect(200)
            .end((err, res) => {
                assert.equal(res.headers['upload-offset'], 0);
                assert.equal(res.headers['upload-length'], TEST_FILE_SIZE);
                done();
            });
        });

        it('should return the updated offset and length', (done) => {
            const source = fs.createReadStream(TEST_FILE_PATH);
            const dest = fs.createWriteStream(created_file_path);
            source.pipe(dest);
            source.on('end', () => {
                request(server.listen())
                .head(`${STORE_PATH}/${created_file_name}`)
                .set('Tus-Resumable', TUS_RESUMABLE)
                .expect(200)
                .end((err, res) => {
                    assert.equal(res.headers['upload-offset'], TEST_FILE_SIZE);
                    assert.equal(res.headers['upload-length'], TEST_FILE_SIZE);
                    done();
                });
            });
        });

        it('should return the defer header for deferred files', (done) => {
            request(server.listen())
            .head(`${STORE_PATH}/${deferred_file_name}`)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .expect(200)
            .end((err, res) => {
                assert.equal(res.headers['upload-defer-length'], 1);
                assert.equal(res.headers['upload-length'], undefined);
                done();
            });
        });

        it('should return the upload metatata', (done) => {
            request(server.listen())
            .head(`${STORE_PATH}/${created_file_name}`)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .expect(200)
            .end((err, res) => {
                assert.equal(res.headers['upload-metadata'], TEST_METADATA);
                done();
            });
        });

        it('should 404 files that dont exist', (done) => {
            request(server.listen())
            .head(`${STORE_PATH}/a_file_that_doesnt_exist`)
            .set('Tus-Resumable', TUS_RESUMABLE)
            .expect(404, done);
        });
    });
});
<|endoftext|>


<commit_before>/**
 * breit-wheeler
 *
 * Copyright © 2016 Allen Smith <loranallensmith@github.com>. All rights reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE.txt file in the root directory of this source tree.
 */

import { expect } from 'chai';
import Input from '../src/input-text';

describe('Input', () => {

  describe('render', () => {

    it('Should render the correct element', () => {
      let myInput = new Input({}, []);
      expect(myInput.virtualElement.tagName).to.equal('INPUT')
    });

    it('Should have the correct default classes', () => {
      let myInput = new Input({}, []);
      expect(myInput.virtualElement.properties.className).to.include('form-control');
    });

    it('Should support multiple classes', () => {
      let myInput = new Input({ classNames: ['extra-class', 'another-class'] }, []);
      expect(myInput.virtualElement.properties.className).to.include('another-class');
      expect(myInput.virtualElement.properties.className).to.include('extra-class');
    });

    it('Should render the correct type', () => {
      let myInput = new Input({ type: 'text'}, []);
      expect(myInput.virtualElement.properties).to.contain.keys({ type: 'text' });
    });

    it('Should render the name', () => {
      let myInput = new Input({ name: 'my-input'}, []);
      expect(myInput.virtualElement.properties).to.contain.keys({ name: 'my-input' });
    });

  });

});
<commit_msg>Fix typo<commit_after>/**
 * breit-wheeler
 *
 * Copyright © 2016 Allen Smith <loranallensmith@github.com>. All rights reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE.txt file in the root directory of this source tree.
 */

import { expect } from 'chai';
import Input from '../src/input';

describe('Input', () => {

  describe('render', () => {

    it('Should render the correct element', () => {
      let myInput = new Input({}, []);
      expect(myInput.virtualElement.tagName).to.equal('INPUT')
    });

    it('Should have the correct default classes', () => {
      let myInput = new Input({}, []);
      expect(myInput.virtualElement.properties.className).to.include('form-control');
    });

    it('Should support multiple classes', () => {
      let myInput = new Input({ classNames: ['extra-class', 'another-class'] }, []);
      expect(myInput.virtualElement.properties.className).to.include('another-class');
      expect(myInput.virtualElement.properties.className).to.include('extra-class');
    });

    it('Should render the correct type', () => {
      let myInput = new Input({ type: 'text'}, []);
      expect(myInput.virtualElement.properties).to.contain.keys({ type: 'text' });
    });

    it('Should render the name', () => {
      let myInput = new Input({ name: 'my-input'}, []);
      expect(myInput.virtualElement.properties).to.contain.keys({ name: 'my-input' });
    });

  });

});
<|endoftext|>


<commit_before>var Settings = {
    //The url that points to team city
    teamCityUrl: 'http://tclive:8111',

    //The main branch to show the master build status on the right hand panel on the screen
    mainBranch: 'develop',

    //Proxy to handle the cross domain ajax request.
    // This will need to be hosted on the relevant server e.g. proxy-node.js on Node.js or proxy-aspnet.ashx on IIS
    // You could write your own proxy just so long as it is hosted form the same domain as the rest of the app
    proxy: 'proxy-aspnet.ashx?url=',

    //How often to call the TeamCity webservices and update the screen
    checkIntervalMs: 5000, //5000 ms = 5 seconds

    //How often to refresh the build image;
    buildImageIntervalMs: 3600000, //3600000 ms = 1 hour

    //use this to stop the screen from updating automatically e.g. if you manually refresh it.
    enableAutoUpdate: true,
}

//Allow the settings to be overridden by querystring parameters
//(url parameters are currently only treated as strings)
jQuery.extend(Settings, UrlParams);

//----------------------
// TEAM CITY URLS
//----------------------
//The url for the list of all builds on the left hand side of the screen
Settings.buildsUrl = Settings.proxy + Settings.teamCityUrl + '/guestAuth/app/rest/builds?locator=running:any,branch:branched:any,count:20';

//The url for the list of build types (used for mapping the build id (e.g. bt11) to the name (e.g. Website Tests)
Settings.buildTypesUrl = Settings.proxy + Settings.teamCityUrl + '/guestAuth/app/rest/buildTypes';

//The url for the status of the build on the main branch
Settings.buildStatusUrl = Settings.proxy + Settings.teamCityUrl + '/guestAuth/app/rest/builds/branch:' + Settings.mainBranch + ',running:any,canceled:any';<commit_msg>removed unused "develop"<commit_after>var Settings = {
    //The url that points to team city
    teamCityUrl: 'http://tclive:8111',

    //The main branch to show the master build status on the right hand panel on the screen
    mainBranch: '',

    //Proxy to handle the cross domain ajax request.
    // This will need to be hosted on the relevant server e.g. proxy-node.js on Node.js or proxy-aspnet.ashx on IIS
    // You could write your own proxy just so long as it is hosted form the same domain as the rest of the app
    proxy: 'proxy-aspnet.ashx?url=',

    //How often to call the TeamCity webservices and update the screen
    checkIntervalMs: 5000, //5000 ms = 5 seconds

    //How often to refresh the build image;
    buildImageIntervalMs: 3600000, //3600000 ms = 1 hour

    //use this to stop the screen from updating automatically e.g. if you manually refresh it.
    enableAutoUpdate: true,
}

//Allow the settings to be overridden by querystring parameters
//(url parameters are currently only treated as strings)
jQuery.extend(Settings, UrlParams);

//----------------------
// TEAM CITY URLS
//----------------------
//The url for the list of all builds on the left hand side of the screen
Settings.buildsUrl = Settings.proxy + Settings.teamCityUrl + '/guestAuth/app/rest/builds?locator=running:any,branch:branched:any,count:20';

//The url for the list of build types (used for mapping the build id (e.g. bt11) to the name (e.g. Website Tests)
Settings.buildTypesUrl = Settings.proxy + Settings.teamCityUrl + '/guestAuth/app/rest/buildTypes';

//The url for the status of the build on the main branch
Settings.buildStatusUrl = Settings.proxy + Settings.teamCityUrl + '/guestAuth/app/rest/builds/branch:' + Settings.mainBranch + ',running:any,canceled:any';<|endoftext|>


<commit_before>(function(){

    angular.module("ngImgur", []).provider("$imgur", function () {

        this.accessToken = null;
        this.apiBase = "https://api.imgur.com/3";

        this.$get = ['$q', '$http', function ($q, $http) {

            if(this.accessToken === null)
                throw new Error('An access token is required to create $imgur');

            var Imgur = function(accessToken) {
                this.accessToken = accessToken;
                this.apiBase = "https://api.imgur.com/3";
            };

            Imgur.prototype = {

                getAccessToken: function() {
                    return this.accessToken;
                },

                /*
                 * Request standard user information. If you need the username for the account that is logged in, it is returned in the request for an access token.
                 */
                getAccount: function(username) {

                    var url = this.apiBase + "/account/" + username;

                    return this._makeRequest(url, "GET");
                },

                /*
                 * Return the images the user has favorited in the gallery.
                 */
                getAccountGalleryFavorites: function(username) {

                    var url = this.apiBase + "/account/" + username + "/gallery_favorites";

                    return this._makeRequest(url, "GET");
                },

                /*
                 * Returns the users favorited images, only accessible if you're logged in as the user.
                 */
                getAccountFavorites: function(username) {

                    var url = this.apiBase + "/account/" + username + "/favorites";

                    return this._makeRequest(url, "GET");
                },

                /*
                 * Return the images a user has submitted to the gallery
                 */
                getAccountSubmissions: function(username) {

                    var url = this.apiBase + "/account/" + username + "/submissions";

                    return this._makeRequest(url, "GET");
                },

                /*
                 * Returns the account settings, only accessible if you're logged in as the user.
                 */
                getAccountSettings: function(username) {

                    var url = this.apiBase + "/account/" + username + "/settings";

                    return this._makeRequest(url, "GET");
                },

                /*
                 * Updates the account settings for a given user, the user must be logged in.
                 */
                setAccountSettings: function(username, params) {

                    var url = this.apiBase + "/account/" + username + "/settings"

                    return this._makeRequest(url, "POST", params);
                },

                /*
                 * Return all of the images associated with the account. You can page through the images by setting the page, this defaults to 0.
                 */
                getAccountImages: function(username, page) {

                    if(page === undefined) {
                        page = 0;
                    }

                    var url = this.apiBase + "/account/" + username + "/images/" + page;

                    return this._makeRequest(url, "GET");
                },

                /*
                 * Upload a new image.
                 */
                imageUpload: function(params) {

                    var url = this.apiBase + "/image";

                    return this._makeRequest(url, "POST", params);
                },

                /*
                 * Returns the images in the gallery.
                 */
                getGallery: function(section, sort, page, dateRange, showViral) {

                    var galleryEndpoint = this.apiBase + "/gallery";
                    if(section !== undefined) { galleryEndpoint += "/" + section; }
                    if(sort !== undefined) { galleryEndpoint += "/" + sort; }
                    if(dateRange !== undefined) { galleryEndpoint += "/" + dateRange; }
                    if(showViral !== undefined) { galleryEndpoint += "?showViral=" + showViral; }

                    return this._makeRequest(galleryEndpoint, "GET");
                },

                /*
                 * Get list of all conversations for the logged in user.
                 */
                getConversationList: function() {

                    var url = this.apiBase + "/conversations";

                    return this._makeRequest(url, "GET");
                },

                /**
                 * Executes a request with the given url, method, and optional
                 * params and returns a $q promise for the result
                 * @param {string} url - the url to access
                 * @param {string} method - the http method to use
                 * @param {?object} params - any parameters to pass
                 * @returns {Promise<object>} a promise for the result
                 */
                _makeRequest: function(url, method, params) {

                    var settings = {
                        method: method,
                        url: url,
                        headers: {
                            "Authorization": "Bearer " + this.accessToken
                        }
                    };

                    if(params) {
                        settings.params = params
                    }

                    var deferred = $q.defer();

                    $http(settings)
                        .success(function (result) {
                            deferred.resolve(result);
                        })
                        .error(function (error) {
                            deferred.reject(error);
                        });

                    return deferred.promise;
                }
            };

            return new Imgur(this.accessToken);
        }];
    });

})();
<commit_msg>forgot a semicolon<commit_after>(function(){

    angular.module("ngImgur", []).provider("$imgur", function () {

        this.accessToken = null;
        this.apiBase = "https://api.imgur.com/3";

        this.$get = ['$q', '$http', function ($q, $http) {

            if(this.accessToken === null)
                throw new Error('An access token is required to create $imgur');

            var Imgur = function(accessToken) {
                this.accessToken = accessToken;
                this.apiBase = "https://api.imgur.com/3";
            };

            Imgur.prototype = {

                getAccessToken: function() {
                    return this.accessToken;
                },

                /*
                 * Request standard user information. If you need the username for the account that is logged in, it is returned in the request for an access token.
                 */
                getAccount: function(username) {

                    var url = this.apiBase + "/account/" + username;

                    return this._makeRequest(url, "GET");
                },

                /*
                 * Return the images the user has favorited in the gallery.
                 */
                getAccountGalleryFavorites: function(username) {

                    var url = this.apiBase + "/account/" + username + "/gallery_favorites";

                    return this._makeRequest(url, "GET");
                },

                /*
                 * Returns the users favorited images, only accessible if you're logged in as the user.
                 */
                getAccountFavorites: function(username) {

                    var url = this.apiBase + "/account/" + username + "/favorites";

                    return this._makeRequest(url, "GET");
                },

                /*
                 * Return the images a user has submitted to the gallery
                 */
                getAccountSubmissions: function(username) {

                    var url = this.apiBase + "/account/" + username + "/submissions";

                    return this._makeRequest(url, "GET");
                },

                /*
                 * Returns the account settings, only accessible if you're logged in as the user.
                 */
                getAccountSettings: function(username) {

                    var url = this.apiBase + "/account/" + username + "/settings";

                    return this._makeRequest(url, "GET");
                },

                /*
                 * Updates the account settings for a given user, the user must be logged in.
                 */
                setAccountSettings: function(username, params) {

                    var url = this.apiBase + "/account/" + username + "/settings"

                    return this._makeRequest(url, "POST", params);
                },

                /*
                 * Return all of the images associated with the account. You can page through the images by setting the page, this defaults to 0.
                 */
                getAccountImages: function(username, page) {

                    if(page === undefined) {
                        page = 0;
                    }

                    var url = this.apiBase + "/account/" + username + "/images/" + page;

                    return this._makeRequest(url, "GET");
                },

                /*
                 * Upload a new image.
                 */
                imageUpload: function(params) {

                    var url = this.apiBase + "/image";

                    return this._makeRequest(url, "POST", params);
                },

                /*
                 * Returns the images in the gallery.
                 */
                getGallery: function(section, sort, page, dateRange, showViral) {

                    var galleryEndpoint = this.apiBase + "/gallery";
                    if(section !== undefined) { galleryEndpoint += "/" + section; }
                    if(sort !== undefined) { galleryEndpoint += "/" + sort; }
                    if(dateRange !== undefined) { galleryEndpoint += "/" + dateRange; }
                    if(showViral !== undefined) { galleryEndpoint += "?showViral=" + showViral; }

                    return this._makeRequest(galleryEndpoint, "GET");
                },

                /*
                 * Get list of all conversations for the logged in user.
                 */
                getConversationList: function() {

                    var url = this.apiBase + "/conversations";

                    return this._makeRequest(url, "GET");
                },

                /**
                 * Executes a request with the given url, method, and optional
                 * params and returns a $q promise for the result
                 * @param {string} url - the url to access
                 * @param {string} method - the http method to use
                 * @param {?object} params - any parameters to pass
                 * @returns {Promise<object>} a promise for the result
                 */
                _makeRequest: function(url, method, params) {

                    var settings = {
                        method: method,
                        url: url,
                        headers: {
                            "Authorization": "Bearer " + this.accessToken
                        }
                    };

                    if(params) {
                        settings.params = params;
                    }

                    var deferred = $q.defer();

                    $http(settings)
                        .success(function (result) {
                            deferred.resolve(result);
                        })
                        .error(function (error) {
                            deferred.reject(error);
                        });

                    return deferred.promise;
                }
            };

            return new Imgur(this.accessToken);
        }];
    });

})();
<|endoftext|>


<commit_before>import React from 'react';
import moment from 'moment';
import { Link } from 'react-router-dom';

import { getEventDescriptions } from '../utilities/event';

const UpcomingEventHero = ({ event }) => {
  const { mainTitle, subTitle, byline, description } = getEventDescriptions(
    event
  );
  const date = moment(event.time);
  return (

        <Link to={`/events/${event.id}`}>
          {mainTitle}
          {subTitle &&

              :
              {' '}

                {subTitle}

            }
        </Link>

{byline}

{date.format('MMMM Do YYYY')}

  );
};

export default UpcomingEventHero;
<commit_msg>Disable background image for now<commit_after>import React from 'react';
import moment from 'moment';
import { Link } from 'react-router-dom';

import { getEventDescriptions } from '../utilities/event';

const UpcomingEventHero = ({ event }) => {
  const { mainTitle, subTitle, byline, description } = getEventDescriptions(
    event
  );
  const date = moment(event.time);
  return (

        <Link to={`/events/${event.id}`}>
          {mainTitle}
          {subTitle &&

              :
              {' '}

                {subTitle}

            }
        </Link>

{byline}

{date.format('MMMM Do YYYY')}

  );
};

export default UpcomingEventHero;
<|endoftext|>


<commit_before>// Tests for the input module, which is mainly about interpreting the headers
// (everything before '$end') of the input files.

var vows = require('vows');
var assert = require('assert');
var input = require('../lib/input.js');

var config = {
  paths: {
    data: '/data',
    templates: '/templates/',
    resources: '/resources/',
    posts: '/data/posts'
  }
};

var END = '\n\n';

var paths = input.getPaths(config);

vows.describe('Date').addBatch({
  'A date string': {
    'from a valid Date': {
      topic: input.formatDate(new Date('2011-12-15')),
      'should be formatted in a human readable form in American English': function (topic) {
        assert.strictEqual(topic, 'Thu Dec 15 2011');
      }
    },
    'from an uninitialized Date': {
      topic: input.formatDate(new Date(null)),
      'should be formatted in a human readable form in American English': function (topic) {
        assert.strictEqual(topic, 'Thu Jan 01 1970');
      }
    },
    'from an invalid Date': {
      topic: input.formatDate(new Date(undefined)),
      'should be "Invalid Date"': function (topic) {
        assert.strictEqual(topic, 'Invalid Date');
      }
    }
  }
}).export(module);

vows.describe('Source').addBatch({
  'Attempting to get source from input': {
    'without header': {
      'should throw an error': function() {
        assert.throws(function() {
          input.getSource('');
        });
      }
    },
    'with header, but without template': {
      'should throw an error': function() {
        assert.throws(function() {
          input.getSource('{ }\n$end\n', 'test', paths);
        });
      }
    },
    'with header and template': {
      'should not throw an error': function() {
        assert.doesNotThrow(function() {
          input.getSource('{ "template":"name.jade" }' + END, 'path/to/file.md', paths);
        });
      }
    }
  },
  'Source from input with header': {
    'without date': {
      topic: input.getSource('{ "template":"name.jade" }' + END, 'path/to/file.md', paths),
      'should have the current date': function(topic) {
        assert.strictEqual(topic.dateString, input.formatDate(new Date()));
      }
    },
    'without name': {
      topic: input.getSource('{ "template":"name.jade" }' + ,END 'path/to/file.md', paths),
      'should have filename from input file name': function(topic) {
        assert.strictEqual(topic.name, 'file.html');
      }
    },
    'with template name': {
      topic: input.getSource('{ "template":"name.jade" }' + END, 'path/to/file.md', paths),
      'should have complete path to template file': function(topic) {
        assert.strictEqual(topic.templatePath, '/templates/name.jade');
      }
    },
    'without path': {
      topic: input.getSource('{ "template":"name.jade" }' + END, '/data/posts/path/to/file.md', paths),
      'should have path from input filename': function(topic) {
        assert.strictEqual(topic.path, '/path/to');
      },
      'should have the correct link': function(topic) {
        assert.strictEqual(topic.link, '/path/to/file');
      }
    },
    'with path': {
      topic: input.getSource('{ "template":"name.jade", "path":"/path/to" }' + END, 'path/to/file.md', paths),
      'should have the correct link': function(topic) {
        assert.strictEqual(topic.link, '/path/to/file');
      },
      'should have the correct path': function(topic) {
        assert.strictEqual(topic.path, '/path/to');
      }
    }
  }
}).export(module);
<commit_msg>Fix typo in tests<commit_after>// Tests for the input module, which is mainly about interpreting the headers
// (everything before '$end') of the input files.

var vows = require('vows');
var assert = require('assert');
var input = require('../lib/input.js');

var config = {
  paths: {
    data: '/data',
    templates: '/templates/',
    resources: '/resources/',
    posts: '/data/posts'
  }
};

var END = '\n\n';

var paths = input.getPaths(config);

vows.describe('Date').addBatch({
  'A date string': {
    'from a valid Date': {
      topic: input.formatDate(new Date('2011-12-15')),
      'should be formatted in a human readable form in American English': function (topic) {
        assert.strictEqual(topic, 'Thu Dec 15 2011');
      }
    },
    'from an uninitialized Date': {
      topic: input.formatDate(new Date(null)),
      'should be formatted in a human readable form in American English': function (topic) {
        assert.strictEqual(topic, 'Thu Jan 01 1970');
      }
    },
    'from an invalid Date': {
      topic: input.formatDate(new Date(undefined)),
      'should be "Invalid Date"': function (topic) {
        assert.strictEqual(topic, 'Invalid Date');
      }
    }
  }
}).export(module);

vows.describe('Source').addBatch({
  'Attempting to get source from input': {
    'without header': {
      'should throw an error': function() {
        assert.throws(function() {
          input.getSource('');
        });
      }
    },
    'with header, but without template': {
      'should throw an error': function() {
        assert.throws(function() {
          input.getSource('{ }\n\n', 'test', paths);
        });
      }
    },
    'with header and template': {
      'should not throw an error': function() {
        assert.doesNotThrow(function() {
          input.getSource('{ "template":"name.jade" }' + END, 'path/to/file.md', paths);
        });
      }
    }
  },
  'Source from input with header': {
    'without date': {
      topic: input.getSource('{ "template":"name.jade" }' + END, 'path/to/file.md', paths),
      'should have the current date': function(topic) {
        assert.strictEqual(topic.dateString, input.formatDate(new Date()));
      }
    },
    'without name': {
      topic: input.getSource('{ "template":"name.jade" }' + END, 'path/to/file.md', paths),
      'should have filename from input file name': function(topic) {
        assert.strictEqual(topic.name, 'file.html');
      }
    },
    'with template name': {
      topic: input.getSource('{ "template":"name.jade" }' + END, 'path/to/file.md', paths),
      'should have complete path to template file': function(topic) {
        assert.strictEqual(topic.templatePath, '/templates/name.jade');
      }
    },
    'without path': {
      topic: input.getSource('{ "template":"name.jade" }' + END, '/data/posts/path/to/file.md', paths),
      'should have path from input filename': function(topic) {
        assert.strictEqual(topic.path, '/path/to');
      },
      'should have the correct link': function(topic) {
        assert.strictEqual(topic.link, '/path/to/file');
      }
    },
    'with path': {
      topic: input.getSource('{ "template":"name.jade", "path":"/path/to" }' + END, 'path/to/file.md', paths),
      'should have the correct link': function(topic) {
        assert.strictEqual(topic.link, '/path/to/file');
      },
      'should have the correct path': function(topic) {
        assert.strictEqual(topic.path, '/path/to');
      }
    }
  }
}).export(module);
<|endoftext|>


<commit_before>var fs = require('fs'),
	keystone = require('../index');

exports.init = function (shouldRunTests) {
	keystone.init();

	var typesLoc = __dirname + '/../fields/types';
	var types = fs.readdirSync(typesLoc);

	types.forEach(function(name) {
		var serverTestLoc = typesLoc + '/' + name + '/test/server.js';
		if (!fs.existsSync(serverTestLoc)) return;

		console.log(keystone.mongoose);

		var List = keystone.List(name + "Test", { nocreate: true });
		var test = require(serverTestLoc);

		if (test.initList) {
			test.initList(List);
		}

		List.register();

		if (shouldRunTests && test.testFieldType) {
			describe(name, function () {
				test.testFieldType(List);
			});
		}
	});
};
<commit_msg>Remove keystone mongoose logging in fields test helper<commit_after>var fs = require('fs'),
	keystone = require('../index');

exports.init = function (shouldRunTests) {
	keystone.init();

	var typesLoc = __dirname + '/../fields/types';
	var types = fs.readdirSync(typesLoc);

	types.forEach(function(name) {
		var serverTestLoc = typesLoc + '/' + name + '/test/server.js';
		if (!fs.existsSync(serverTestLoc)) return;

		var List = keystone.List(name + "Test", { nocreate: true });
		var test = require(serverTestLoc);

		if (test.initList) {
			test.initList(List);
		}

		List.register();

		if (shouldRunTests && test.testFieldType) {
			describe(name, function () {
				test.testFieldType(List);
			});
		}
	});
};
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import { View, TouchableOpacity } from 'react-native'
import { observable, transaction, computed, action } from 'mobx'
import { observer } from 'mobx-react/native'

import Icon from 'react-native-vector-icons/FontAwesome'
import Drawer from 'react-native-drawer'

import { PureComponent } from './Component.js'
import { T } from './AppText.js'

@observer
export class SideMenu extends PureComponent {
    /* properties:
        content: Component or [Component]
    */
    render = () => {
        //Parallax Effect (slack style)
        const open = drawerStore.open
        const content = open ? this.props.content : undefined
        return <Drawer
                    ref={ref => { this.drawer = ref}}
                    open={drawerStore.open}
                    onOpen={drawerStore.setOpen}
                    onClose={drawerStore.setClosed}
                    type="static"
                    content={content}
                    openDrawerOffset={0.25}
                    panCloseMask={0.25}
                    styles={drawerStyles}
                    disabled={drawerStore.disabled}
                    tweenHandler={Drawer.tweenPresets.parallax}
                    tapToClose={true}
                    /*elevation={2}*/
                    >
                {this.props.children}
            </Drawer>
    }
}

@observer
export class MenuIcon extends PureComponent {
    render = () => {
        return <TouchableOpacity onPress={drawerStore.toggleOpenClose}>
            <Icon name="bars" size={30} color="#000" />
        </TouchableOpacity>
    }
}

class DrawerStore {
    @observable open = false
    @observable disabled = false

    @action disable = () => this.disabled = true
    @action enable = () => this.disabled = false

    @action setOpen = () => this.open = true
    @action setClosed = () => this.open = false

    toggleOpenClose = () => {
        this.open = !this.open
    }
}

export const drawerStore = new DrawerStore()

const drawerStyles = {
    drawer: {
        shadowColor: '#000000',
        shadowOpacity: 0.8,
        // shadowRadius: 3,
    },
    main: {
        // paddingLeft: 3,
    },
}
<commit_msg>Remove shadow from side menu when closed (doesn't work well on iOS)<commit_after>import React, { Component } from 'react'
import { View, TouchableOpacity, Platform } from 'react-native'
import { observable, transaction, computed, action } from 'mobx'
import { observer } from 'mobx-react/native'

import Icon from 'react-native-vector-icons/FontAwesome'
import Drawer from 'react-native-drawer'

import { PureComponent } from './Component.js'
import { T } from './AppText.js'

@observer
export class SideMenu extends PureComponent {
    /* properties:
        content: Component or [Component]
    */
    render = () => {
        //Parallax Effect (slack style)
        const open = drawerStore.open
        const content = open ? this.props.content : undefined
        return <Drawer
                    ref={ref => { this.drawer = ref}}
                    open={drawerStore.open}
                    onOpen={drawerStore.setOpen}
                    onClose={drawerStore.setClosed}
                    type="static"
                    content={content}
                    openDrawerOffset={0.25}
                    panCloseMask={0.25}
                    styles={drawerStore.drawerStyle}
                    disabled={drawerStore.disabled}
                    tweenHandler={Drawer.tweenPresets.parallax}
                    tapToClose={true}
                    /*elevation={2}*/
                    >
                {this.props.children}
            </Drawer>
    }
}

@observer
export class MenuIcon extends PureComponent {
    render = () => {
        return <TouchableOpacity onPress={drawerStore.toggleOpenClose}>
            <Icon name="bars" size={30} color="#000" />
        </TouchableOpacity>
    }
}

class DrawerStore {
    @observable open = false
    @observable disabled = false

    @action disable = () => this.disabled = true
    @action enable = () => this.disabled = false

    @action setOpen = () => this.open = true
    @action setClosed = () => this.open = false

    toggleOpenClose = () => {
        this.open = !this.open
    }

    @computed get drawerStyle() {
        var drawerStyle = {}
        if (this.open && !this.disabled) {
            drawerStyle = {
                shadowColor: '#000000',
                shadowOpacity: 0.8
                // shadowRadius: 3,
            }
        }
        return { drawer: drawerStyle, main: {} }
    }
}

export const drawerStore = new DrawerStore()

// const drawerStyle =
//     Platform.OS === 'android'
//         ? { shadowColor: '#000000'
//           , shadowOpacity: 0.8
//           // , shadowRadius: 3
//           }
//         : {}
<|endoftext|>


<commit_before>/**
 * Author: Olaolu Olawuyi

 * Date Created: 20/08/2015

 * Last Modified: 10/01/2016

 * Twitter: @whizkydee, @webtwic

 * Github: https://github.com/whizkydee/webtwic

*/

var Webtwic = {
	// Basic Information about Webtwic
	name: "Webtwic",
	description: "A community for coding and tech tweaks where beginners and experts can learn.",
	repo: "https://github.com/whizkydee/webtwic",
	author: "Olaolu Olawuyi",
	version: 1.07,

	// Start main functions for the object

	// Function to add browser name and version to body class
	_showBrowser: function(b) {
		$("body").addClass(b + " " + b + parseInt(browser.version));
	},

	// Function to set attributes and values to elements
	_setAttr: function(elem, name, val) {
		$(elem, this).attr(name, val);
	},

	// Function to hide Notifications
	_hideNotif: function() {
		$("#disabledFeatures").removeClass("dfvisible");
	},

	// Function to push down .site-content
	_adjustSiteContent: function() {
		$(".top-stripe").addClass("adjustedMargin");
		$("#header .logo").addClass("adjustedTop");
		$(".main-menu").addClass("adjustedMargin");
		$("#header").addClass("adjustSiteContent");
	},

	// Function to push up .site-content to default state
	_undoSiteAdjust: function() {
		$(".top-stripe").removeClass("adjustedMargin");
		$("#header .logo").removeClass("adjustedTop");
		$(".main-menu").removeClass("adjustedMargin");
		$("#header").removeClass("adjustSiteContent");
	},

	_resetSite: function() {
		WBT._showNotif();
		WBT._adjustSiteContent();
	},

	_undoReset: function() {
		WBT._hideNotif();
		WBT._undoSiteAdjust();
	},

	// Function to show Notifications before header
	_showNotif: function() {
		WBT._adjustSiteContent();
		if( $("body").hasClass("default-body") ) {
			$(".top-stripe").before(
				"
Certain features which affect the functionality of webtwic are disabled in your browser. Please try refreshing the page or get a better browser.
"
			);
		} else if ( $("body").hasClass("secondary-body") ) {

		} else {
			return;
		}
		$(".close-button").on("click", function() {
			WBT._hideNotif();
			WBT._undoSiteAdjust();
		});
	}
	// ;

}, WBT = Webtwic;

jQuery(document).ready(function() {

	// Define Variables
	var is_DisabledFeatures = $("div").hasClass("dfvisible"),
	img = $("img"),
	is_noAlt = img.attr("alt", null),
	scripts = $("script"),
	is_noType = scripts.attr("type", null),
	is_noCharset = scripts.attr("charset", null);

	var $to_top = $("a.to-top"),
	is_toggle_active = $(".hamburger").hasClass("is_active"),
	height = 200;

	// Performance Fixes

	// Start browser detection

	browser = bowser;
	browser.ie = browser.msie;

	if (browser.webkit) {
		$("body").addClass("webkit");
	}
	if(browser.chrome) {
		WBT._showBrowser("chrome");
	}
	if(browser.msedge) {
		WBT._showBrowser("msedge");
	}
	if(browser.firefox) {
		WBT._showBrowser("firefox");
	}
	if(browser.msie) {
		WBT._showBrowser("ie");
	}
	if(browser.safari) {
		WBT._showBrowser("safari");
	}
	if(browser.opera) {
		WBT._showBrowser("opera");
	}

	// Performance fixes for attributes

	if(is_noAlt) {
		WBT._setAttr(img, "alt", "...");
	} else {
		return img.attr("alt");
	}

	if(is_noCharset) {
		WBT._setAttr(scripts, "charset", "utf-8");
	} else {
		return scripts.attr("charset");
	}

	if(is_noType) {
		WBT._setAttr(scripts, "type", "text/javascript");
	} else {
		return scripts.attr("type");
	}

	if (Modernizr.flexbox || !Modernizr.inlinesvg || !Modernizr.svg || !Modernizr.svgclippaths || !Modernizr.fontface || !Modernizr.boxshadow || !Modernizr.borderradius || !Modernizr.csstransforms) {
		WBT._showNotif();
	} else {
		WBT._hideNotif();
	}

	// Start Main Page

	$("nav li ul.drop-nav").hide().removeClass("dropNavFallback");

	if ( $(window).width() > 860 ) {
		$("nav li.dropdown").hover(function() {
			$("ul.drop-nav", this).stop().slideDown("fast");
		}, function() {
			$("ul.drop-nav", this).stop().slideUp("fast");
		});
	}

	$(".active-item").append(
		"<svg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'><path d='M 16.682,19.674c 0.010-0.012, 0.014-0.028, 0.024-0.040l 6.982-7.714c 0.39-0.434, 0.39-1.138,0-1.572 c-0.004-0.004-0.008-0.006-0.012-0.008C 23.496,10.13, 23.244,10, 22.964,10L 8.998,10 c-0.286,0-0.54,0.138-0.722,0.352L 8.272,10.348 c-0.39,0.434-0.39,1.138,0,1.572l 6.998,7.754C 15.66,20.108, 16.292,20.108, 16.682,19.674z'></path></svg>"
	);

	$("a.hamburger").click(function(e) {
		$(this).toggleClass("is_active");

		e.preventDefault();
	});
	$(".mobile-header .mobile-nav").hide();

	$(window).scroll(function() {
		($(this).scrollTop() > height) ? $to_top.addClass("topVisible"): $to_top.removeClass("topVisible");
	});

	// Styles for homepage article-social

	$(".article .article-social a").hover(function() {
		$("span", this).eq(1).fadeIn(400);
	}, function() {
		$("span", this).eq(1).fadeOut(250);
	});

	// Functions for search-icon and ui-overlay-search

	$(".header-follow-search .search-wrap").on("click", function() {
		openUiOverlay();
	});

	$(".header-follow-search .close-icon").on("click", function() {
		closeUiOverlay();
	});

	$(".header-follow-search .close-icon").hover(
		function() {
			$(this).addClass("spin");
		},
		function() {
			$(this).removeClass("spin");
		});

	function openUiOverlay() {
		$(".ui-overlay-search").fadeIn(400).addClass("is_open");
	}

	function closeUiOverlay() {
		$(".ui-overlay-search").fadeOut(400).removeClass("is_open");
	}

	$(".ui-overlay-search .search-input input").focus(function() {
		$(".search-input").attr("data-focused", true);
		$(".search-input input").addClass("_is-focused");
		$("#__expand").addClass("visible");
	});

	$(".ui-overlay-search .search-input input").blur(function() {
		$(".search-input").attr("data-focused", false);
		$(".search-input input").removeClass("_is-focused");
		$("#__expand").removeClass("visible");
	});

	$(".post-meta .post-meta-inner .cta-btns a.is_inactive").on("click", function(e) {
		e.preventDefault();
	});

	$(document).keydown(function(e) {
    if (e.keyCode == 27) {
        closeUiOverlay();
    } else {
		 return;
	 }});

	 $(".mobile-nav").hide();

	// Functions for more-posts button click

	function addLoader() {
		$(".secondary-category-section .category-posts-column [data-clicked='false']").attr("data-clicked", "true").append("");
	}
	$(".secondary-category-section .category-posts-column [data-clicked='false']").on("click", function(e) {
		$(this).css({
			"background": "#50ABF1",
			"color": "#fff"
		});
		addLoader();
		e.preventDefault();
	});

	$to_top.on("click", function(e) {
		$("body,html").animate({
			scrollTop: 0
		}, 1000);

		e.preventDefault();
	});

});
<commit_msg>Rename #disabledFeatures and .dfvisible<commit_after>/**
 * Author: Olaolu Olawuyi

 * Date Created: 20/08/2015

 * Last Modified: 10/01/2016

 * Twitter: @whizkydee, @webtwic

 * Github: https://github.com/whizkydee/webtwic

*/

var Webtwic = {
	// Basic Information about Webtwic
	name: "Webtwic",
	description: "A community for coding and tech tweaks where beginners and experts can learn.",
	repo: "https://github.com/whizkydee/webtwic",
	author: "Olaolu Olawuyi",
	version: 1.07,

	// Start main functions for the object

	// Function to add browser name and version to body class
	_showBrowser: function(b) {
		$("body").addClass(b + " " + b + parseInt(browser.version));
	},

	// Function to set attributes and values to elements
	_setAttr: function(elem, name, val) {
		$(elem, this).attr(name, val);
	},

	// Function to hide Notifications
	_hideNotif: function() {
		$("#notifBanner").removeClass("notif_vis");
	},

	// Function to push down .site-content
	_adjustSiteContent: function() {
		$(".top-stripe").addClass("adjustedMargin");
		$("#header .logo").addClass("adjustedTop");
		$(".main-menu").addClass("adjustedMargin");
		$("#header").addClass("adjustSiteContent");
	},

	// Function to push up .site-content to default state
	_undoSiteAdjust: function() {
		$(".top-stripe").removeClass("adjustedMargin");
		$("#header .logo").removeClass("adjustedTop");
		$(".main-menu").removeClass("adjustedMargin");
		$("#header").removeClass("adjustSiteContent");
	},

	_resetSite: function() {
		WBT._showNotif();
		WBT._adjustSiteContent();
	},

	_undoReset: function() {
		WBT._hideNotif();
		WBT._undoSiteAdjust();
	},

	// Function to show Notifications before header
	_showNotif: function() {
		WBT._adjustSiteContent();
		if( $("body").hasClass("default-body") ) {
			$(".top-stripe").before(
				"
Certain features which affect the functionality of webtwic are disabled in your browser. Please try refreshing the page or get a better browser.
"
			);
		} else if ( $("body").hasClass("secondary-body") ) {

		} else {
			return;
		}
		$(".close-button").on("click", function() {
			WBT._hideNotif();
			WBT._undoSiteAdjust();
		});
	}
	// ;

}, WBT = Webtwic;

jQuery(document).ready(function() {

	// Define Variables
	var is_notifBanner = $("div").hasClass("notif_vis"),
	img = $("img"),
	is_noAlt = img.attr("alt", null),
	scripts = $("script"),
	is_noType = scripts.attr("type", null),
	is_noCharset = scripts.attr("charset", null);

	var $to_top = $("a.to-top"),
	is_toggle_active = $(".hamburger").hasClass("is_active"),
	height = 200;

	// Performance Fixes

	// Start browser detection

	browser = bowser;
	browser.ie = browser.msie;

	if (browser.webkit) {
		$("body").addClass("webkit");
	}
	if(browser.chrome) {
		WBT._showBrowser("chrome");
	}
	if(browser.msedge) {
		WBT._showBrowser("msedge");
	}
	if(browser.firefox) {
		WBT._showBrowser("firefox");
	}
	if(browser.msie) {
		WBT._showBrowser("ie");
	}
	if(browser.safari) {
		WBT._showBrowser("safari");
	}
	if(browser.opera) {
		WBT._showBrowser("opera");
	}

	// Performance fixes for attributes

	if(is_noAlt) {
		WBT._setAttr(img, "alt", "...");
	} else {
		return img.attr("alt");
	}

	if(is_noCharset) {
		WBT._setAttr(scripts, "charset", "utf-8");
	} else {
		return scripts.attr("charset");
	}

	if(is_noType) {
		WBT._setAttr(scripts, "type", "text/javascript");
	} else {
		return scripts.attr("type");
	}

	if (Modernizr.flexbox || !Modernizr.inlinesvg || !Modernizr.svg || !Modernizr.svgclippaths || !Modernizr.fontface || !Modernizr.boxshadow || !Modernizr.borderradius || !Modernizr.csstransforms) {
		WBT._showNotif();
	} else {
		WBT._hideNotif();
	}

	// Start Main Page

	$("nav li ul.drop-nav").hide().removeClass("dropNavFallback");

	if ( $(window).width() > 860 ) {
		$("nav li.dropdown").hover(function() {
			$("ul.drop-nav", this).stop().slideDown("fast");
		}, function() {
			$("ul.drop-nav", this).stop().slideUp("fast");
		});
	}

	$(".active-item").append(
		"<svg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'><path d='M 16.682,19.674c 0.010-0.012, 0.014-0.028, 0.024-0.040l 6.982-7.714c 0.39-0.434, 0.39-1.138,0-1.572 c-0.004-0.004-0.008-0.006-0.012-0.008C 23.496,10.13, 23.244,10, 22.964,10L 8.998,10 c-0.286,0-0.54,0.138-0.722,0.352L 8.272,10.348 c-0.39,0.434-0.39,1.138,0,1.572l 6.998,7.754C 15.66,20.108, 16.292,20.108, 16.682,19.674z'></path></svg>"
	);

	$("a.hamburger").click(function(e) {
		$(this).toggleClass("is_active");

		e.preventDefault();
	});
	$(".mobile-header .mobile-nav").hide();

	$(window).scroll(function() {
		($(this).scrollTop() > height) ? $to_top.addClass("topVisible"): $to_top.removeClass("topVisible");
	});

	// Styles for homepage article-social

	$(".article .article-social a").hover(function() {
		$("span", this).eq(1).fadeIn(400);
	}, function() {
		$("span", this).eq(1).fadeOut(250);
	});

	// Functions for search-icon and ui-overlay-search

	$(".header-follow-search .search-wrap").on("click", function() {
		openUiOverlay();
	});

	$(".header-follow-search .close-icon").on("click", function() {
		closeUiOverlay();
	});

	$(".header-follow-search .close-icon").hover(
		function() {
			$(this).addClass("spin");
		},
		function() {
			$(this).removeClass("spin");
		});

	function openUiOverlay() {
		$(".ui-overlay-search").fadeIn(400).addClass("is_open");
	}

	function closeUiOverlay() {
		$(".ui-overlay-search").fadeOut(400).removeClass("is_open");
	}

	$(".ui-overlay-search .search-input input").focus(function() {
		$(".search-input").attr("data-focused", true);
		$(".search-input input").addClass("_is-focused");
		$("#__expand").addClass("visible");
	});

	$(".ui-overlay-search .search-input input").blur(function() {
		$(".search-input").attr("data-focused", false);
		$(".search-input input").removeClass("_is-focused");
		$("#__expand").removeClass("visible");
	});

	$(".post-meta .post-meta-inner .cta-btns a.is_inactive").on("click", function(e) {
		e.preventDefault();
	});

	$(document).keydown(function(e) {
    if (e.keyCode == 27) {
        closeUiOverlay();
    } else {
		 return;
	 }});

	 $(".mobile-nav").hide();

	// Functions for more-posts button click

	function addLoader() {
		$(".secondary-category-section .category-posts-column [data-clicked='false']").attr("data-clicked", "true").append("");
	}
	$(".secondary-category-section .category-posts-column [data-clicked='false']").on("click", function(e) {
		$(this).css({
			"background": "#50ABF1",
			"color": "#fff"
		});
		addLoader();
		e.preventDefault();
	});

	$to_top.on("click", function(e) {
		$("body,html").animate({
			scrollTop: 0
		}, 1000);

		e.preventDefault();
	});

});
<|endoftext|>