<commit_before>/* global RTCPeerConnection, MediaStream */
// jshint unused:false

var cordova = window.cordova;

// Expose WebRTC Globals
if (cordova && cordova.plugins && cordova.plugins.iosrtc) {
  cordova.plugins.iosrtc.registerGlobals();
  //cordova.plugins.iosrtc.debug.disable('*', true);
  //cordova.plugins.iosrtc.debug.enable('*', true);
  cordova.plugins.iosrtc.initAudioDevices();
  cordova.plugins.iosrtc.turnOnSpeaker(true);
  cordova.plugins.iosrtc.requestPermission(true, true, function (result) {
    console.log('requestPermission.result', result);
  });
}

//
// Container
//

document.body.innerHTML = "";
var appContainer = document.body;

//
// getUserMedia
//

var localStream;
var localVideoEl;
function TestGetUserMedia() {

  // Note: Support for Multiple TestRTCPeerConnection calls
  if (localStream) {

    // Close local Stream if already connected
    localStream.stop();

    // Clear local video element
    if (localVideoEl.srcObject) {
      localVideoEl.srcObject = null;
      if (localVideoEl.parentNode === appContainer) {
        appContainer.removeChild(localVideoEl);
      }
    }
  }

  localVideoEl = document.createElement('video');
  localVideoEl.setAttribute('autoplay', 'autoplay');
  localVideoEl.setAttribute('playsinline', 'playsinline');

  // Note: Test CSS positioning
  localVideoEl.style.backgroundColor = 'purple'; // Cause zIndex - 1 failure
  localVideoEl.style.position = 'absolute';
  localVideoEl.style.top = 0;
  localVideoEl.style.left = 0;
  localVideoEl.style.width = "100px";
  localVideoEl.style.height = "100px";
  localVideoEl.style.transform = "scaleX(-1)";
  appContainer.appendChild(localVideoEl);

  navigator.mediaDevices.enumerateDevices().then(function (devices) {
      console.log('getMediaDevices.ok', devices);
      devices.forEach(function (device, idx) {
        console.log('getMediaDevices.devices', idx, device.label, device.kind, device.deviceId);
      });
  }, function (err) {
      console.log('getMediaDevices.err', err);
  });

  return navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
    /*
    video: {
      // Test Back Camera
      //deviceId: 'com.apple.avfoundation.avcapturedevice.built-in_video:0'
      //sourceId: 'com.apple.avfoundation.avcapturedevice.built-in_video:0'
      deviceId: {
        exact: 'com.apple.avfoundation.avcapturedevice.built-in_video:0'
      }
    },
    audio: {
      exact: 'Built-In Microphone'
    }*/
  }).then(function (stream) {

    console.log('getUserMedia.stream', stream);
    console.log('getUserMedia.stream.getTracks', stream.getTracks());

    // Note: Expose for debug
    localStream = stream;

    // Attach local stream to video element
    localVideoEl.srcObject = localStream;

    // Test HTML over Video element
    TestPluginMediaStreamRenderer(localVideoEl);

    return localStream;

  }).catch(function (err) {
    console.log('getUserMediaError', err, err.stack);
  });
}

var useAnimateVideo = false;
function TestPluginMediaStreamRenderer(localVideoEl) {

  // Animate video position
  var currentPosition = {
    x: 0,
    y: 0
  };

  var animateTimer;
  function animateVideo() {

    currentPosition.x = currentPosition.x < (window.innerWidth - parseInt(localVideoEl.style.width, 10)) ? currentPosition.x + 1 : 0;
    currentPosition.y = currentPosition.y < (window.innerHeight - parseInt(localVideoEl.style.height, 10)) ? currentPosition.y + 1 : 0;

    localVideoEl.style.top = currentPosition.y + 'px';
    localVideoEl.style.left = currentPosition.x + 'px';

    if (cordova && cordova.plugins && cordova.plugins.iosrtc) {
      //cordova.plugins.iosrtc.refreshVideos();
    }

    return (animateTimer = requestAnimationFrame(animateVideo));
  }

  if (useAnimateVideo) {
    animateTimer = animateVideo();
  }

  //
  // Test Video behind Element
  //

  // Note: Test HTML over video requiring Z-Index -1 and transparent <html> and <body>
  document.body.style.background = "transparent";
  document.documentElement.style.background = "transparent";
  localVideoEl.style.backgroundColor = '';
  localVideoEl.style.zIndex = -1;

  var overEl = document.createElement('button');
  overEl.style.backgroundColor = 'red';
  overEl.style.position = 'absolute';
  overEl.style.left = 0;
  overEl.style.top = 0;
  overEl.style.width = "100px";
  overEl.style.height = "100px";

  overEl.addEventListener('click', function () {
    overEl.style.backgroundColor = overEl.style.backgroundColor === 'red' ? 'green' : 'red';

    if (overEl.style.backgroundColor === 'red') {
      animateTimer = animateVideo();
    } else {
      cancelAnimationFrame(animateTimer);
    }
  });

  appContainer.appendChild(overEl);

  overEl.style.left = ((window.innerWidth / 2)  - parseInt(overEl.style.width, 10)) + 'px';
  overEl.style.top  = ((window.innerHeight / 2) - parseInt(overEl.style.height, 10)) + 'px';

}

//
// Test RTCPeerConnection
//

var peerConnectionConfig = {
    offerToReceiveVideo: true,
    //iceTransportPolicy: 'relay',
    sdpSemantics: 'unified-plan',
    offerToReceiveAudio: true,
    //sdpSemantics: 'plan-b',
    //bundlePolicy: 'max-compat',
    //rtcpMuxPolicy: 'negotiate',
    iceServers: [
        {
            url: "stun:stun.stunprotocol.org"
        }
    ]
};

// This plugin handle 'addstream' and 'track' event for MediaStream creation.
var useTrackEvent = Object.getOwnPropertyDescriptors(RTCPeerConnection.prototype).ontrack;

var pc1 = new RTCPeerConnection(peerConnectionConfig),
    pc2 = new RTCPeerConnection(peerConnectionConfig);

var sendChannel, receiveChannel;
function TestPeerDataChannel() {
  sendChannel = pc1.createDataChannel("sendChannel");

  sendChannel.addEventListener('message',  function (event) {
    console.log('sendChannel.message', event, event.data);
  });

  sendChannel.addEventListener('open', function (event) {
      console.log('sendChannel.open', event);
      sendChannel.send("data message Date: " + new Date());
  });

  sendChannel.addEventListener('close', function (event) {
      console.log('sendChannel.close', event);
  });

  pc2.addEventListener('datachannel', function (event) {
    receiveChannel = event.channel;
    receiveChannel.addEventListener('message',  function (event) {
      console.log('receiveChannel.message', event, event.data);
    });
    receiveChannel.addEventListener('open',  function (event) {
      console.log('receiveChannel.open', event);
    });
    receiveChannel.addEventListener('close',  function (event) {
      console.log('receiveChannel.close', event);
    });
  });
}

var peerVideoEl;
var peerStream;
function TestRTCPeerConnection(localStream) {

  // Note: Support for Multiple TestRTCPeerConnection calls
  if (peerStream) {

    // Close peer Stream if already connected
    peerStream.stop();

    // Clear peer video element
    if (peerVideoEl.srcObject) {
      peerVideoEl.srcObject = null;
      appContainer.removeChild(peerVideoEl);
    }

    // Disconnect peer
    if (pc1.iceConnectionState === 'completed') {
      pc1.close();

      // Current you cannot reuse previous RTCPeerConnection
      pc1 = new RTCPeerConnection();
      pc2 = new RTCPeerConnection();
    }
  }

  if (useTrackEvent) {

    // Add local stream tracks to RTCPeerConnection
    var localPeerStream = new MediaStream();
    localStream.getTracks().forEach(function (track) {
      console.log('pc1.addTrack', track, localPeerStream);
      pc1.addTrack(track, localPeerStream);
    });

  // Note: Deprecated but supported
  } else {
     pc1.addStream(localStream);

     // Note: Deprecated Test removeStream
     // pc1.removeStream(pc1.getLocalStreams()[0]);<
  }

  function onAddIceCandidate(pc, can) {
    console.log('addIceCandidate', pc, can);
    return can && pc.addIceCandidate(can).catch(function (err) {
      console.log('addIceCandidateError', err);
    });
  }

  pc1.addEventListener('icecandidate', function (e) {
    onAddIceCandidate(pc2, e.candidate);
  });

  pc2.addEventListener('icecandidate', function (e) {
    onAddIceCandidate(pc1, e.candidate);
  });

  pc2.addEventListener('track', function (e) {
    console.log('pc2.track', e);
  });

  pc2.addEventListener('removestream', function (e) {
    console.log('pc2.removeStream', e);
  });

  function setPeerVideoStream(stream) {

    peerVideoEl = document.createElement('video');
    peerVideoEl.setAttribute('autoplay', 'autoplay');
    peerVideoEl.setAttribute('playsinline', 'playsinline');

    // Note: Test Object-fix
    peerVideoEl.style.objectFit = "cover";

    // Note: Test CSS positioning
    peerVideoEl.style.backgroundColor = 'blue';
    peerVideoEl.style.position = 'fixed';
    peerVideoEl.style.width = "100px";
    peerVideoEl.style.height = "100px";
    peerVideoEl.style.top = 0;
    peerVideoEl.style.left = (window.innerWidth - parseInt(peerVideoEl.style.width, 10)) + 'px';
    appContainer.appendChild(peerVideoEl);

    // Note: Expose for debug
    peerStream = stream;

    // Attach peer stream to video element
    peerVideoEl.srcObject = peerStream;
  }

  if (useTrackEvent) {
    var peerStream;
    pc2.addEventListener('track', function(e) {
        console.log('pc2.track', e);
        var peerStream = e.streams[0] || new MediaStream();
        setPeerVideoStream(peerStream);
        peerStream.addTrack(e.track);
      });
  } else {

	 pc2.addEventListener('addstream', function(e) {
       console.log('pc2.addStream', e);
       setPeerVideoStream(e.stream);
     });
  }

  pc1.addEventListener('iceconnectionstatechange', function (e) {
    console.log('pc1.iceConnectionState', e, pc1.iceConnectionState);

    if (pc1.iceConnectionState === 'completed') {
      console.log('pc1.getSenders', pc1.getSenders());
      console.log('pc2.getReceivers', pc2.getReceivers());
    }
  });

  pc1.addEventListener('icegatheringstatechange', function (e) {
    console.log('pc1.iceGatheringStateChange', e);
  });

  // https://stackoverflow.com/questions/48963787/failed-to-set-local-answer-sdp-called-in-wrong-state-kstable
  // https://bugs.chromium.org/p/chromium/issues/detail?id=740501
  var isNegotiating = false;
  pc1.addEventListener('signalingstatechange', function (e) {
    console.log('pc1.signalingstatechange',  e);
    isNegotiating = (pc1.signalingState !== "stable");
  });

  pc1.addEventListener('negotiationneeded', function (e) {

    if (isNegotiating) {
      // Should not trigger on iosrtc cause of PluginRTCPeerConnection.swift fix
      console.log("pc1.negotiatioNeeded", "SKIP nested negotiations");
      return;
    }
    isNegotiating = true;

    console.log('pc1.negotiatioNeeded', e);

    return pc1.createOffer().then(function (d) {
      var desc = {
        type: d.type,
        sdp: d.sdp
      };
      console.log('pc1.setLocalDescription', desc);
      return pc1.setLocalDescription(desc);
    }).then(function () {
      var desc = {
        type: pc1.localDescription.type,
        sdp: pc1.localDescription.sdp
      };
      console.log('pc2.setLocalDescription', desc);
      return pc2.setRemoteDescription(desc);
    }).then(function () {
      console.log('pc2.createAnswer');
      return pc2.createAnswer();
    }).then(function (d) {
      var desc = {
        type: d.type,
        sdp: d.sdp
      };
      console.log('pc2.setLocalDescription', desc);
      return pc2.setLocalDescription(d);
    }).then(function () {
      var desc = {
        type: pc2.localDescription.type,
        sdp: pc2.localDescription.sdp
      };
      console.log('pc1.setRemoteDescription', desc);
      return pc1.setRemoteDescription(desc);
    }).then(function () {
      TestMediaRenderCatpure(peerVideoEl);
      TestMediaRenderCatpure(localVideoEl);
      TestPeerDataChannel();
    }).catch(function (err) {
      console.log('pc1.createOfferError', err);
    });
  });
}

var canvasEl;
function TestMediaRenderCatpure(videoEl) {

  if (!canvasEl) {
    canvasEl = document.createElement('canvas');
  }

  var ctx = canvasEl.getContext("2d");
  canvasEl.style.position = 'absolute';
  canvasEl.style.left = 0;
  canvasEl.style.bottom = 0;
  appContainer.appendChild(canvasEl);
  var image = new Image();
  image.addEventListener('load', function() {
    canvasEl.width = image.width;
    canvasEl.height = image.height;
    ctx.drawImage(image, 0, 0);
  });
  localVideoEl.render.save(function (data) {
    image.src = "data:image/jpg;base64," + data;
  });
}

var testMediaRenderCatpureAnimateFrame;
function TestMediaRenderCatpureAnimate() {
  TestMediaRenderCatpure(peerVideoEl);
  testMediaRenderCatpureAnimateFrame = requestAnimationFrame(TestMediaRenderCatpureAnimate);
}

// Disabled to avoid confusion with remoteStream
var cloneStream;
var cloneVideoEl;
function TestPluginMediaStreamClone(mediaStream) {
  cloneVideoEl = document.createElement('video');
  cloneVideoEl.setAttribute('autoplay', 'autoplay');
  cloneVideoEl.setAttribute('playsinline', 'playsinline');
  cloneVideoEl.style.backgroundColor = 'purple';
  cloneVideoEl.style.position = 'absolute';
  cloneVideoEl.style.bottom = 0;
  cloneVideoEl.style.left = 0;
  cloneVideoEl.style.width = "100px";
  cloneVideoEl.style.height = "100px";
  cloneVideoEl.style.transform = "scaleX(-1)";

  cloneStream = mediaStream.clone();
  cloneVideoEl.srcObject = cloneStream;

  appContainer.appendChild(cloneVideoEl);
}

var useWebRTCAdapter = false;

// Expose webrtc-adapter
if (useWebRTCAdapter && typeof window.adapter === 'undefined') {

    // load adapter.js
    var version = 'latest';
    var script = document.createElement("script");
    script.type = "text/javascript";
    //script.src = "adapter-latest.js";
    script.src = "https://webrtc.github.io/adapter/adapter-" + version + ".js";
    script.async = false;
    document.getElementsByTagName("head")[0].appendChild(script);
    script.addEventListener('load', function () {
      console.log('useWebRTCAdapter.loaded', script.src);
      TestGetUserMedia().then(function (localStream) {
        TestRTCPeerConnection(localStream);
      });
    });
} else {
  TestGetUserMedia().then(function (localStream) {
    TestRTCPeerConnection(localStream);
  });
}

<commit_msg>Update renderer-and-libwebrtc-tests.js<commit_after>/* global RTCPeerConnection, MediaStream */
// jshint unused:false

var cordova = window.cordova;

// Expose WebRTC Globals
if (cordova && cordova.plugins && cordova.plugins.iosrtc) {
  cordova.plugins.iosrtc.registerGlobals();
  //cordova.plugins.iosrtc.debug.disable('*', true);
  //cordova.plugins.iosrtc.debug.enable('*', true);
  cordova.plugins.iosrtc.initAudioDevices();
  cordova.plugins.iosrtc.turnOnSpeaker(true);
  cordova.plugins.iosrtc.requestPermission(true, true, function (result) {
    console.log('requestPermission.result', result);
  });
}

//
// Container
//

document.body.innerHTML = "";
var appContainer = document.body;

//
// getUserMedia
//

var localStream;
var localVideoEl;
function TestGetUserMedia() {

  // Note: Support for Multiple TestRTCPeerConnection calls
  if (localStream) {

    // Close local Stream if already connected
    localStream.stop();

    // Clear local video element
    if (localVideoEl.srcObject) {
      localVideoEl.srcObject = null;
      if (localVideoEl.parentNode === appContainer) {
        appContainer.removeChild(localVideoEl);
      }
    }
  }

  localVideoEl = document.createElement('video');
  localVideoEl.setAttribute('autoplay', 'autoplay');
  localVideoEl.setAttribute('playsinline', 'playsinline');

  // Note: Test CSS positioning
  localVideoEl.style.backgroundColor = 'purple'; // Cause zIndex - 1 failure
  localVideoEl.style.position = 'absolute';
  localVideoEl.style.top = 0;
  localVideoEl.style.left = 0;
  localVideoEl.style.width = "100px";
  localVideoEl.style.height = "100px";
  localVideoEl.style.transform = "scaleX(-1)";
  appContainer.appendChild(localVideoEl);

  navigator.mediaDevices.enumerateDevices().then(function (devices) {
      console.log('getMediaDevices.ok', devices);
      devices.forEach(function (device, idx) {
        console.log('getMediaDevices.devices', idx, device.label, device.kind, device.deviceId);
      });
  }, function (err) {
      console.log('getMediaDevices.err', err);
  });

  return navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
    /*
    video: {
      // Test Back Camera
      //deviceId: 'com.apple.avfoundation.avcapturedevice.built-in_video:0'
      //sourceId: 'com.apple.avfoundation.avcapturedevice.built-in_video:0'
      deviceId: {
        exact: 'com.apple.avfoundation.avcapturedevice.built-in_video:0'
      }
    },
    audio: {
      exact: 'Built-In Microphone'
    }*/
  }).then(function (stream) {

    console.log('getUserMedia.stream', stream);
    console.log('getUserMedia.stream.getTracks', stream.getTracks());

    // Note: Expose for debug
    localStream = stream;

    // Attach local stream to video element
    localVideoEl.srcObject = localStream;

    // Test HTML over Video element
    TestPluginMediaStreamRenderer(localVideoEl);

    return localStream;

  }).catch(function (err) {
    console.log('getUserMediaError', err, err.stack);
  });
}

var useAnimateVideo = false;
function TestPluginMediaStreamRenderer(localVideoEl) {

  // Animate video position
  var currentPosition = {
    x: 0,
    y: 0
  };

  var animateTimer;
  function animateVideo() {

    currentPosition.x = currentPosition.x < (window.innerWidth - parseInt(localVideoEl.style.width, 10)) ? currentPosition.x + 1 : 0;
    currentPosition.y = currentPosition.y < (window.innerHeight - parseInt(localVideoEl.style.height, 10)) ? currentPosition.y + 1 : 0;

    localVideoEl.style.top = currentPosition.y + 'px';
    localVideoEl.style.left = currentPosition.x + 'px';

    if (cordova && cordova.plugins && cordova.plugins.iosrtc) {
      //cordova.plugins.iosrtc.refreshVideos();
    }

    return (animateTimer = requestAnimationFrame(animateVideo));
  }

  if (useAnimateVideo) {
    animateTimer = animateVideo();
  }

  //
  // Test Video behind Element
  //

  // Note: Test HTML over video requiring Z-Index -1 and transparent <html> and <body>
  document.body.style.background = "transparent";
  document.documentElement.style.background = "transparent";
  localVideoEl.style.backgroundColor = '';
  localVideoEl.style.zIndex = -1;

  var overEl = document.createElement('button');
  overEl.style.backgroundColor = 'red';
  overEl.style.position = 'absolute';
  overEl.style.left = 0;
  overEl.style.top = 0;
  overEl.style.width = "100px";
  overEl.style.height = "100px";

  overEl.addEventListener('click', function () {
    overEl.style.backgroundColor = overEl.style.backgroundColor === 'red' ? 'green' : 'red';

    if (overEl.style.backgroundColor === 'red') {
      animateTimer = animateVideo();
    } else {
      cancelAnimationFrame(animateTimer);
    }
  });

  appContainer.appendChild(overEl);

  overEl.style.left = ((window.innerWidth / 2)  - parseInt(overEl.style.width, 10)) + 'px';
  overEl.style.top  = ((window.innerHeight / 2) - parseInt(overEl.style.height, 10)) + 'px';

}

//
// Test RTCPeerConnection
//

var peerConnectionConfig = {
    offerToReceiveVideo: true,
    //iceTransportPolicy: 'relay',
    sdpSemantics: 'unified-plan',
    offerToReceiveAudio: true,
    //sdpSemantics: 'plan-b',
    //bundlePolicy: 'max-compat',
    //rtcpMuxPolicy: 'negotiate',
    iceServers: [
        {
            url: "stun:stun.stunprotocol.org"
        }
    ]
};

// This plugin handle 'addstream' and 'track' event for MediaStream creation.
var useTrackEvent = Object.getOwnPropertyDescriptors(RTCPeerConnection.prototype).ontrack;

var pc1 = new RTCPeerConnection(peerConnectionConfig),
    pc2 = new RTCPeerConnection(peerConnectionConfig);

var sendChannel, receiveChannel;
function TestPeerDataChannel() {
  sendChannel = pc1.createDataChannel("sendChannel");

  sendChannel.addEventListener('message',  function (event) {
    console.log('sendChannel.message', event, event.data);
  });

  sendChannel.addEventListener('open', function (event) {
      console.log('sendChannel.open', event);
      sendChannel.send("data message Date: " + new Date());
  });

  sendChannel.addEventListener('close', function (event) {
      console.log('sendChannel.close', event);
  });

  pc2.addEventListener('datachannel', function (event) {
    receiveChannel = event.channel;
    receiveChannel.addEventListener('message',  function (event) {
      console.log('receiveChannel.message', event, event.data);
    });
    receiveChannel.addEventListener('open',  function (event) {
      console.log('receiveChannel.open', event);
    });
    receiveChannel.addEventListener('close',  function (event) {
      console.log('receiveChannel.close', event);
    });
  });
}

var peerVideoEl;
var peerStream;
function TestRTCPeerConnection(localStream) {

  // Note: Support for Multiple TestRTCPeerConnection calls
  if (peerStream) {

    // Close peer Stream if already connected
    peerStream.stop();

    // Clear peer video element
    if (peerVideoEl.srcObject) {
      peerVideoEl.srcObject = null;
      appContainer.removeChild(peerVideoEl);
    }

    // Disconnect peer
    if (pc1.iceConnectionState === 'completed') {
      pc1.close();

      // Current you cannot reuse previous RTCPeerConnection
      pc1 = new RTCPeerConnection();
      pc2 = new RTCPeerConnection();
    }
  }

  if (useTrackEvent) {

    // Add local stream tracks to RTCPeerConnection
    var localPeerStream = new MediaStream();
    localStream.getTracks().forEach(function (track) {
      console.log('pc1.addTrack', track, localPeerStream);
      pc1.addTrack(track, localPeerStream);
    });

  // Note: Deprecated but supported
  } else {
     pc1.addStream(localStream);

     // Note: Deprecated Test removeStream
     // pc1.removeStream(pc1.getLocalStreams()[0]);<
  }

  function onAddIceCandidate(pc, can) {
    console.log('addIceCandidate', pc, can);
    return can && pc.addIceCandidate(can).catch(function (err) {
      console.log('addIceCandidateError', err);
    });
  }

  pc1.addEventListener('icecandidate', function (e) {
    onAddIceCandidate(pc2, e.candidate);
  });

  pc2.addEventListener('icecandidate', function (e) {
    onAddIceCandidate(pc1, e.candidate);
  });

  pc2.addEventListener('track', function (e) {
    console.log('pc2.track', e);
  });

  pc2.addEventListener('removestream', function (e) {
    console.log('pc2.removeStream', e);
  });

  function setPeerVideoStream(stream) {

    peerVideoEl = document.createElement('video');
    peerVideoEl.setAttribute('autoplay', 'autoplay');
    peerVideoEl.setAttribute('playsinline', 'playsinline');

    // Note: Test Object-fix
    peerVideoEl.style.objectFit = "cover";

    // Note: Test CSS positioning
    peerVideoEl.style.backgroundColor = 'blue';
    peerVideoEl.style.position = 'fixed';
    peerVideoEl.style.width = "100px";
    peerVideoEl.style.height = "100px";
    peerVideoEl.style.top = 0;
    peerVideoEl.style.left = (window.innerWidth - parseInt(peerVideoEl.style.width, 10)) + 'px';
    appContainer.appendChild(peerVideoEl);

    // Note: Expose for debug
    peerStream = stream;

    // Attach peer stream to video element
    peerVideoEl.srcObject = peerStream;
  }

  if (useTrackEvent) {
    var peerStream;
    pc2.addEventListener('track', function(e) {
      console.log('pc2.track', e);
      var peerStream = e.streams[0] || new MediaStream();
      setPeerVideoStream(peerStream);
      peerStream.addTrack(e.track);
    });

  // Note: Deprecated but supported
  } else {
    pc2.addEventListener('addstream', function(e) {
	  console.log('pc2.addStream', e);
      setPeerVideoStream(e.stream);
    });
  }

  pc1.addEventListener('iceconnectionstatechange', function (e) {
    console.log('pc1.iceConnectionState', e, pc1.iceConnectionState);

    if (pc1.iceConnectionState === 'completed') {
      console.log('pc1.getSenders', pc1.getSenders());
      console.log('pc2.getReceivers', pc2.getReceivers());
    }
  });

  pc1.addEventListener('icegatheringstatechange', function (e) {
    console.log('pc1.iceGatheringStateChange', e);
  });

  // https://stackoverflow.com/questions/48963787/failed-to-set-local-answer-sdp-called-in-wrong-state-kstable
  // https://bugs.chromium.org/p/chromium/issues/detail?id=740501
  var isNegotiating = false;
  pc1.addEventListener('signalingstatechange', function (e) {
    console.log('pc1.signalingstatechange',  e);
    isNegotiating = (pc1.signalingState !== "stable");
  });

  pc1.addEventListener('negotiationneeded', function (e) {

    if (isNegotiating) {
      // Should not trigger on iosrtc cause of PluginRTCPeerConnection.swift fix
      console.log("pc1.negotiatioNeeded", "SKIP nested negotiations");
      return;
    }
    isNegotiating = true;

    console.log('pc1.negotiatioNeeded', e);

    return pc1.createOffer().then(function (d) {
      var desc = {
        type: d.type,
        sdp: d.sdp
      };
      console.log('pc1.setLocalDescription', desc);
      return pc1.setLocalDescription(desc);
    }).then(function () {
      var desc = {
        type: pc1.localDescription.type,
        sdp: pc1.localDescription.sdp
      };
      console.log('pc2.setLocalDescription', desc);
      return pc2.setRemoteDescription(desc);
    }).then(function () {
      console.log('pc2.createAnswer');
      return pc2.createAnswer();
    }).then(function (d) {
      var desc = {
        type: d.type,
        sdp: d.sdp
      };
      console.log('pc2.setLocalDescription', desc);
      return pc2.setLocalDescription(d);
    }).then(function () {
      var desc = {
        type: pc2.localDescription.type,
        sdp: pc2.localDescription.sdp
      };
      console.log('pc1.setRemoteDescription', desc);
      return pc1.setRemoteDescription(desc);
    }).then(function () {
      TestMediaRenderCatpure(peerVideoEl);
      TestMediaRenderCatpure(localVideoEl);
      TestPeerDataChannel();
    }).catch(function (err) {
      console.log('pc1.createOfferError', err);
    });
  });
}

var canvasEl;
function TestMediaRenderCatpure(videoEl) {

  if (!canvasEl) {
    canvasEl = document.createElement('canvas');
  }

  var ctx = canvasEl.getContext("2d");
  canvasEl.style.position = 'absolute';
  canvasEl.style.left = 0;
  canvasEl.style.bottom = 0;
  appContainer.appendChild(canvasEl);
  var image = new Image();
  image.addEventListener('load', function() {
    canvasEl.width = image.width;
    canvasEl.height = image.height;
    ctx.drawImage(image, 0, 0);
  });
  localVideoEl.render.save(function (data) {
    image.src = "data:image/jpg;base64," + data;
  });
}

var testMediaRenderCatpureAnimateFrame;
function TestMediaRenderCatpureAnimate() {
  TestMediaRenderCatpure(peerVideoEl);
  testMediaRenderCatpureAnimateFrame = requestAnimationFrame(TestMediaRenderCatpureAnimate);
}

// Disabled to avoid confusion with remoteStream
var cloneStream;
var cloneVideoEl;
function TestPluginMediaStreamClone(mediaStream) {
  cloneVideoEl = document.createElement('video');
  cloneVideoEl.setAttribute('autoplay', 'autoplay');
  cloneVideoEl.setAttribute('playsinline', 'playsinline');
  cloneVideoEl.style.backgroundColor = 'purple';
  cloneVideoEl.style.position = 'absolute';
  cloneVideoEl.style.bottom = 0;
  cloneVideoEl.style.left = 0;
  cloneVideoEl.style.width = "100px";
  cloneVideoEl.style.height = "100px";
  cloneVideoEl.style.transform = "scaleX(-1)";

  cloneStream = mediaStream.clone();
  cloneVideoEl.srcObject = cloneStream;

  appContainer.appendChild(cloneVideoEl);
}

var useWebRTCAdapter = false;

// Expose webrtc-adapter
if (useWebRTCAdapter && typeof window.adapter === 'undefined') {

    // load adapter.js
    var version = 'latest';
    var script = document.createElement("script");
    script.type = "text/javascript";
    //script.src = "adapter-latest.js";
    script.src = "https://webrtc.github.io/adapter/adapter-" + version + ".js";
    script.async = false;
    document.getElementsByTagName("head")[0].appendChild(script);
    script.addEventListener('load', function () {
      console.log('useWebRTCAdapter.loaded', script.src);
      TestGetUserMedia().then(function (localStream) {
        TestRTCPeerConnection(localStream);
      });
    });
} else {
  TestGetUserMedia().then(function (localStream) {
    TestRTCPeerConnection(localStream);
  });
}

<|endoftext|>


<commit_before>import {map, baseLayerDict, dragElement, overlay_list} from '../util.js';
import {currentDomain, current_display, current_timestamp, currentSimulation, rasters, raster_base, sorted_timestamps, organization} from './Controller.js';

/**
 * Component that handles adding and removing layers to the map. Provides user with a window
 * to choose different layers available to add.
 */
export class LayerController extends HTMLElement {
    constructor() {
        super();
        this.innerHTML = `
            <link rel="stylesheet" href="css/layerController.css"/>

                    Base Maps

                    Rasters

                    Overlays

        `;
        this.mapType = 'OSM';
        this.currentSimulation = '';
        this.overlayDict = {};
        this.rasterDict = {};
        this.imgCanvas = document.createElement('canvas');
        this.clrBarCanvas = document.createElement('canvas');
        this.displayedColorbar = null; // name of layer currently displaying its colorbar (maybe display multiple cbs?)
        this.displayedColorbars = [];
        this.markerIcon = L.icon({iconUrl: 'icons/square_icon_filled.png', iconSize: [5,5]});
        this.markers = [];
        this.overlayOrder = [];
    }

    /** Disable map events from within the layer selection window to prevent unwanted zooming
     * and panning. */
    connectedCallback() {
        const layerController = this.querySelector('#layer-controller-container');
        dragElement(layerController, '');
        L.DomEvent.disableClickPropagation(layerController);
        L.DomEvent.disableScrollPropagation(layerController);

        currentDomain.subscribe(() => this.domainSwitch());
        current_timestamp.subscribe(() => this.updateMarkers());
        this.buildMapBase();
    }

    /** Called when a layer is selected. */
    handleOverlayadd(name, layer) {
        // register in currently displayed layers and bring to front if it's an overlay
        console.log('name ' + name + ' layer ' + layer);
        layer.addTo(map);
        current_display.getValue()[name] = layer;
        if(overlay_list.indexOf(name) >= 0) {
            layer.bringToFront();
        } else {
            layer.bringToBack();
        }

        // if the overlay being added now has a colorbar and there is none displayed, show it
        var rasters_now = rasters.getValue()[currentDomain.getValue()][current_timestamp.getValue()];
        if('colorbar' in rasters_now[name]) {
            var cb_url = raster_base.getValue() + rasters_now[name].colorbar;
            const rasterColorbar = document.querySelector('#raster-colorbar');
            rasterColorbar.src = cb_url;
            rasterColorbar.style.display = 'block';
            this.displayedColorbar = name;
            this.displayedColorbars.push({name: name, url: cb_url});
            var img = layer._image;
            img.ondblclick = (e) => {
                var latLon = map.mouseEventToLatLng(e);
                e.stopPropagation();
                var popUp = L.popup({closeOnClick: false, autoClose: false}).setLatLng([latLon.lat, latLon.lng]).openOn(map);
                popUp.imageCoords = {layerX: e.layerX /img.width, layerY: e.layerY / img.height};
                this.updateMarker(popUp);
                this.markers.push(popUp);
            }
            img.onload = () => {
                this.drawCanvas(img);
                this.updateMarkers();
            }
            map.on('zoomend', () => this.drawCanvas(img));
            this.drawCanvas(img);
            this.updateMarkers();
        }
        this.overlayOrder.push(layer);
    }

    drawCanvas(img) {
        this.imgCanvas = null;
        if (img != null) {
            this.imgCanvas = document.createElement('canvas');
            this.imgCanvas.width = img.width;
            this.imgCanvas.height = img.height;
            this.imgCanvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
        }
    }

    updateMarkers() {
        this.markers.map(marker => {
            this.updateMarker(marker);
        });
    }

    updateMarker(marker) {
        var popupContent = "No layer bar with colobar to show values of";
        if (this.imgCanvas) {
            var imageCoords = marker.imageCoords;
            var xCoord = Math.floor(imageCoords.layerX * this.imgCanvas.width);
            var yCoord = Math.floor(imageCoords.layerY * this.imgCanvas.height);
            var pixelData = this.imgCanvas.getContext('2d').getImageData(xCoord, yCoord, 1, 1).data;
            popupContent = `<p style="color: rgb(${pixelData[0]}, ${pixelData[1]}, ${pixelData[2]})">R:${pixelData[0]} G:${pixelData[1]} B:${pixelData[2]}</p>`;
        }
        marker.setContent(popupContent);
    }

    /** Called when a new domain is selected or a new simulation is selected. */
    domainSwitch() {
        for(var layerName in current_display.getValue()) {
            this.handleOverlayRemove(layerName, current_display.getValue()[layerName]);
        }
        var prevDisplay = current_display.getValue();
        if (this.currentSimulation != currentSimulation.getValue()) {
            prevDisplay = {};
            this.currentSimulation = currentSimulation.getValue();
            this.querySelector('#layer-controller-container').style.display = 'block';
        }
        current_display.setValue({});
        var first_rasters = rasters.getValue()[currentDomain.getValue()][current_timestamp.getValue()];
        var vars = Object.keys(first_rasters);
        var cs = first_rasters[vars[0]].coords;
        map.fitBounds([ [cs[0][1], cs[0][0]], [cs[2][1], cs[2][0]] ]);

        // build the layer groups
        this.rasterDict = {};
        this.overlayDict = {};
        Object.entries(first_rasters).map(entry => {
            var r = entry[0];
            var raster_info = first_rasters[r];
            var cs = raster_info.coords;
            var layer = L.imageOverlay(raster_base.getValue() + raster_info.raster,
                                        [[cs[0][1], cs[0][0]], [cs[2][1], cs[2][0]]],
                                        {
                                            attribution: organization.getValue(),
                                            opacity: 0.5,
                                            interactive: true
                                        });
            if(r in prevDisplay) current_display.getValue()[r] = layer;
            if(overlay_list.indexOf(r) >= 0) {
                this.overlayDict[r] = layer;
            } else {
                this.rasterDict[r] = layer;
            }
        });
        this.buildLayerBoxes();
    }

    /** Adds checkboxes for the different available map types. Should only be called once after
     * the map has been initialized. */
    buildMapBase() {
        const baseMapDiv = this.querySelector('#map-checkboxes');
        for (const [name, layer] of Object.entries(baseLayerDict)) {
            let mapCheckBox = this.buildMapCheckBox(name, layer);
            baseMapDiv.appendChild(mapCheckBox);
        }
    }

    /** Builds a checkbox for each raster layer and overlay layer */
    buildLayerBoxes() {
        const rasterRegion = this.querySelector('#raster-layers');
        rasterRegion.style.display = 'block';
        if (Object.keys(this.rasterDict).length == 0) rasterRegion.style.display = 'none';

        const overlayRegion = this.querySelector('#overlay-layers');
        overlayRegion.style.display = 'block';
        if (Object.keys(this.overlayDict).length == 0) overlayRegion.style.display = 'none';

        const rasterDiv = this.querySelector('#raster-checkboxes');
        rasterDiv.innerHTML = '';
        const overlayDiv = this.querySelector('#overlay-checkboxes');
        overlayDiv.innerHTML = '';

        [[rasterDiv, this.rasterDict], [overlayDiv, this.overlayDict]].map(([layerDiv, layerDict]) => {
            for (const [name, layer] of Object.entries(layerDict)) {
                let layerBox = this.buildLayerBox(name, layer);
                layerDiv.appendChild(layerBox);
            }
        });
    }

    /** Builds a radio box for each map base that can be chosen */
    buildMapCheckBox(name, layer) {
        let [div, input] = this.buildCheckBox(name);
        input.type = 'radio';
        input.name = 'base';
        input.checked = name == this.mapType;
        input.onclick = () => {
            if (input.checked) {
                layer.addTo(map);
                this.mapType = name;
                layer.bringToFront();
            } else {
                layer.remove(map);
            }
        }
        return div;
    }

    /** Creates the checkbox for a layer. Displays name and when clicked adds layer to the map. base is
     * a boolean that indicates whether creating a checkbox for a map type or a layer.*/
    buildLayerBox(name, layer) {
        let [div, input] = this.buildCheckBox(name, layer);
        input.type = 'checkbox';
        input.name = 'layers';
        input.checked = name in current_display.getValue();
        if (name in current_display.getValue()) {
            this.handleOverlayadd(name, layer);
        }
        input.onclick = () => {
            if (input.checked) this.handleOverlayadd(name, layer);
            else {
                this.handleOverlayRemove(name, layer);
                delete current_display.getValue()[name];
            }
        }
        return div;
    }

    buildCheckBox(name) {
        var div = document.createElement('div');
        div.className = 'layer-checkbox';
        const input = document.createElement('input');
        input.id = name;
        var label = document.createElement('label');
        label.for = name;
        label.innerText = name;
        div.appendChild(input);
        div.appendChild(label);
        return [div, input];
    }

    /** Called when a layer is de-selected. */
    handleOverlayRemove(name, layer) {
        layer.remove(map);
        this.overlayOrder.splice(this.overlayOrder.indexOf(layer), 1)
        this.displayedColorbars = this.displayedColorbars.filter(colorbars => colorbars.name != name);
        const rasterColorbar = document.querySelector('#raster-colorbar');
        var img = null;
        if (this.displayedColorbars.length == 0) {
            rasterColorbar.src = '';
            rasterColorbar.style.display = 'none';
            this.displayedColorbar = null;
        } else {
            let mostRecentColorBar = this.displayedColorbars[this.displayedColorbars.length - 1];
            rasterColorbar.src = mostRecentColorBar.url;
            this.displayedColorbar = mostRecentColorBar.name;
            if (mostRecentColorBar.name in overlay_list) {
                img = this.overlayDict[mostRecentColorBar.name]._image;
            } else {
                img = this.rasterDict[mostRecentColorBar.name]._image;
            }
        }
        this.drawCanvas(img)
    }
}

window.customElements.define('layer-controller', LayerController);<commit_msg>Updates the markers after removing layers<commit_after>import {map, baseLayerDict, dragElement, overlay_list} from '../util.js';
import {currentDomain, current_display, current_timestamp, currentSimulation, rasters, raster_base, sorted_timestamps, organization} from './Controller.js';

/**
 * Component that handles adding and removing layers to the map. Provides user with a window
 * to choose different layers available to add.
 */
export class LayerController extends HTMLElement {
    constructor() {
        super();
        this.innerHTML = `
            <link rel="stylesheet" href="css/layerController.css"/>

                    Base Maps

                    Rasters

                    Overlays

        `;
        this.mapType = 'OSM';
        this.currentSimulation = '';
        this.overlayDict = {};
        this.rasterDict = {};
        this.imgCanvas = document.createElement('canvas');
        this.clrBarCanvas = document.createElement('canvas');
        this.displayedColorbar = null; // name of layer currently displaying its colorbar (maybe display multiple cbs?)
        this.displayedColorbars = [];
        this.markerIcon = L.icon({iconUrl: 'icons/square_icon_filled.png', iconSize: [5,5]});
        this.markers = [];
        this.overlayOrder = [];
    }

    /** Disable map events from within the layer selection window to prevent unwanted zooming
     * and panning. */
    connectedCallback() {
        const layerController = this.querySelector('#layer-controller-container');
        dragElement(layerController, '');
        L.DomEvent.disableClickPropagation(layerController);
        L.DomEvent.disableScrollPropagation(layerController);

        currentDomain.subscribe(() => this.domainSwitch());
        current_timestamp.subscribe(() => this.updateMarkers());
        this.buildMapBase();
    }

    /** Called when a layer is selected. */
    handleOverlayadd(name, layer) {
        // register in currently displayed layers and bring to front if it's an overlay
        console.log('name ' + name + ' layer ' + layer);
        layer.addTo(map);
        current_display.getValue()[name] = layer;
        if(overlay_list.indexOf(name) >= 0) {
            layer.bringToFront();
        } else {
            layer.bringToBack();
        }

        // if the overlay being added now has a colorbar and there is none displayed, show it
        var rasters_now = rasters.getValue()[currentDomain.getValue()][current_timestamp.getValue()];
        if('colorbar' in rasters_now[name]) {
            var cb_url = raster_base.getValue() + rasters_now[name].colorbar;
            const rasterColorbar = document.querySelector('#raster-colorbar');
            rasterColorbar.src = cb_url;
            rasterColorbar.style.display = 'block';
            this.displayedColorbar = name;
            this.displayedColorbars.push({name: name, url: cb_url});
            var img = layer._image;
            img.ondblclick = (e) => {
                var latLon = map.mouseEventToLatLng(e);
                e.stopPropagation();
                var popUp = L.popup({closeOnClick: false, autoClose: false}).setLatLng([latLon.lat, latLon.lng]).openOn(map);
                popUp.imageCoords = {layerX: e.layerX /img.width, layerY: e.layerY / img.height};
                this.updateMarker(popUp);
                this.markers.push(popUp);
            }
            img.onload = () => {
                this.drawCanvas(img);
                this.updateMarkers();
            }
            map.on('zoomend', () => this.drawCanvas(img));
            this.drawCanvas(img);
            this.updateMarkers();
        }
        this.overlayOrder.push(layer);
    }

    drawCanvas(img) {
        this.imgCanvas = null;
        if (img != null) {
            this.imgCanvas = document.createElement('canvas');
            this.imgCanvas.width = img.width;
            this.imgCanvas.height = img.height;
            this.imgCanvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
        }
    }

    updateMarkers() {
        this.markers.map(marker => {
            this.updateMarker(marker);
        });
    }

    updateMarker(marker) {
        var popupContent = "No layer bar with colobar to show values of";
        if (this.imgCanvas) {
            var imageCoords = marker.imageCoords;
            var xCoord = Math.floor(imageCoords.layerX * this.imgCanvas.width);
            var yCoord = Math.floor(imageCoords.layerY * this.imgCanvas.height);
            var pixelData = this.imgCanvas.getContext('2d').getImageData(xCoord, yCoord, 1, 1).data;
            popupContent = `<p style="color: rgb(${pixelData[0]}, ${pixelData[1]}, ${pixelData[2]})">R:${pixelData[0]} G:${pixelData[1]} B:${pixelData[2]}</p>`;
        }
        marker.setContent(popupContent);
    }

    /** Called when a new domain is selected or a new simulation is selected. */
    domainSwitch() {
        for(var layerName in current_display.getValue()) {
            this.handleOverlayRemove(layerName, current_display.getValue()[layerName]);
        }
        var prevDisplay = current_display.getValue();
        if (this.currentSimulation != currentSimulation.getValue()) {
            prevDisplay = {};
            this.currentSimulation = currentSimulation.getValue();
            this.querySelector('#layer-controller-container').style.display = 'block';
        }
        current_display.setValue({});
        var first_rasters = rasters.getValue()[currentDomain.getValue()][current_timestamp.getValue()];
        var vars = Object.keys(first_rasters);
        var cs = first_rasters[vars[0]].coords;
        map.fitBounds([ [cs[0][1], cs[0][0]], [cs[2][1], cs[2][0]] ]);

        // build the layer groups
        this.rasterDict = {};
        this.overlayDict = {};
        Object.entries(first_rasters).map(entry => {
            var r = entry[0];
            var raster_info = first_rasters[r];
            var cs = raster_info.coords;
            var layer = L.imageOverlay(raster_base.getValue() + raster_info.raster,
                                        [[cs[0][1], cs[0][0]], [cs[2][1], cs[2][0]]],
                                        {
                                            attribution: organization.getValue(),
                                            opacity: 0.5,
                                            interactive: true
                                        });
            if(r in prevDisplay) current_display.getValue()[r] = layer;
            if(overlay_list.indexOf(r) >= 0) {
                this.overlayDict[r] = layer;
            } else {
                this.rasterDict[r] = layer;
            }
        });
        this.buildLayerBoxes();
    }

    /** Adds checkboxes for the different available map types. Should only be called once after
     * the map has been initialized. */
    buildMapBase() {
        const baseMapDiv = this.querySelector('#map-checkboxes');
        for (const [name, layer] of Object.entries(baseLayerDict)) {
            let mapCheckBox = this.buildMapCheckBox(name, layer);
            baseMapDiv.appendChild(mapCheckBox);
        }
    }

    /** Builds a checkbox for each raster layer and overlay layer */
    buildLayerBoxes() {
        const rasterRegion = this.querySelector('#raster-layers');
        rasterRegion.style.display = 'block';
        if (Object.keys(this.rasterDict).length == 0) rasterRegion.style.display = 'none';

        const overlayRegion = this.querySelector('#overlay-layers');
        overlayRegion.style.display = 'block';
        if (Object.keys(this.overlayDict).length == 0) overlayRegion.style.display = 'none';

        const rasterDiv = this.querySelector('#raster-checkboxes');
        rasterDiv.innerHTML = '';
        const overlayDiv = this.querySelector('#overlay-checkboxes');
        overlayDiv.innerHTML = '';

        [[rasterDiv, this.rasterDict], [overlayDiv, this.overlayDict]].map(([layerDiv, layerDict]) => {
            for (const [name, layer] of Object.entries(layerDict)) {
                let layerBox = this.buildLayerBox(name, layer);
                layerDiv.appendChild(layerBox);
            }
        });
    }

    /** Builds a radio box for each map base that can be chosen */
    buildMapCheckBox(name, layer) {
        let [div, input] = this.buildCheckBox(name);
        input.type = 'radio';
        input.name = 'base';
        input.checked = name == this.mapType;
        input.onclick = () => {
            if (input.checked) {
                layer.addTo(map);
                this.mapType = name;
                layer.bringToFront();
            } else {
                layer.remove(map);
            }
        }
        return div;
    }

    /** Creates the checkbox for a layer. Displays name and when clicked adds layer to the map. base is
     * a boolean that indicates whether creating a checkbox for a map type or a layer.*/
    buildLayerBox(name, layer) {
        let [div, input] = this.buildCheckBox(name, layer);
        input.type = 'checkbox';
        input.name = 'layers';
        input.checked = name in current_display.getValue();
        if (name in current_display.getValue()) {
            this.handleOverlayadd(name, layer);
        }
        input.onclick = () => {
            if (input.checked) this.handleOverlayadd(name, layer);
            else {
                this.handleOverlayRemove(name, layer);
                delete current_display.getValue()[name];
            }
        }
        return div;
    }

    buildCheckBox(name) {
        var div = document.createElement('div');
        div.className = 'layer-checkbox';
        const input = document.createElement('input');
        input.id = name;
        var label = document.createElement('label');
        label.for = name;
        label.innerText = name;
        div.appendChild(input);
        div.appendChild(label);
        return [div, input];
    }

    /** Called when a layer is de-selected. */
    handleOverlayRemove(name, layer) {
        layer.remove(map);
        this.overlayOrder.splice(this.overlayOrder.indexOf(layer), 1)
        this.displayedColorbars = this.displayedColorbars.filter(colorbars => colorbars.name != name);
        const rasterColorbar = document.querySelector('#raster-colorbar');
        var img = null;
        if (this.displayedColorbars.length == 0) {
            rasterColorbar.src = '';
            rasterColorbar.style.display = 'none';
            this.displayedColorbar = null;
        } else {
            let mostRecentColorBar = this.displayedColorbars[this.displayedColorbars.length - 1];
            rasterColorbar.src = mostRecentColorBar.url;
            this.displayedColorbar = mostRecentColorBar.name;
            if (mostRecentColorBar.name in overlay_list) {
                img = this.overlayDict[mostRecentColorBar.name]._image;
            } else {
                img = this.rasterDict[mostRecentColorBar.name]._image;
            }
        }
        this.drawCanvas(img);
        this.updateMarkers();
    }
}

window.customElements.define('layer-controller', LayerController);<|endoftext|>


<commit_before>var feedhoos = angular.module("feedhoos", ["ngCookies", "ngRoute", 'ngSanitize', "ui.bootstrap", "feedhoosControllers"]);
feedhoos.factory("uiSetter",function(){
    var uiSetter = {
        bar: function() {
            var content_bar = document.getElementById("content_bar");
            var feed_bar = document.getElementById("feed_bar");
            try {
                var content_bar_top = content_bar.getBoundingClientRect().top
                var feed_bar_top = feed_bar.getBoundingClientRect().top
            } catch (e) {
                return
            }
            var innerHeight = window.innerHeight;
            content_bar.style.height = innerHeight - content_bar_top + "px";
            feed_bar.style.height = innerHeight - feed_bar_top + "px";
        },
        timeline_url: function(feed_id) {
            return "/reader/feed/timeline/" + feed_id + "/page/1/";
        },
        feed_url: function(feed_id) {
            return "/reader/feed/" + feed_id + "/page/1/";
        }
    };

    window.addEventListener("resize", uiSetter.bar, false);
    return uiSetter;
});
feedhoos.service("readingManager", ["$http", "$rootScope", function($http, $rootScope){
    var that = this;
    this.readings = null;
    this.request = function() {
        if (this.readings === null) {
            $http.get("/reader/feed/reading/").success(function(data) {
                that.readings = data;
                $rootScope.$broadcast("readings");
            });
        }
        else {
            $rootScope.$broadcast("readings");
        }
    };
}]);
feedhoos.config(["$routeProvider",
    function($routeProvider) {
        $routeProvider.
        when("/reader", {
            templateUrl: "/static/partials/reader.html",
            controller: "ReaderCtrl"
        }).
        when("/reader/list", {
            templateUrl: "/static/partials/list.html",
            controller: "ListCtrl"
        }).
        when("/finder", {
            templateUrl: "/static/partials/finder.html",
            controller: "FinderCtrl"
        }).
        otherwise({
            redirectTo: "/reader"
        });
    }
]);
//        feedhoos.filter("timeline_url", function() {
//            return function(feed_id) {
//                return "/reader/feed/timeline/" + feed_id + "/page/1/"
//            }
//       });
var feedhoosControllers = angular.module("feedhoosControllers", []);
feedhoosControllers.controller("ReaderCtrl", ["$scope", "$routeParams", "$http", "uiSetter", "readingManager",
    function($scope, $routeParams, $http, uiSetter, readingManager) {
        uiSetter.bar()

        $scope.type = "";
        $scope._feed_id = null;
        $scope.feed = null;
        $scope.entries = null;
        $scope.feeds = [];
        $scope.active_feed_id = -1;
        $scope.active_timeline_id = -1;
        $scope.feed_tab = true;
        $scope.timeline_tab = true;
        $http.get("/reader/feed/list/all/").success(function(data) {
            $scope.feeds = data;
        });
        $scope.$on("readings", function() {
            $scope.readings = readingManager.readings;
        });
        readingManager.request();
        $scope.read_timeline = function(feed_id) {
            if ($scope._feed_id == feed_id && $scope.type == "timeline") {
                return;
            }
            $scope._feed_id = feed_id;
            $scope.type = "timeline";
            $scope.active_timeline_id = feed_id;
            var timeline_url = uiSetter.timeline_url(feed_id);
            $http.get(timeline_url).success(function(data) {
                $scope.feed = data.feed;
                $scope.entries = data.entries;
            });
        }
        $scope.read_feed = function(feed_id) {
            if ($scope._feed_id == feed_id && $scope.type == "feed") {
                return;
            }
            $scope._feed_id = feed_id;
            $scope.type = "feed";
            $scope.active_feed_id = feed_id;
            var feed_url = uiSetter.feed_url(feed_id);
            $http.get(feed_url).success(function(data) {
                $scope.feed = data.feed;
                $scope.entries = data.entries;
            });
            $scope.readings.map(function(feed){
                if (feed.id == feed_id) {
                    feed.unread_count = 0;
                }
            });
        }
    }]
)
.directive("scrollTop", function() {
    return {
        restrict: "A",
        link: function(scope, element, attrs, ReaderCtrl) {
            scope.$watch(function() {
                if (scope.feed) {
                    return (scope.feed.id + scope.type);
                }
                else {
                    return scope.feed;
                }
            }, function() {
                element[0].scrollIntoView();
            });
        },
    }
});

feedhoosControllers.controller("ListCtrl", ["$scope", "$http", "$cookies",
    function($scope, $http, $cookies) {
        $http.get("/reader/feed/list/all/").success(function(data) {
            data.shift();
            $scope.feeds = data;
        });
        $http.get("/bookmark/list/").success(function(data) {
            $scope.bookmark = data;
        });
        $scope.remove = function(feed_id) {
            var csrftoken = $cookies.csrftoken;
            $http({
                 "url": "/reader/feed/list/delete/",
                 "method": "POST",
                 "xsrfHeaderName": "X-CSRFToken",
                 "xsrfCookieName": "csrftoken",
                 "headers": {'Content-Type': 'application/x-www-form-urlencoded'},
                 "data": "feed_id=" + feed_id
            }).success(function(data) {
                data.shift();
                $scope.feeds = data;
            });
        }
    }]
);

feedhoosControllers.controller("FinderCtrl", ["$scope", "$http", "$cookies",
    function($scope, $http, $cookies) {
        $scope.state = 1;
        $scope.set_state = function(state) {
            $scope.url = null;
            $scope.feed_urls = null;
            $scope.result = null;
            $scope.state = state;
        }
        $scope.search = function() {
            $scope.state = 2;
            $scope.feed_urls = null;
            var csrftoken = $cookies.csrftoken;
            $http({
                 "url": "/finder/get/",
                 "method": "POST",
                 "xsrfHeaderName": "X-CSRFToken",
                 "xsrfCookieName": "csrftoken",
                 "headers": {'Content-Type': 'application/x-www-form-urlencoded'},
                 "data": "url=" + $scope.url
            }).success(function(data) {
                $scope.feed_urls = data;
            });
        }
        $scope.add = function(fu) {
            $scope.state = 3;
            $scope.result = null;
            var csrftoken = $cookies.csrftoken;
            $http({
                 "url": "/finder/registered/",
                 "method": "POST",
                 "xsrfHeaderName": "X-CSRFToken",
                 "xsrfCookieName": "csrftoken",
                 "headers": {'Content-Type': 'application/x-www-form-urlencoded'},
                 "data": "url=" + fu
            }).success(function(data) {
                $scope.result = data;
            });
        }
    }]
);

feedhoosControllers.controller("RatingCtrl", ["$scope", "$http", "$cookies",
    function($scope, $http, $cookies) {
        $scope.max = 5;
        $scope.isReadonly = false;

        $scope.hoveringOver = function(value) {
            $scope.overStar = value;
        };

        $scope.$watch("rating", function(new_rating, old_rating) {
            if (new_rating == old_rating) {
                return;
            }
            var csrftoken = $cookies.csrftoken;
            $http({
                 "url": "/bookmark/rating/",
                 "method": "POST",
                 "xsrfHeaderName": "X-CSRFToken",
                 "xsrfCookieName": "csrftoken",
                 "headers": {"Content-Type": "application/x-www-form-urlencoded"},
                 "data": ("feed_id=" + $scope.feed_id + "&rating=" + $scope.rating)
            }).success(function(data) {
                $scope.result = data;
            });
        });
    }]
)
.directive("setInitialRating", function() {
    return {
        restrict: "A",
        link: function(scope, element, attrs, ReaderCtrl) {
            scope.rating = scope.bookmark[attrs.feedId + ""].rating;
            scope.feed_id = attrs.feedId;
        },
    }
});

feedhoosControllers.config([
    function() {
    }
]);
<commit_msg>add feed manager service<commit_after>var feedhoos = angular.module("feedhoos", ["ngCookies", "ngRoute", 'ngSanitize', "ui.bootstrap", "feedhoosControllers"]);
feedhoos.factory("uiSetter",function(){
    var uiSetter = {
        bar: function() {
            var content_bar = document.getElementById("content_bar");
            var feed_bar = document.getElementById("feed_bar");
            try {
                var content_bar_top = content_bar.getBoundingClientRect().top
                var feed_bar_top = feed_bar.getBoundingClientRect().top
            } catch (e) {
                return
            }
            var innerHeight = window.innerHeight;
            content_bar.style.height = innerHeight - content_bar_top + "px";
            feed_bar.style.height = innerHeight - feed_bar_top + "px";
        },
        timeline_url: function(feed_id) {
            return "/reader/feed/timeline/" + feed_id + "/page/1/";
        },
        feed_url: function(feed_id) {
            return "/reader/feed/" + feed_id + "/page/1/";
        }
    };

    window.addEventListener("resize", uiSetter.bar, false);
    return uiSetter;
});

feedhoos.service("readingManager", ["$http", "$rootScope", function($http, $rootScope){
    var that = this;
    this.readings = null;
    this.request = function() {
        if (this.readings === null) {
            $http.get("/reader/feed/reading/").success(function(data) {
                that.readings = data;
                $rootScope.$broadcast("readings");
            });
        }
        else {
            $rootScope.$broadcast("readings");
        }
    };
}]);

feedhoos.service("feedManager", ["$http", "$rootScope", function($http, $rootScope){
    var that = this;
    this.feeds = null;
    this.request = function() {
        if (this.feeds === null) {
            $http.get("/reader/feed/list/all/").success(function(data) {
                that.feeds = data;
                $rootScope.$broadcast("feeds");
            });
        }
        else {
            $rootScope.$broadcast("feeds");
        }
    };
}]);

feedhoos.config(["$routeProvider",
    function($routeProvider) {
        $routeProvider.
        when("/reader", {
            templateUrl: "/static/partials/reader.html",
            controller: "ReaderCtrl"
        }).
        when("/reader/list", {
            templateUrl: "/static/partials/list.html",
            controller: "ListCtrl"
        }).
        when("/finder", {
            templateUrl: "/static/partials/finder.html",
            controller: "FinderCtrl"
        }).
        otherwise({
            redirectTo: "/reader"
        });
    }
]);
//        feedhoos.filter("timeline_url", function() {
//            return function(feed_id) {
//                return "/reader/feed/timeline/" + feed_id + "/page/1/"
//            }
//       });
var feedhoosControllers = angular.module("feedhoosControllers", []);
feedhoosControllers.controller(
    "ReaderCtrl",
    ["$scope", "$routeParams", "$http", "uiSetter", "readingManager", "feedManager",
    function($scope, $routeParams, $http, uiSetter, readingManager, feedManager) {
        uiSetter.bar()

        $scope.type = "";
        $scope._feed_id = null;
        $scope.feed = null;
        $scope.entries = null;
        $scope.feeds = [];
        $scope.active_feed_id = -1;
        $scope.active_timeline_id = -1;
        $scope.feed_tab = true;
        $scope.timeline_tab = true;
        $scope.$on("feeds", function() {
            $scope.feeds = feedManager.feeds;
        });
        feedManager.request();
        $scope.$on("readings", function() {
            $scope.readings = readingManager.readings;
        });
        readingManager.request();
        $scope.read_timeline = function(feed_id) {
            if ($scope._feed_id == feed_id && $scope.type == "timeline") {
                return;
            }
            $scope._feed_id = feed_id;
            $scope.type = "timeline";
            $scope.active_timeline_id = feed_id;
            var timeline_url = uiSetter.timeline_url(feed_id);
            $http.get(timeline_url).success(function(data) {
                $scope.feed = data.feed;
                $scope.entries = data.entries;
            });
        }
        $scope.read_feed = function(feed_id) {
            if ($scope._feed_id == feed_id && $scope.type == "feed") {
                return;
            }
            $scope._feed_id = feed_id;
            $scope.type = "feed";
            $scope.active_feed_id = feed_id;
            var feed_url = uiSetter.feed_url(feed_id);
            $http.get(feed_url).success(function(data) {
                $scope.feed = data.feed;
                $scope.entries = data.entries;
            });
            $scope.readings.map(function(feed){
                if (feed.id == feed_id) {
                    feed.unread_count = 0;
                }
            });
        }
    }]
)
.directive("scrollTop", function() {
    return {
        restrict: "A",
        link: function(scope, element, attrs, ReaderCtrl) {
            scope.$watch(function() {
                if (scope.feed) {
                    return (scope.feed.id + scope.type);
                }
                else {
                    return scope.feed;
                }
            }, function() {
                element[0].scrollIntoView();
            });
        },
    }
});

feedhoosControllers.controller("ListCtrl", ["$scope", "$http", "$cookies",
    function($scope, $http, $cookies) {
        $http.get("/reader/feed/list/all/").success(function(data) {
            data.shift();
            $scope.feeds = data;
        });
        $http.get("/bookmark/list/").success(function(data) {
            $scope.bookmark = data;
        });
        $scope.remove = function(feed_id) {
            var csrftoken = $cookies.csrftoken;
            $http({
                 "url": "/reader/feed/list/delete/",
                 "method": "POST",
                 "xsrfHeaderName": "X-CSRFToken",
                 "xsrfCookieName": "csrftoken",
                 "headers": {'Content-Type': 'application/x-www-form-urlencoded'},
                 "data": "feed_id=" + feed_id
            }).success(function(data) {
                data.shift();
                $scope.feeds = data;
            });
        }
    }]
);

feedhoosControllers.controller("FinderCtrl", ["$scope", "$http", "$cookies",
    function($scope, $http, $cookies) {
        $scope.state = 1;
        $scope.set_state = function(state) {
            $scope.url = null;
            $scope.feed_urls = null;
            $scope.result = null;
            $scope.state = state;
        }
        $scope.search = function() {
            $scope.state = 2;
            $scope.feed_urls = null;
            var csrftoken = $cookies.csrftoken;
            $http({
                 "url": "/finder/get/",
                 "method": "POST",
                 "xsrfHeaderName": "X-CSRFToken",
                 "xsrfCookieName": "csrftoken",
                 "headers": {'Content-Type': 'application/x-www-form-urlencoded'},
                 "data": "url=" + $scope.url
            }).success(function(data) {
                $scope.feed_urls = data;
            });
        }
        $scope.add = function(fu) {
            $scope.state = 3;
            $scope.result = null;
            var csrftoken = $cookies.csrftoken;
            $http({
                 "url": "/finder/registered/",
                 "method": "POST",
                 "xsrfHeaderName": "X-CSRFToken",
                 "xsrfCookieName": "csrftoken",
                 "headers": {'Content-Type': 'application/x-www-form-urlencoded'},
                 "data": "url=" + fu
            }).success(function(data) {
                $scope.result = data;
            });
        }
    }]
);

feedhoosControllers.controller("RatingCtrl", ["$scope", "$http", "$cookies",
    function($scope, $http, $cookies) {
        $scope.max = 5;
        $scope.isReadonly = false;

        $scope.hoveringOver = function(value) {
            $scope.overStar = value;
        };

        $scope.$watch("rating", function(new_rating, old_rating) {
            if (new_rating == old_rating) {
                return;
            }
            var csrftoken = $cookies.csrftoken;
            $http({
                 "url": "/bookmark/rating/",
                 "method": "POST",
                 "xsrfHeaderName": "X-CSRFToken",
                 "xsrfCookieName": "csrftoken",
                 "headers": {"Content-Type": "application/x-www-form-urlencoded"},
                 "data": ("feed_id=" + $scope.feed_id + "&rating=" + $scope.rating)
            }).success(function(data) {
                $scope.result = data;
            });
        });
    }]
)
.directive("setInitialRating", function() {
    return {
        restrict: "A",
        link: function(scope, element, attrs, ReaderCtrl) {
            scope.rating = scope.bookmark[attrs.feedId + ""].rating;
            scope.feed_id = attrs.feedId;
        },
    }
});

feedhoosControllers.config([
    function() {
    }
]);
<|endoftext|>


<commit_before>/*!
 * Module dependencies.
 */

var _ = require('underscore'),
	util = require('util'),
	super_ = require('../Type');

var REGEXP_LNGLAT = /^\s*(\d+(?:\.\d+)?)\s*\,\s*(\d+(?:\.\d+)?)\s*$/;

/**
 * Geo FieldType Constructor
 * @extends Field
 * @api public
 */

function geopoint(list, path, options) {

	this._fixedSize = 'medium';

	// TODO: implement filtering, hard-coded as disabled for now
	options.nofilter = true;
	geopoint.super_.call(this, list, path, options);

}

/*!
 * Inherit from Field
 */

util.inherits(geopoint, super_);

/**
 * Registers the field on the List's Mongoose Schema.
 *
 * Adds a 2dsphere indexed lat/lng pair
 *
 * @api public
 */

geopoint.prototype.addToSchema = function() {
	this.list.schema.path(this.path, { type: [Number], index: '2dsphere' });
	this.bindUnderscoreMethods();
};

/**
 * Gets the field's data from an Item, as used by the React components
 */

geopoint.prototype.getData = function(item) {
	var points = item.get(this.path);
	return (points && points.length === 2) ? points : [];
};

/**
 * Formats the field value
 *
 * @api public
 */

geopoint.prototype.format = function(item) {
	return item.get(this.path).join(', ');
};

/**
 * Validates that a value for this field has been provided in a data object
 *
 * @api public
 */

geopoint.prototype.validateInput = function(data, required, item) {

	// Input is valid if the field is not required, and not present
	if (!this.path in data && !required) return true;

	var values = data[this.path];

	if (_.isArray(values)) {
		values = _.compact(values).join(',');
	}

	if (values === '' && !required) return true;

	return REGEXP_LNGLAT.test(values);

};

/**
 * Updates the value for this field in the item from a data object
 *
 * @api public
 */

geopoint.prototype.updateItem = function(item, data) {

	if (!_.isObject(data)) return;

	if (this.path in data && _.isString(data[this.path])) {

		// Value should be formatted lng,lat
		var values = REGEXP_LNGLAT.exec(data[this.path]);

		if (values) {
			item.set(this.path, [values[1], values[2]]);
		} else {
			item.set(this.path, undefined);
		}

	} else if (this.path in data && _.isArray(data[this.path])) {

		var values = data[this.path];

		if (values.length === 2 && REGEXP_LNGLAT.test(_.compact(values).join(','))) {
			item.set(this.path, values);
		} else {
			item.set(this.path, undefined);
		}

	}

};

/*!
 * Export class
 */

exports = module.exports = geopoint;
<commit_msg>Fixing defaults and other options for GeoPoint, ref #969<commit_after>/*!
 * Module dependencies.
 */

var _ = require('underscore'),
	util = require('util'),
	super_ = require('../Type');

var REGEXP_LNGLAT = /^\s*(\d+(?:\.\d+)?)\s*\,\s*(\d+(?:\.\d+)?)\s*$/;

/**
 * Geo FieldType Constructor
 * @extends Field
 * @api public
 */

function geopoint(list, path, options) {

	this._fixedSize = 'medium';

	// TODO: implement filtering, hard-coded as disabled for now
	options.nofilter = true;
	geopoint.super_.call(this, list, path, options);

}

/*!
 * Inherit from Field
 */

util.inherits(geopoint, super_);

/**
 * Registers the field on the List's Mongoose Schema.
 *
 * Adds a 2dsphere indexed lat/lng pair
 *
 * @api public
 */

geopoint.prototype.addToSchema = function() {
	this.list.schema.path(this.path, _.defaults({ type: [Number], index: '2dsphere' }, this.options));
	this.bindUnderscoreMethods();
};

/**
 * Gets the field's data from an Item, as used by the React components
 */

geopoint.prototype.getData = function(item) {
	var points = item.get(this.path);
	return (points && points.length === 2) ? points : [];
};

/**
 * Formats the field value
 *
 * @api public
 */

geopoint.prototype.format = function(item) {
	return item.get(this.path).join(', ');
};

/**
 * Validates that a value for this field has been provided in a data object
 *
 * @api public
 */

geopoint.prototype.validateInput = function(data, required, item) {

	// Input is valid if the field is not required, and not present
	if (!this.path in data && !required) return true;

	var values = data[this.path];

	if (_.isArray(values)) {
		values = _.compact(values).join(',');
	}

	if (values === '' && !required) return true;

	return REGEXP_LNGLAT.test(values);

};

/**
 * Updates the value for this field in the item from a data object
 *
 * @api public
 */

geopoint.prototype.updateItem = function(item, data) {

	if (!_.isObject(data)) return;

	if (this.path in data && _.isString(data[this.path])) {

		// Value should be formatted lng,lat
		var values = REGEXP_LNGLAT.exec(data[this.path]);

		if (values) {
			item.set(this.path, [values[1], values[2]]);
		} else {
			item.set(this.path, undefined);
		}

	} else if (this.path in data && _.isArray(data[this.path])) {

		var values = data[this.path];

		if (values.length === 2 && REGEXP_LNGLAT.test(_.compact(values).join(','))) {
			item.set(this.path, values);
		} else {
			item.set(this.path, undefined);
		}

	}

};

/*!
 * Export class
 */

exports = module.exports = geopoint;
<|endoftext|>


<commit_before>var _ = require('underscore');
var bcrypt = require('bcrypt-nodejs');
var FieldType = require('../Type');
var util = require('util');

/**
 * password FieldType Constructor
 * @extends Field
 * @api public
 */
function password(list, path, options) {
	this._nativeType = String;
	this._underscoreMethods = ['format', 'compare'];
	this._fixedSize = 'large';
	// You can't sort on password fields
	options.nosort = true;
	options.nofilter = true; // TODO: remove this when 0.4 is merged
	this.workFactor = options.workFactor || 10;
	password.super_.call(this, list, path, options);
}
util.inherits(password, FieldType);

/**
 * Registers the field on the List's Mongoose Schema.
 *
 * Adds ...
 *
 * @api public
 */
password.prototype.addToSchema = function() {
	var field = this;
	var schema = this.list.schema;
	var needs_hashing = '__' + field.path + '_needs_hashing';

	this.paths = {
		hash: this.options.hashPath || this._path.append('_hash'),
		confirm: this.options.confirmPath || this._path.append('_confirm')
	};

	schema.path(this.path, _.defaults({
		type: String,
		set: function(newValue) {
			this[needs_hashing] = true;
			return newValue;
		}
	}, this.options));

	schema.virtual(this.paths.hash).set(function(newValue) {
		this.set(field.path, newValue);
		this[needs_hashing] = false;
	});

	schema.pre('save', function(next) {
		if (!this.isModified(field.path) || !this[needs_hashing]) {
			return next();
		}
		if (!this.get(field.path)) {
			this.set(field.path, undefined);
			return next();
		}
		var item = this;
		bcrypt.genSalt(field.workFactor, function(err, salt) {
			if (err) {
				return next(err);
			}
			bcrypt.hash(item.get(field.path), salt, function () {}, function(err, hash) {
				if (err) {
					return next(err);
				}
				// override the cleartext password with the hashed one
				item.set(field.path, hash);
				next();
			});
		});
	});
	this.bindUnderscoreMethods();
};

/**
 * Add filters to a query
 */
password.prototype.addFilterToQuery = function(filter, query) {
	query = query || {};
	query[this.path] = (filter.exists) ? { $ne: null } : null;
	return query;
};

/**
 * Formats the field value
 *
 * Password fields are always formatted as a random no. of asterisks,
 * because the saved hash should never be displayed nor the length
 * of the actual password hinted at.
 *
 * @api public
 */
password.prototype.format = function(item) {
	if (!item.get(this.path)) return '';
	var len = Math.round(Math.random() * 4) + 6;
	var stars = '';
	for (var i = 0; i < len; i++) stars += '*';
	return stars;
};

/**
 * Compares
 *
 * @api public
 */
password.prototype.compare = function(item, candidate, callback) {
	if ('function' !== typeof callback) throw new Error('Password.compare() requires a callback function.');
	var value = item.get(this.path);
	if (!value) return callback(null, false);
	bcrypt.compare(candidate, item.get(this.path), callback);
};

/**
 * If password fields are required, check that either a value has been
 * provided or already exists in the field.
 *
 * Otherwise, input is always considered valid, as providing an empty
 * value will not change the password.
 *
 * @api public
 */
password.prototype.validateInput = function(data, required, item) {
	if (data[this.path] && this.paths.confirm in data) {
		return data[this.path] === data[this.paths.confirm] ? true : false;
	}
	if (data[this.path] || data[this.paths.hash] || (item && item.get(this.path))) return true;
	return required ? false : true;
};

/**
 * Updates the value for this field in the item from a data object
 *
 * Will accept either the field path, or paths.hash to bypass bcrypt
 *
 * @api public
 */
password.prototype.updateItem = function(item, data) {
	if (this.path in data) {
		item.set(this.path, data[this.path]);
	} else if (this.paths.hash in data) {
		item.set(this.paths.hash, data[this.paths.hash]);
	}
};

/* Export Field Type */
exports = module.exports = password;
<commit_msg>#2561 | Inherited Users can't log in<commit_after>var _ = require('underscore');
var bcrypt = require('bcrypt-nodejs');
var FieldType = require('../Type');
var util = require('util');

/**
 * password FieldType Constructor
 * @extends Field
 * @api public
 */
function password(list, path, options) {
	this._nativeType = String;
	this._underscoreMethods = ['format', 'compare'];
	this._fixedSize = 'large';
	// You can't sort on password fields
	options.nosort = true;
	options.nofilter = true; // TODO: remove this when 0.4 is merged
	this.workFactor = options.workFactor || 10;
	password.super_.call(this, list, path, options);
}
util.inherits(password, FieldType);

/**
 * Registers the field on the List's Mongoose Schema.
 *
 * Adds ...
 *
 * @api public
 */
password.prototype.addToSchema = function() {
	var field = this;
	var schema = this.list.schema;
	var needs_hashing = '__' + field.path + '_needs_hashing';

	this.paths = {
		hash: this.options.hashPath || this._path.append('_hash'),
		confirm: this.options.confirmPath || this._path.append('_confirm')
	};

	schema.path(this.path, _.defaults({
		type: String,
		set: function(newValue) {
			this[needs_hashing] = true;
			return newValue;
		}
	}, this.options));

	schema.virtual(this.paths.hash).set(function(newValue) {
		this.set(field.path, newValue);
		this[needs_hashing] = false;
	});

	schema.pre('save', function(next) {
		if (!this.isModified(field.path) || !this[needs_hashing]) {
			return next();
		}
		if (!this.get(field.path)) {
			this.set(field.path, undefined);
			return next();
		}
		var item = this;
		if(!item.isEncrypted) {
            bcrypt.genSalt(field.workFactor, function(err, salt) {
                if (err) {
                    return next(err);
                }
                bcrypt.hash(item.get(field.path), salt, function () {}, function(err, hash) {
                    if (err) {
                        return next(err);
                    }
                    // override the cleartext password with the hashed one
                    item.set(field.path, hash);
                    // inherited models save twice, this ensures the encrypted password hash won't be encrypted a second time
                    item.isEncrypted = true;
                    next();
                });
            });
        } else {
            return next();
        }
	});
	this.bindUnderscoreMethods();
};

/**
 * Add filters to a query
 */
password.prototype.addFilterToQuery = function(filter, query) {
	query = query || {};
	query[this.path] = (filter.exists) ? { $ne: null } : null;
	return query;
};

/**
 * Formats the field value
 *
 * Password fields are always formatted as a random no. of asterisks,
 * because the saved hash should never be displayed nor the length
 * of the actual password hinted at.
 *
 * @api public
 */
password.prototype.format = function(item) {
	if (!item.get(this.path)) return '';
	var len = Math.round(Math.random() * 4) + 6;
	var stars = '';
	for (var i = 0; i < len; i++) stars += '*';
	return stars;
};

/**
 * Compares
 *
 * @api public
 */
password.prototype.compare = function(item, candidate, callback) {
	if ('function' !== typeof callback) throw new Error('Password.compare() requires a callback function.');
	var value = item.get(this.path);
	if (!value) return callback(null, false);
	bcrypt.compare(candidate, item.get(this.path), callback);
};

/**
 * If password fields are required, check that either a value has been
 * provided or already exists in the field.
 *
 * Otherwise, input is always considered valid, as providing an empty
 * value will not change the password.
 *
 * @api public
 */
password.prototype.validateInput = function(data, required, item) {
	if (data[this.path] && this.paths.confirm in data) {
		return data[this.path] === data[this.paths.confirm] ? true : false;
	}
	if (data[this.path] || data[this.paths.hash] || (item && item.get(this.path))) return true;
	return required ? false : true;
};

/**
 * Updates the value for this field in the item from a data object
 *
 * Will accept either the field path, or paths.hash to bypass bcrypt
 *
 * @api public
 */
password.prototype.updateItem = function(item, data) {
	if (this.path in data) {
		item.set(this.path, data[this.path]);
	} else if (this.paths.hash in data) {
		item.set(this.paths.hash, data[this.paths.hash]);
	}
};

/* Export Field Type */
exports = module.exports = password;
<|endoftext|>


<commit_before>/* global SunCalc */

async function dayNight(day, night) {
  // syntax: dayNight((function() { day opts }), (function() { night opts }));

  if (!document.body) {
    return window.requestAnimationFrame(() => dayNight(day, night));
  }

  // check if location is secure (needed for getCurrentPosition in auto mode)
  const secure = window.location.protocol === 'https:';

  // stores settings
  const store = localStorage.daynight;

  const settings = (async () => {
    const mode = (!secure && 'manual') || (store && JSON.parse(store).mode) || 'auto';

    const darkened = (async () => {
      if (mode === 'auto') {
        await fetch('https://cdn.rawgit.com/mourner/suncalc/master/suncalc.js')
          .then((git) => {
            if (git.status === 200) { return git.text(); }
            throw new Error(`${git.status}. Couldn't fetch suncalc from git.`);
          })
          .then(eval)
          .catch(window.console.error);

        return new Promise(
          geo => navigator.geolocation.getCurrentPosition(geo),
        ).then(
          (pos) => {
            const { latitude: lat, longitude: lon } = pos.coords;
            const date = new Date().getTime();

            const times = SunCalc.getTimes(date, lat, lon);

            return (() => {
              switch (true) {
                case (date > times.sunset.getTime()): return true;
                case (date > times.sunrise.getTime()): return false;
                case (date < times.sunrise.getTime()): return true;
                default: return false;
              }
            })();
          },
          err => window.console.warn(err),
        );
      }
      return store ? JSON.parse(store).darkened : false;
      // if mode is not auto then look into store or set default(false)
    })();
    darkened.then(e => window.console.log(e));
    return { mode, darkened };
  })();
  // settings.then(e => e.darkened.then(a => console.log(e.mode, a)));

  settings.then(s => s.darkened.then((theme) => {
    let darkened = theme;
    let mode = s.mode;

    const lights = {
      on() { day.bind(document).apply(); },
      off() { night.bind(document).apply(); },
    };

    const icons = { sun: "\u2600\uFE0E", moon: "\u263D" };

    const btn = document.body.insertBefore(
      document.createElement("div"),
      document.body.firstElementChild,
    );
    btn.setAttribute("id", "night_btn");
    Object.assign(btn.style, {
      color: 'inherit',
      cursor: 'pointer',
      font: '32px/24px "Open Sans"',
      opacity: '0.6',
      position: 'fixed',
      zIndex: String(1e+4),
    });

    const switchingCSS = () => ({
      bottom: `${darkened ? 10 : 8}px`,
      left: `${darkened ? 38 : 44}px`,
      transform: (darkened ? '' : 'rotate(45deg)'),
    });

    const switchStyle = () => {
      darkened ? lights.off() : lights.on();
      btn.setAttribute("title", `Toggle ${darkened ? "day" : "night"} mode.`);
      btn.innerText = (darkened ? icons.sun : icons.moon);
      Object.assign(btn.style, switchingCSS());
    };
    // changing scope
    switchStyle();

    let int;
    const changeOpacity = (e) => {
      clearInterval(int);
      const opacityMax = ((/enter/).test(e) ? 1.1 : 0.5);
      const opacity = () => Math.round(
        (parseFloat(btn.style.opacity) + ((/enter/).test(e) ? 0.1 : -0.1)) * 10
      ) / 10;
      // if mouseenter increment opacity, else - decrement; round for correct parsing
      int = setInterval(() => {
        if (opacity() === opacityMax) {
          clearInterval(int);
        } else {
          btn.style.opacity = String(opacity());
        }
      }, 40);
    };

    ['mouseenter', 'mouseleave'].forEach(e => btn.addEventListener(e, () => changeOpacity(e)));

    btn.onclick = () => {
      darkened = !darkened;
      switchStyle();
    };

    const modebtn = document.body.insertBefore(
      document.createElement("div"),
      btn.nextElementSibling,
    );
    modebtn.setAttribute('id', 'night_btn_mode');
    Object.assign(modebtn.style, {
      color: 'red',
      cursor: 'pointer',
      position: 'fixed',
      zIndex: String(1e+4),
      bottom: '35px',
      left: '38px',
    });
    const modebtnText = () => `change mode to ${mode === 'auto' ? 'manual' : 'auto'}`;
    modebtn.innerText = modebtnText();
    modebtn.onclick = () => {
      mode = (mode === 'auto') ? 'manual' : 'auto';
      modebtn.innerText = modebtnText();
    };
    try {
      waitress();
    } catch (e) {
      fetch('https://rawgit.com/HK49/tampermonkey-plugins/master/libs/waitress.js')
        .then((git) => {
          if (git.status === 200) { return git.text(); }
          throw new Error(`${git.status}. Couldn't fetch waitress from git.`);
        })
        .then(eval)
        .catch(window.console.error);
    }
    waitress.style({
      '#night_btn_mode': {
        display: 'none',
      },
      '#night_btn_mode:hover, #night_btn:hover + #night_btn_mode': {
        display: 'block',
      },
    });

    window.addEventListener(
      "beforeunload",
      () => (localStorage.daynight = JSON.stringify({ mode, darkened })),
    );
    // store mode for staying the same in night/day mode;
  }));
}
<commit_msg>daynight: speed up page reflow; wait body only to init btns<commit_after>/* global SunCalc */

async function dayNight(day, night) {
  // syntax: dayNight((function() { day opts }), (function() { night opts }));

  // check if location is secure (needed for getCurrentPosition in auto mode)
  const secure = window.location.protocol === 'https:';

  // stores settings
  const store = localStorage.daynight;

  const lights = {
    on() { day.bind(document).apply(); },
    off() { night.bind(document).apply(); },
  };

  // before fetching - retrieve darkened value to force premature reflow
  if (store) {
    JSON.parse(store).darkened ? lights.off() : lights.on();
  }

  const settings = (async () => {
    const mode = (!secure && 'manual') || (store && JSON.parse(store).mode) || 'auto';

    const darkened = (async () => {
      if (mode === 'auto') {
        await fetch('https://cdn.rawgit.com/mourner/suncalc/master/suncalc.js')
          .then((git) => {
            if (git.status === 200) { return git.text(); }
            throw new Error(`${git.status}. Couldn't fetch suncalc from git.`);
          })
          .then(eval)
          .catch(window.console.error);

        return new Promise(
          geo => navigator.geolocation.getCurrentPosition(geo),
        ).then(
          (pos) => {
            const { latitude: lat, longitude: lon } = pos.coords;
            const date = new Date().getTime();

            const times = SunCalc.getTimes(date, lat, lon);

            return (() => {
              switch (true) {
                case (date > times.sunset.getTime()): return true;
                case (date > times.sunrise.getTime()): return false;
                case (date < times.sunrise.getTime()): return true;
                default: return false;
              }
            })();
          },
          err => window.console.warn(err),
        );
      }
      return store ? JSON.parse(store).darkened : false;
      // if mode is not auto then look into store or set default(false)
    })();
    return { mode, darkened };
  })();
  // settings.then(e => e.darkened.then(a => console.log(e.mode, a)));

  settings.then(s => s.darkened.then((theme) => {
    let darkened = theme;
    let mode = s.mode;

    darkened ? lights.off() : lights.on();

    (function btns() {
      if (!document.body) {
        return window.requestAnimationFrame(btns);
      }
      const icons = { sun: "\u2600\uFE0E", moon: "\u263D" };

      const btn = document.body.insertBefore(
        document.createElement("div"),
        document.body.firstElementChild,
      );
      btn.setAttribute("id", "night_btn");
      Object.assign(btn.style, {
        color: 'inherit',
        cursor: 'pointer',
        font: '32px/24px "Open Sans"',
        opacity: '0.6',
        position: 'fixed',
        zIndex: String(1e+4),
      });

      const switchingCSS = () => ({
        bottom: `${darkened ? 10 : 8}px`,
        left: `${darkened ? 38 : 44}px`,
        transform: (darkened ? '' : 'rotate(45deg)'),
      });

      const switchStyle = () => {
        darkened ? lights.off() : lights.on();
        btn.setAttribute("title", `Toggle ${darkened ? "day" : "night"} mode.`);
        btn.innerText = (darkened ? icons.sun : icons.moon);
        Object.assign(btn.style, switchingCSS());
      };
      // changing scope
      switchStyle();

      let int;
      const changeOpacity = (e) => {
        clearInterval(int);
        const opacityMax = ((/enter/).test(e) ? 1.1 : 0.5);
        const opacity = () => Math.round(
          (parseFloat(btn.style.opacity) + ((/enter/).test(e) ? 0.1 : -0.1)) * 10
        ) / 10;
        // if mouseenter increment opacity, else - decrement; round for correct parsing
        int = setInterval(() => {
          if (opacity() === opacityMax) {
            clearInterval(int);
          } else {
            btn.style.opacity = String(opacity());
          }
        }, 40);
      };

      ['mouseenter', 'mouseleave'].forEach(e => btn.addEventListener(e, () => changeOpacity(e)));

      btn.onclick = () => {
        darkened = !darkened;
        switchStyle();
        // should add if clicked - change mode to manual?
      };

      const modebtn = document.body.insertBefore(
        document.createElement("div"),
        btn.nextElementSibling,
      );
      modebtn.setAttribute('id', 'night_btn_mode');
      Object.assign(modebtn.style, {
        color: 'red',
        cursor: 'pointer',
        position: 'fixed',
        zIndex: String(1e+4),
        bottom: '35px',
        left: '38px',
      });
      const modebtnText = () => `change mode to ${mode === 'auto' ? 'manual' : 'auto'}`;
      modebtn.innerText = modebtnText();
      modebtn.onclick = () => {
        mode = (mode === 'auto') ? 'manual' : 'auto';
        modebtn.innerText = modebtnText();
      };
      try {
        waitress();
      } catch (e) {
        fetch('https://rawgit.com/HK49/tampermonkey-plugins/master/libs/waitress.js')
          .then((git) => {
            if (git.status === 200) { return git.text(); }
            throw new Error(`${git.status}. Couldn't fetch waitress from git.`);
          })
          .then(eval)
          .catch(window.console.error);
      }
      waitress.style({
        '#night_btn_mode': {
          display: 'none',
        },
        '#night_btn_mode:hover, #night_btn:hover + #night_btn_mode': {
          display: 'block',
        },
      });

      window.addEventListener(
        "beforeunload",
        () => (localStorage.daynight = JSON.stringify({ mode, darkened })),
      );
      // store mode for staying the same in night/day mode;
    })();
  }));
}
<|endoftext|>