<commit_before>'use strict';

var ObjectId = require("mongodb").ObjectId;
var should = require('should');
var helper = require("../../helper");
var Manager = require("../../../src/managers/garment-master-plan/booking-order-manager");
var manager = null;
var dataUtil =require("../../data-util/garment-master-plan/booking-order-data-util");
var validate = require("dl-models").validator.garmentMasterPlan.bookingOrder;

var moment = require('moment');

before('#00. connect db', function (done) {
    helper.getDb()
        .then(db => {
            manager = new Manager(db, {
                username: 'unit-test'
            });
            done();
        })
        .catch(e => {
            done(e);
        })
});

it("#01. should error when create new data with empty data", function (done) {
    manager.create({})
        .then((id) => {
            done("should error when create new data with empty data");
        })
        .catch((e) => {
            e.name.should.equal("ValidationError");
            e.should.have.property("errors");
            e.errors.should.instanceof(Object);
            done();
        });
});

it("#02. should error when create new data with no items", function (done) {
    dataUtil.getNewData()
        .then((data) => {
            data.items = [];
            manager.create(data)
                .then((id) => {
                    done("should error when create new data with no items");
                })
                .catch((e) => {
                    e.name.should.equal("ValidationError");
                    e.should.have.property("errors");
                    e.errors.should.instanceof(Object);
                    e.errors.should.have.property("items");
                    done();
                });
        })
        .catch((e) => {
            done(e);
        });
});

it("#03. should error when create new data with deliveryDate < bookingDate ", function (done) {
    dataUtil.getNewData()
        .then((data) => {
            var targetDate=new Date();
            data.deliveryDate=new Date(targetDate.setDate(targetDate.getDate() - 10));
            manager.create(data)
                .then((id) => {
                    done("should error when create new data with deliveryDate < bookingDate");
                })
                .catch((e) => {
                    e.name.should.equal("ValidationError");
                    e.should.have.property("errors");
                    e.errors.should.instanceof(Object);
                    e.errors.should.have.property("deliveryDate");
                    done();
                });
        })
        .catch((e) => {
            done(e);
        });
});

it("#04. should error when create new data with orderQuantity not equal total quantity in items", function (done) {
    dataUtil.getNewData()
        .then((data) => {
            data.orderQuantity=2000;
            manager.create(data)
                .then((id) => {
                    done("should error when create new data with date greater than today");
                })
                .catch((e) => {
                    e.name.should.equal("ValidationError");
                    e.should.have.property("errors");
                    e.errors.should.instanceof(Object);
                    e.errors.should.have.property("orderQuantity");
                    done();
                });
        })
        .catch((e) => {
            done(e);
        });
});
<commit_msg>fix unit test<commit_after>'use strict';

var ObjectId = require("mongodb").ObjectId;
var should = require('should');
var helper = require("../../helper");
var Manager = require("../../../src/managers/garment-master-plan/booking-order-manager");
var manager = null;
var dataUtil =require("../../data-util/garment-master-plan/booking-order-data-util");
var validate = require("dl-models").validator.garmentMasterPlan.bookingOrder;

var moment = require('moment');

before('#00. connect db', function (done) {
    helper.getDb()
        .then(db => {
            manager = new Manager(db, {
                username: 'unit-test'
            });
            done();
        })
        .catch(e => {
            done(e);
        })
});

it("#01. should error when create new data with empty data", function (done) {
    manager.create({})
        .then((id) => {
            done("should error when create new data with empty data");
        })
        .catch((e) => {
            e.name.should.equal("ValidationError");
            e.should.have.property("errors");
            e.errors.should.instanceof(Object);
            done();
        });
});

// it("#02. should error when create new data with no items", function (done) {
//     dataUtil.getNewData()
//         .then((data) => {
//             data.items = [];
//             manager.create(data)
//                 .then((id) => {
//                     done("should error when create new data with no items");
//                 })
//                 .catch((e) => {
//                     e.name.should.equal("ValidationError");
//                     e.should.have.property("errors");
//                     e.errors.should.instanceof(Object);
//                     e.errors.should.have.property("items");
//                     done();
//                 });
//         })
//         .catch((e) => {
//             done(e);
//         });
// });

it("#02. should error when create new data with deliveryDate < bookingDate ", function (done) {
    dataUtil.getNewData()
        .then((data) => {
            var targetDate=new Date();
            data.deliveryDate=new Date(targetDate.setDate(targetDate.getDate() - 10));
            manager.create(data)
                .then((id) => {
                    done("should error when create new data with deliveryDate < bookingDate");
                })
                .catch((e) => {
                    e.name.should.equal("ValidationError");
                    e.should.have.property("errors");
                    e.errors.should.instanceof(Object);
                    e.errors.should.have.property("deliveryDate");
                    done();
                });
        })
        .catch((e) => {
            done(e);
        });
});

// it("#04. should error when create new data with orderQuantity not equal total quantity in items", function (done) {
//     dataUtil.getNewData()
//         .then((data) => {
//             data.orderQuantity=2000;
//             manager.create(data)
//                 .then((id) => {
//                     done("should error when create new data with date greater than today");
//                 })
//                 .catch((e) => {
//                     e.name.should.equal("ValidationError");
//                     e.should.have.property("errors");
//                     e.errors.should.instanceof(Object);
//                     e.errors.should.have.property("orderQuantity");
//                     done();
//                 });
//         })
//         .catch((e) => {
//             done(e);
//         });
// });
<|endoftext|>


<commit_before>(function(){
	angular
		.module('podstationBackgroundApp')
		.factory('valueHandlerService', ['$injector', '$interval', '$q', 'messageService', 'analyticsService', 'lightningService', 'podcastIndexOrgService', valueHandlerService]);

	function valueHandlerService($injector, $interval, $q, messageService, _analyticsService, lightningService, podcastIndexOrgService) {

		const PODSTATION_LIGHTNING_NODE_ID = '033868c219bdb51a33560d854d500fe7d3898a1ad9e05dd89d0007e11313588500';
		const LNPAY_WALLET_IT_CUSTOM_RECORD_KEY = 696969;
		const PODSTATION_LNPAY_WALLET_ID = 'wal_WMh3MmyNvUoAN';

		const unsettledValues = [];
		var lightningOptions = {};

		/**
		 * cached podcast value
		 */
		const podcastsValueCache = {};

		messageService.for('audioPlayer').onMessage('segmentPlayed', (playedSegment) => handlePlayedSegment(playedSegment));
		messageService.for('lightningService').onMessage('optionsChanged', (options) => {lightningOptions = options});

		lightningService.getOptions().then((options) => {lightningOptions = options});

		$interval(settleValues, 60000);

		return {};

		/**
		 *
		 * @param {EpisodePlayedSegment} playedSegment
		 */
		function handlePlayedSegment(playedSegment) {
			console.debug('valueHandlerService - handling played segment', playedSegment);

			if(!lightningService.isActive())
				return;

			getLightningEpisodeValue(playedSegment.episodeId).then((valueConfiguration) => {
				if(valueConfiguration) {
					const msatsPerSecond = lightningOptions.value / 3600.0;
					const segmentValue = msatsPerSecond * (playedSegment.endPosition - playedSegment.startPosition);

					const proratedValues = prorateSegmentValue(segmentValue, valueConfiguration);

					cumulateAddressValues(proratedValues);
				}
			});
		}

		/**
		 *
		 * @param {EpisodeId} episodeId
		 */
		function getLightningEpisodeValue(episodeId) {
			const deferred = $q.defer();
			const podcastAndEpisode = getPodcastAndEpisode(episodeId);

			const podcast = podcastAndEpisode.podcast;

			const value = podcast.values && podcast.values.find((value) => value.type === 'lightning');

			if(value) {
				deferred.resolve(value)
			}
			else if(typeof podcastsValueCache[podcast.url] !== 'undefined'){
				deferred.resolve(podcastsValueCache[podcast.url]);
			}
			else {
				podcastIndexOrgService.getPodcastValue(podcast.url).then((response) => {
					if(response.status === 200 && response.data.value.model.type === 'lightning') {
						const valueFromPodcastIndexOrg = processValueFromPodcastIndexOrg(response.data.value);

						podcastsValueCache[podcast.url] = valueFromPodcastIndexOrg;
						deferred.resolve(valueFromPodcastIndexOrg);
					}
					else {
						podcastsValueCache[podcast.url] = null;
						deferred.resolve();
					}
				});
			}

			return deferred.promise;
		}

		/**
		 * Convert a value block returned from podcastindex.org API into the
		 * value block storage format.
		 */
		function processValueFromPodcastIndexOrg(value) {
			return {
				type: value.model.type,
				method: value.model.method,
				suggested: value.model.suggested,
				recipients: value.destinations.map((destination) => {
					return {
						name: destination.name,
						type: destination.type,
						address: destination.address,
						split: destination.split
					}
				})
			};
		}

		function prorateSegmentValue(segmentValue, valueConfiguration) {
			const splitSum = valueConfiguration.recipients.reduce((accumulator, recipient) => accumulator + recipient.split, 0);
			const appRate = 0.01;
			// const appRate = 0.01;
			const normalizerMultiple = (1 - appRate) / splitSum;
			const proratedSegmentValues = valueConfiguration.recipients.map((recipient) => {
				return {
					address: recipient.address,
					value: segmentValue * recipient.split * normalizerMultiple
				}
			});

			if(appRate) {
				proratedSegmentValues.push({
					address: PODSTATION_LIGHTNING_NODE_ID,
					customRecordKey: LNPAY_WALLET_IT_CUSTOM_RECORD_KEY,
					customRecordValue: PODSTATION_LNPAY_WALLET_ID,
					value: segmentValue * appRate
				});
			}

			return proratedSegmentValues;
		}

		function cumulateAddressValues(valuePerAddresses) {
			console.debug('valueHandlerService - values to cumulate', JSON.stringify(valuePerAddresses, null, 2))

			valuePerAddresses.forEach(valuePerAddress => {
				var unsettledValueForAddress = unsettledValues.find((unsettledValue) => isSameRecipient(unsettledValue, valuePerAddress));

				if(unsettledValueForAddress) {
					unsettledValueForAddress.value += valuePerAddress.value;
				}
				else {
					unsettledValues.push(valuePerAddress);
				}
			});

			console.debug('valueHandlerService - cumulated values', JSON.stringify(unsettledValues, null, 2));
		}

		function getPodcastAndEpisode(episodeId) {
			return $injector.get('podcastManager').getPodcastAndEpisode(episodeId);
		}

		function settleValues() {
			const valuesToSettle = unsettledValues.splice(0, unsettledValues.length);

			console.debug('valueHandlerService - will try to settle values', JSON.stringify(valuesToSettle, null, 2));

			valuesToSettle.forEach((valueToSettle) => {
				lightningService.sendPaymentWithKeySend(valueToSettle.address, valueToSettle.value, valueToSettle.customRecordKey, valueToSettle.customRecordValue)
				.catch((error) => {
					cumulateAddressValues([valueToSettle]);
				});
			});
		}

		function isSameRecipient(valuePerAddress1, valuePerAddress2) {
			return valuePerAddress1.address === valuePerAddress2.address &&
				   valuePerAddress1.customRecordKey === valuePerAddress2.customRecordKey &&
				   valuePerAddress1.customRecordValue === valuePerAddress2.customRecordValue;
		}
	}
})();<commit_msg>Removed inactive code<commit_after>(function(){
	angular
		.module('podstationBackgroundApp')
		.factory('valueHandlerService', ['$injector', '$interval', '$q', 'messageService', 'analyticsService', 'lightningService', 'podcastIndexOrgService', valueHandlerService]);

	function valueHandlerService($injector, $interval, $q, messageService, _analyticsService, lightningService, podcastIndexOrgService) {

		const PODSTATION_LIGHTNING_NODE_ID = '033868c219bdb51a33560d854d500fe7d3898a1ad9e05dd89d0007e11313588500';
		const LNPAY_WALLET_IT_CUSTOM_RECORD_KEY = 696969;
		const PODSTATION_LNPAY_WALLET_ID = 'wal_WMh3MmyNvUoAN';

		const unsettledValues = [];
		var lightningOptions = {};

		/**
		 * cached podcast value
		 */
		const podcastsValueCache = {};

		messageService.for('audioPlayer').onMessage('segmentPlayed', (playedSegment) => handlePlayedSegment(playedSegment));
		messageService.for('lightningService').onMessage('optionsChanged', (options) => {lightningOptions = options});

		lightningService.getOptions().then((options) => {lightningOptions = options});

		$interval(settleValues, 60000);

		return {};

		/**
		 *
		 * @param {EpisodePlayedSegment} playedSegment
		 */
		function handlePlayedSegment(playedSegment) {
			console.debug('valueHandlerService - handling played segment', playedSegment);

			if(!lightningService.isActive())
				return;

			getLightningEpisodeValue(playedSegment.episodeId).then((valueConfiguration) => {
				if(valueConfiguration) {
					const msatsPerSecond = lightningOptions.value / 3600.0;
					const segmentValue = msatsPerSecond * (playedSegment.endPosition - playedSegment.startPosition);

					const proratedValues = prorateSegmentValue(segmentValue, valueConfiguration);

					cumulateAddressValues(proratedValues);
				}
			});
		}

		/**
		 *
		 * @param {EpisodeId} episodeId
		 */
		function getLightningEpisodeValue(episodeId) {
			const deferred = $q.defer();
			const podcastAndEpisode = getPodcastAndEpisode(episodeId);

			const podcast = podcastAndEpisode.podcast;

			const value = podcast.values && podcast.values.find((value) => value.type === 'lightning');

			if(value) {
				deferred.resolve(value)
			}
			else if(typeof podcastsValueCache[podcast.url] !== 'undefined'){
				deferred.resolve(podcastsValueCache[podcast.url]);
			}
			else {
				podcastIndexOrgService.getPodcastValue(podcast.url).then((response) => {
					if(response.status === 200 && response.data.value.model.type === 'lightning') {
						const valueFromPodcastIndexOrg = processValueFromPodcastIndexOrg(response.data.value);

						podcastsValueCache[podcast.url] = valueFromPodcastIndexOrg;
						deferred.resolve(valueFromPodcastIndexOrg);
					}
					else {
						podcastsValueCache[podcast.url] = null;
						deferred.resolve();
					}
				});
			}

			return deferred.promise;
		}

		/**
		 * Convert a value block returned from podcastindex.org API into the
		 * value block storage format.
		 */
		function processValueFromPodcastIndexOrg(value) {
			return {
				type: value.model.type,
				method: value.model.method,
				suggested: value.model.suggested,
				recipients: value.destinations.map((destination) => {
					return {
						name: destination.name,
						type: destination.type,
						address: destination.address,
						split: destination.split
					}
				})
			};
		}

		function prorateSegmentValue(segmentValue, valueConfiguration) {
			const splitSum = valueConfiguration.recipients.reduce((accumulator, recipient) => accumulator + recipient.split, 0);
			const appRate = 0.01;
			const normalizerMultiple = (1 - appRate) / splitSum;
			const proratedSegmentValues = valueConfiguration.recipients.map((recipient) => {
				return {
					address: recipient.address,
					value: segmentValue * recipient.split * normalizerMultiple
				}
			});

			if(appRate) {
				proratedSegmentValues.push({
					address: PODSTATION_LIGHTNING_NODE_ID,
					customRecordKey: LNPAY_WALLET_IT_CUSTOM_RECORD_KEY,
					customRecordValue: PODSTATION_LNPAY_WALLET_ID,
					value: segmentValue * appRate
				});
			}

			return proratedSegmentValues;
		}

		function cumulateAddressValues(valuePerAddresses) {
			console.debug('valueHandlerService - values to cumulate', JSON.stringify(valuePerAddresses, null, 2))

			valuePerAddresses.forEach(valuePerAddress => {
				var unsettledValueForAddress = unsettledValues.find((unsettledValue) => isSameRecipient(unsettledValue, valuePerAddress));

				if(unsettledValueForAddress) {
					unsettledValueForAddress.value += valuePerAddress.value;
				}
				else {
					unsettledValues.push(valuePerAddress);
				}
			});

			console.debug('valueHandlerService - cumulated values', JSON.stringify(unsettledValues, null, 2));
		}

		function getPodcastAndEpisode(episodeId) {
			return $injector.get('podcastManager').getPodcastAndEpisode(episodeId);
		}

		function settleValues() {
			const valuesToSettle = unsettledValues.splice(0, unsettledValues.length);

			console.debug('valueHandlerService - will try to settle values', JSON.stringify(valuesToSettle, null, 2));

			valuesToSettle.forEach((valueToSettle) => {
				lightningService.sendPaymentWithKeySend(valueToSettle.address, valueToSettle.value, valueToSettle.customRecordKey, valueToSettle.customRecordValue)
				.catch((error) => {
					cumulateAddressValues([valueToSettle]);
				});
			});
		}

		function isSameRecipient(valuePerAddress1, valuePerAddress2) {
			return valuePerAddress1.address === valuePerAddress2.address &&
				   valuePerAddress1.customRecordKey === valuePerAddress2.customRecordKey &&
				   valuePerAddress1.customRecordValue === valuePerAddress2.customRecordValue;
		}
	}
})();<|endoftext|>


<commit_before>// toilet-paper-icon_32 from Rokey (http://www.iconarchive.com/show/smooth-icons-by-rokey/toilet-paper-icon.html)
// 48-fork-and-knife-icon by Glyphish (http://glyphish.com/)
// test
// jpm run --binary-args http://www.just-eat.co.uk/area/nn1-northampton

function ShowElement(element) {
	element.style.display = 'none';
}

function HideElement(element) {
	element.style.display = '';
}

function AppendImg(element, filename) {
    var img = document.createElement('img');
    img.src = self.options.prefixDataURI + filename;
    element.append(img);
}

function ApplyFilter(ratingFilterRange, restaurantEntries, excludeNoData) {
	excludeNoData = typeof excludeNoData !== 'undefined' ? excludeNoData : true;
	Array.prototype.forEach.call(restaurantEntries, function (el, i) {
		var ratingElement = el.querySelectorAll('div#nomorvom[data-rating]');
		if (ratingElement.length) {
			var rating = ratingElement[0].getAttribute('data-rating');
			//if ( ((rating == -1) && excludeNoData) || (rating < ratingFilterRange[0]) || (rating > ratingFilterRange[1]) ) {
			if ( (rating < ratingFilterRange[0]) || (rating > ratingFilterRange[1]) ) {
				ShowElement(el);
			}
			else { HideElement(el); }
		}
		else { HideElement(el); }
	});
}

var restaurantEntries = document.querySelectorAll('div.restaurant:not(.offlineRestaurant)');

var config = document.createElement('div');
config.id = "nomorvom_config"
config.style.border = "thin dashed red";
config.style.padding = "5px 10px 25px 10px";
config.style.margin = "5px";

var sliderLabel = document.createElement('p');
sliderLabel.id = "nomorvom_config_title";
sliderLabel.appendChild(document.createTextNode("Move the sliders to filter results by hygiene rating:"));
config.appendChild(sliderLabel);

/*
var scoreFilterSlider2 = document.createElement('div');
scoreFilterSlider2.id = "scoreFilter";

noUiSlider.create(scoreFilterSlider2, {
	start: [0, 5],
	orientation: 'vertical',
	connect: true,
	step: 1,
	height: '400px',
	pips: {
		mode: 'steps',
		orientation: 'horizontal'
	},
	range: {
		'min' : 0,
		'max' : 5
	}
});*/

var scoreFilterSlider = document.createElement('div');
scoreFilterSlider.id = "scoreFilterSlider";

$(scoreFilterSlider).slider({
	range: true,
	values: [0, 5],
	min: 0,
	max: 5,
	step: 1,
	slide: function( event, ui ) {
		ApplyFilter(ui.values, restaurantEntries);
	}
});

//
// Add labels to slider whose values
// are specified by min, max and whose
// step is set to 1
//

// Get the number of possible values
var vals = $(scoreFilterSlider).slider("option", "max") - $(scoreFilterSlider).slider("option", "min");

// Space out values
for (var i = 0; i <= vals; i++) {
	var el = $(''+(i)+'').css('left',(i/vals*100)+'%');

	$(scoreFilterSlider).append(el);
}

config.appendChild(scoreFilterSlider);

var excludeNoDataLabel = document.createElement('p');
excludeNoDataLabel.id = "nomorvom_config_title";
excludeNoDataLabel.style.padding = "20px 0px";
excludeNoDataLabel.appendChild(document.createTextNode("Exclude 'No Result' Entries:"));

var excludeNoDataCheckbox = document.createElement('input');
excludeNoDataCheckbox.type = "checkbox"
excludeNoDataCheckbox.id = "nomorvom_config_excludeNoData";
$(excludeNoDataCheckbox).prop('checked', true);
$(excludeNoDataCheckbox).change(function() {
	ApplyFilter($(scoreFilterSlider).slider("values"), restaurantEntries, $(excludeNoDataCheckbox).prop('checked'));
});
excludeNoDataLabel.appendChild(excludeNoDataCheckbox);

//config.appendChild(excludeNoDataLabel);

$("div.restaurants").prepend(config);

// Set up the listener for the result returned from the addon script
self.port.on("restaurantScore", function(restaurantScore) {
	//console.log("id " + restaurantScore.id + ", rating " + restaurantScore.rating);
	// find the score placeholder for the restaurant we've got a result for
	var restaurantScorePlaceholder = $("div.restaurant[data-nomorvom-id='"+restaurantScore.id+"'] div#nomorvom");
	restaurantScorePlaceholder.attr("data-rating", restaurantScore.rating);
	$("p#nomorvom_loading", restaurantScorePlaceholder).remove();
	$("div#nomorvom_progressbar", restaurantScorePlaceholder).remove();

	if (restaurantScore.rating > 0) {
		for (var i = 0; i < restaurantScore.rating; i++) {
			AppendImg(restaurantScorePlaceholder, '48-fork-and-knife-icon.png');
		}
		for (var i = 0; i < 5 - restaurantScore.rating; i++) {
			AppendImg(restaurantScorePlaceholder, 'toilet-paper-icon_32.png');
		}
	}

	var resultText = document.createElement('div');
	resultText.id = "hygieneScore"
	resultText.style.fontWeight = "bold";
	resultText.style.margin = "0px 5px";

	if (restaurantScore.rating == "AwaitingInspection") {
		$(resultText).text("This takeaway is awaiting inspection");
		restaurantScore.rating = 0;
	}
	else {
		if (restaurantScore.rating == -1) {
			$(resultText).text("Sorry, no food hygiene data found");
		}
		else {
			$(resultText).text("Hygiene Score : " + restaurantScore.rating + "/5");
		}
	}
	restaurantScorePlaceholder.append(resultText);

	// Filter accordingly
	ApplyFilter($(scoreFilterSlider).slider("values"), restaurantEntries, $(excludeNoDataCheckbox).prop('checked'));
});

var restaurantId = 0;

Array.prototype.forEach.call(restaurantEntries, function (el, i) {

    var name = el.querySelector('h2.name a').textContent.trim();
    var address = el.querySelector('p.address').childNodes[0].textContent.trim();

    self.port.emit("queryRestaurant", {id:restaurantId, name:name, address:address});

    var scorePlaceholder = document.createElement('div');
	scorePlaceholder.id = "nomorvom";
	scorePlaceholder.style.border = "thin dashed red";
    scorePlaceholder.style.padding = "5px";
	scorePlaceholder.style.margin = "5px";
	scorePlaceholder.width = "50%";

	var loadingText = document.createElement('p');
	loadingText.id = "nomorvom_loading";
	loadingText.style.fontWeight = "bold";
	loadingText.style.padding = "0px 5px";
	loadingText.textContent = "Loading food scores...";

    var loaderImg = document.createElement('div');
	loaderImg.id = "nomorvom_progressbar";
	$(loaderImg).progressbar({
      value: false
	});

	scorePlaceholder.appendChild(loadingText);
	scorePlaceholder.appendChild(loaderImg);

	scorePlaceholder.setAttribute('data-rating', 0);

	el.setAttribute('data-nomorvom-id', restaurantId);

    el.appendChild(scorePlaceholder);

    restaurantId++;
});<commit_msg>All jQuery except progress bar and slider now gone, need to find replacement controls.<commit_after>// toilet-paper-icon_32 from Rokey (http://www.iconarchive.com/show/smooth-icons-by-rokey/toilet-paper-icon.html)
// 48-fork-and-knife-icon by Glyphish (http://glyphish.com/)
// test
// jpm run --binary-args http://www.just-eat.co.uk/area/nn1-northampton

function ShowElement(element) {
	element.style.display = 'none';
}

function HideElement(element) {
	element.style.display = '';
}

function RemoveElement(elementSelector, parentElement) {
	parentElement = typeof parentElement !== 'undefined' ? parentElement : document;
	var el = parentElement.querySelector(elementSelector);
	el.parentNode.removeChild(el);
}

function AppendImg(element, filename) {
    var img = document.createElement('img');
    img.src = self.options.prefixDataURI + filename;
    element.appendChild(img);
}

function ApplyFilter(ratingFilterRange, restaurantEntries, excludeNoData) {
	excludeNoData = typeof excludeNoData !== 'undefined' ? excludeNoData : true;
	Array.prototype.forEach.call(restaurantEntries, function (el, i) {
		var ratingElement = el.querySelectorAll('div#nomorvom[data-rating]');
		if (ratingElement.length) {
			var rating = ratingElement[0].getAttribute('data-rating');
			//if ( ((rating == -1) && excludeNoData) || (rating < ratingFilterRange[0]) || (rating > ratingFilterRange[1]) ) {
			if ( (rating < ratingFilterRange[0]) || (rating > ratingFilterRange[1]) ) {
				ShowElement(el);
			}
			else { HideElement(el); }
		}
		else { HideElement(el); }
	});
}

var restaurantEntries = document.querySelectorAll('div.restaurant:not(.offlineRestaurant)');

var config = document.createElement('div');
config.id = "nomorvom_config"
config.style.border = "thin dashed red";
config.style.padding = "5px 10px 25px 10px";
config.style.margin = "5px";

var sliderLabel = document.createElement('p');
sliderLabel.id = "nomorvom_config_title";
sliderLabel.appendChild(document.createTextNode("Move the sliders to filter results by hygiene rating:"));
config.appendChild(sliderLabel);

/*
var scoreFilterSlider2 = document.createElement('div');
scoreFilterSlider2.id = "scoreFilter";

noUiSlider.create(scoreFilterSlider2, {
	start: [0, 5],
	orientation: 'vertical',
	connect: true,
	step: 1,
	height: '400px',
	pips: {
		mode: 'steps',
		orientation: 'horizontal'
	},
	range: {
		'min' : 0,
		'max' : 5
	}
});*/

var scoreFilterSlider = document.createElement('div');
scoreFilterSlider.id = "scoreFilterSlider";

$(scoreFilterSlider).slider({
	range: true,
	values: [0, 5],
	min: 0,
	max: 5,
	step: 1,
	slide: function( event, ui ) {
		ApplyFilter(ui.values, restaurantEntries);
	}
});

//
// Add labels to slider whose values
// are specified by min, max and whose
// step is set to 1
//

// Get the number of possible values
var vals = $(scoreFilterSlider).slider("option", "max") - $(scoreFilterSlider).slider("option", "min");

// Space out values
for (var i = 0; i <= vals; i++) {
	var el = $(''+(i)+'').css('left',(i/vals*100)+'%');

	$(scoreFilterSlider).append(el);
}

config.appendChild(scoreFilterSlider);

var excludeNoDataLabel = document.createElement('p');
excludeNoDataLabel.id = "nomorvom_config_title";
excludeNoDataLabel.style.padding = "20px 0px";
excludeNoDataLabel.appendChild(document.createTextNode("Exclude 'No Result' Entries:"));

/*
var excludeNoDataCheckbox = document.createElement('input');
excludeNoDataCheckbox.type = "checkbox"
excludeNoDataCheckbox.id = "nomorvom_config_excludeNoData";
$(excludeNoDataCheckbox).prop('checked', true);
$(excludeNoDataCheckbox).change(function() {
	ApplyFilter($(scoreFilterSlider).slider("values"), restaurantEntries, $(excludeNoDataCheckbox).prop('checked'));
});
excludeNoDataLabel.appendChild(excludeNoDataCheckbox);
*/

//config.appendChild(excludeNoDataLabel);

var restaurantsDiv = document.querySelector("div.restaurants");
restaurantsDiv.insertBefore(config, restaurantsDiv.firstChild);

// Set up the listener for the result returned from the addon script
self.port.on("restaurantScore", function(restaurantScore) {
	// find the score placeholder for the restaurant we've got a result for
	var restaurantScorePlaceholder = document.querySelector("div.restaurant[data-nomorvom-id='"+restaurantScore.id+"'] div#nomorvom"); //$("div.restaurant[data-nomorvom-id='"+restaurantScore.id+"'] div#nomorvom");
	restaurantScorePlaceholder.setAttribute('data-rating', restaurantScore.rating);
	RemoveElement('p#nomorvom_loading', restaurantScorePlaceholder);
	RemoveElement('div#nomorvom_progressbar', restaurantScorePlaceholder);

	if (restaurantScore.rating > 0) {
		for (var i = 0; i < restaurantScore.rating; i++) {
			AppendImg(restaurantScorePlaceholder, '48-fork-and-knife-icon.png');
		}
		for (var i = 0; i < 5 - restaurantScore.rating; i++) {
			AppendImg(restaurantScorePlaceholder, 'toilet-paper-icon_32.png');
		}
	}

	var resultText = document.createElement('div');
	resultText.id = "hygieneScore"
	resultText.style.fontWeight = "bold";
	resultText.style.margin = "0px 5px";

	if (restaurantScore.rating == "AwaitingInspection") {
		resultText.textContent = "This takeaway is awaiting inspection";
		restaurantScore.rating = 0;
	}
	else {
		if (restaurantScore.rating == -1) {
			resultText.textContent = "Sorry, no food hygiene data found";
		}
		else {
			resultText.textContent = "Hygiene Score : " + restaurantScore.rating + "/5";
		}
	}
	restaurantScorePlaceholder.appendChild(resultText);

	// Filter accordingly
	ApplyFilter($(scoreFilterSlider).slider("values"), restaurantEntries, true); // $(excludeNoDataCheckbox).getAttribute('checked'));
});

var restaurantId = 0;

Array.prototype.forEach.call(restaurantEntries, function (el, i) {

    var name = el.querySelector('h2.name a').textContent.trim();
    var address = el.querySelector('p.address').childNodes[0].textContent.trim();

    self.port.emit("queryRestaurant", {id:restaurantId, name:name, address:address});

    var scorePlaceholder = document.createElement('div');
	scorePlaceholder.id = "nomorvom";
	scorePlaceholder.style.border = "thin dashed red";
    scorePlaceholder.style.padding = "5px";
	scorePlaceholder.style.margin = "5px";
	scorePlaceholder.width = "50%";

	var loadingText = document.createElement('p');
	loadingText.id = "nomorvom_loading";
	loadingText.style.fontWeight = "bold";
	loadingText.style.padding = "0px 5px";
	loadingText.textContent = "Loading food scores...";

    var loaderImg = document.createElement('div');
	loaderImg.id = "nomorvom_progressbar";
	$(loaderImg).progressbar({
      value: false
	});

	scorePlaceholder.appendChild(loadingText);
	scorePlaceholder.appendChild(loaderImg);

	scorePlaceholder.setAttribute('data-rating', 0);

	el.setAttribute('data-nomorvom-id', restaurantId);

    el.appendChild(scorePlaceholder);

    restaurantId++;
});<|endoftext|>


<commit_before>define(function (require, exports, module) { // jshint ignore:line
    'use strict';

    var $ = require('jquery');
    var Extend = require('structurejs/util/Extend');
    var DOMElement = require('structurejs/display/DOMElement');

    /**
     * The {{#crossLink "Stage"}}{{/crossLink}} class should be extended by your main or root class.
     * @example
     // This example illustrates how to setup your main or root class when extending the {{#crossLink "Stage"}}{{/crossLink}} class.
        define(function (require, exports, module) {
            'use strict';

            var Extend = require('structurejs/util/Extend');
            var Stage = require('replace/path/Stage');

            var MainClass = (function () {

            var _super = Extend(MainClass, Stage);

                function MainClass() {
                    _super.call(this);

                    this.CLASS_NAME = 'MainClass';
                }

                MainClass.prototype.createChildren = function () {
                    _super.prototype.createChildren.call(this);

                    // Create and add your child objects to this parent class.
                }

                MainClass.prototype.layoutChildren = function () {
                    // Layout or update the child objects in this parent class.

                    return this;
                }

                MainClass.prototype.enable = function () {
                    if (this.isEnabled === true) return this;

                    // Enable the child objects and add any event listeners.

                    return _super.prototype.enable.call(this);
                }

                MainClass.prototype.disable = function () {
                    if (this.isEnabled === false) return this;

                    // Disable the child objects and remove any event listeners.

                    return _super.prototype.disable.call(this);
                }

                MainClass.prototype.destroy = function () {
                    _super.prototype.destroy.call(this);

                    // Destroy the child objects and references in this parent class to prevent memory leaks.
                }

                return MainClass;
            })();

            module.exports = MainClass;
        });
     *
     * Instantiation Example

     * This example illustrates how to instantiation your main or root class.
     *
     *      var app = new MainClass();
     *      app.appendTo('body');
     *
     * @class Stage
     * @extends DOMElement
     * @module StructureJS
     * @submodule view
     * @constructor
     * @version 0.1.0
     **/
    var Stage = (function () {

        var _super = Extend(Stage, DOMElement);

        function Stage() {
            _super.call(this);
            /**
             * @overridden DOMElement.CLASS_NAME
             */
            this.CLASS_NAME = 'Stage';
        }

        /**
         * The selected HTML element where all the child elements will be created. This method also starts the lifecycle of the application.
         *
         * @method appendTo
         * @param type {string} A string value that you want the your code appended too. This can be an element id (#some-id), element class (.some-class) or a element tag (body).
         * @param [enabled=true] {boolean} Sets the enabled state of the object.
         * @chainable
         */
        Stage.prototype.appendTo = function (type, enabled) {
            if (typeof enabled === "undefined") { enabled = true; }
            this.$element = jQuery(type);
            this.$element.attr('data-cid', this.cid);

            if (!this.isCreated) {
                this.createChildren();
                this.isCreated = true;
                this.layoutChildren();
            }

            if (enabled) {
                this.enable();
            }

            return this;
        };

        return Stage;
    })();

    module.exports = Stage;

});
<commit_msg>Update Stage.<commit_after>define(function (require, exports, module) { // jshint ignore:line
    'use strict';

    var $ = require('jquery');
    var Extend = require('structurejs/util/Extend');
    var DOMElement = require('structurejs/display/DOMElement');

    /**
     * The {{#crossLink "Stage"}}{{/crossLink}} class should be extended by your main or root class.
     * @example
     // This example illustrates how to setup your main or root class when extending the {{#crossLink "Stage"}}{{/crossLink}} class.
        define(function (require, exports, module) {
            'use strict';

            var Extend = require('structurejs/util/Extend');
            var Stage = require('replace/path/Stage');

            var MainClass = (function () {

            var _super = Extend(MainClass, Stage);

                function MainClass() {
                    _super.call(this);

                    this.CLASS_NAME = 'MainClass';
                }

                MainClass.prototype.createChildren = function () {
                    _super.prototype.createChildren.call(this);

                    // Create and add your child objects to this parent class.
                }

                MainClass.prototype.layoutChildren = function () {
                    // Layout or update the child objects in this parent class.

                    return this;
                }

                MainClass.prototype.enable = function () {
                    if (this.isEnabled === true) return this;

                    // Enable the child objects and add any event listeners.

                    return _super.prototype.enable.call(this);
                }

                MainClass.prototype.disable = function () {
                    if (this.isEnabled === false) return this;

                    // Disable the child objects and remove any event listeners.

                    return _super.prototype.disable.call(this);
                }

                MainClass.prototype.destroy = function () {
                    _super.prototype.destroy.call(this);

                    // Destroy the child objects and references in this parent class to prevent memory leaks.
                }

                return MainClass;
            })();

            module.exports = MainClass;
        });
     *
     * Instantiation Example

     * This example illustrates how to instantiation your main or root class.
     *
     *      var app = new MainClass();
     *      app.appendTo('body');
     *
     * @class Stage
     * @extends DOMElement
     * @module StructureJS
     * @submodule view
     * @constructor
     * @version 0.1.0
     **/
    var Stage = (function () {

        var _super = Extend(Stage, DOMElement);

        function Stage() {
            _super.call(this);
            /**
             * @overridden DOMElement.CLASS_NAME
             */
            this.CLASS_NAME = 'Stage';
        }

        /**
         * The selected HTML element where all the child elements will be created. This method also starts the lifecycle of the application.
         *
         * @method appendTo
         * @param type {string} A string value that you want the your code appended too. This can be an element id (#some-id), element class (.some-class) or a element tag (body).
         * @param [enabled=true] {boolean} Sets the enabled state of the object.
         * @chainable
         */
        Stage.prototype.appendTo = function (type, enabled) {
            if (typeof enabled === "undefined") { enabled = true; }
            this.$element = jQuery(type);
            this.$element.attr('data-cid', this.cid);

            if (!this.isCreated) {
                this.createChildren();
                this.isCreated = true;
                this.layoutChildren();
            }

            if (enabled === false)
            {
                this.disable();
            }
            else
            {
                this.enable();
            }

            return this;
        };

        return Stage;
    })();

    module.exports = Stage;

});
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

define(function (require, exports) {
    "use strict";

    var referenceBy = require("src/lib/reference").wrapper("document"),
        unitsIn = require("src/lib/unit");

    /**
     * Open a document (psd, png, jpg, ai, gif)
     *
     * @param {ActionDescriptor} sourceRef document reference
     * @param {string} name The name of the Document.
     * @param {number} bitDepth The bit depth. 8 or 16
     * @param {string} box Box to crop to. See openDocument.cropTo vals
     * @param {boolean} bAntiAlias true or false
     * @param {boolean} bConstrainProportions true or false
     * @param {number} width The width of the image size.
     * @param {number} height The height of the image size
     * @param {string} colorSpace The color space of the image mode.  See openDocument.mode vals
     * @param {number} pageNumber The number of the page
     * @param {number} resolution The resolution value
     * @param {boolean} suppressWarnings true or false
     *
     * @return {PlayObject}
     *
     */
    var openDocument = function (sourceRef, name, bitDepth, box, bAntiAlias, bConstrainProportions, width, height,
        colorSpace, pageNumber, resolution, suppressWarnings) {
        var fileType = sourceRef.path.lastIndexOf(".");
        if ((fileType === "psd") || (fileType === "jpg") || (fileType === "png")) {
            return {
                command: "open",
                descriptor: {
                    "null": sourceRef
                }
            };
        } else if (fileType === "ai") {
            return {
                command: "open",
                descriptor: {
                    "null": sourceRef,
                    "as": {
                        "obj": "PDFGenericFormat",
                        "value": {
                            "antiAlias": bAntiAlias,
                            "constrainProportions": bConstrainProportions,
                            "crop": {
                                "enum": "cropTo",
                                "value": openDocument.cropTo[box]
                            },
                            "depth": bitDepth,
                            "width":  unitsIn.pixels(width),
                            "height": unitsIn.pixels(height),
                            "mode": {
                                "enum": "colorSpace",
                                "value": openDocument.mode[colorSpace]
                            },
                            "name": name,
                            "pageNumber": pageNumber,
                            "resolution": unitsIn.density(resolution),
                            "suppressWarnings": suppressWarnings,
                            "selection": {
                                "enum": "pdfSelection",
                                "value": "page"
                            }
                        }
                    }
                }
            };
        }
    };

    openDocument.cropTo = {
        bounding: "boundingBox",
        media: "mediaBox",
        crop: "cropBox",
        bleed: "bleedBox",
        trim: "trimBox",
        art: "artBox"
    };

    openDocument.mode = {
        rgb: "RGBColor",
        gray: "grayscaleMode",
        cmyk: "CMYKColorEnum",
        lab: "labColor"
    };

    /**
     * Close a document without saving
     *
     * @param {string} save Whether the document should be saved. "yes", "no"
     *
     * @return {PlayObject}
     *
     * Preconditions:
     * The document should be saved previously and have fileReference path value for saving.
     */
    var closeDocument = function (save) {
        return {
            command: "close",
            "saving": {
                "enum": "yesNo",
                "value": save
            }
        };
    };

    /**
     * Save a document
     *
     *
     */
    var saveDocument = function () {
        //console.log("not implemented.");
    };

    /**
     * Select a document
     *
     * @param {ActionDescriptor} sourceRef document reference
     *
     * @return {PlayObject}
     *
     */
    var selectDocument = function (sourceRef) {
        return {
            command: "select",
            descriptor: {
                "null": sourceRef
            }
        };
    };

    exports.referenceBy = referenceBy;

    exports.open = openDocument;
    exports.close = closeDocument;
    exports.save = saveDocument;
    exports.select = selectDocument;

});
<commit_msg>Added image option for pdfSelection.<commit_after>/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

define(function (require, exports) {
    "use strict";

    var referenceBy = require("src/lib/reference").wrapper("document"),
        unitsIn = require("src/lib/unit");

    /**
     * Open a document (psd, png, jpg, ai, gif)
     *
     * @param {ActionDescriptor} sourceRef document reference
     * @param {string} pdfSelection "page" or "image"
     * @param {number} pageNumber The number of the page
     * @param {boolean} suppressWarnings true or false
     * @param {string} name The name of the Document.
     * @param {number} bitDepth The bit depth. 8 or 16
     * @param {string} box Box to crop to. See openDocument.cropTo vals
     * @param {boolean} bAntiAlias true or false
     * @param {boolean} bConstrainProportions true or false
     * @param {number} width The width of the image size.
     * @param {number} height The height of the image size
     * @param {string} colorSpace The color space of the image mode.  See openDocument.mode vals
     * @param {number} resolution The resolution value
     *
     * @return {PlayObject}
     *
     */
    var openDocument = function (sourceRef, pdfSelection, pageNumber, suppressWarnings, name, bitDepth, box, bAntiAlias,
        bConstrainProportions, width, height, colorSpace, resolution) {
        var fileType = sourceRef.path.lastIndexOf(".");
        if ((fileType === "psd") || (fileType === "jpg") || (fileType === "png")) {
            return {
                command: "open",
                descriptor: {
                    "null": sourceRef
                }
            };
        } else if (fileType === "ai") {
            if (pdfSelection === "page") {
                return {
                    command: "open",
                    descriptor: {
                        "null": sourceRef,
                        "as": {
                            "obj": "PDFGenericFormat",
                            "value": {
                                "antiAlias": bAntiAlias,
                                "constrainProportions": bConstrainProportions,
                                "crop": {
                                    "enum": "cropTo",
                                    "value": openDocument.cropTo[box]
                                },
                                "depth": bitDepth,
                                "width":  unitsIn.pixels(width),
                                "height": unitsIn.pixels(height),
                                "mode": {
                                    "enum": "colorSpace",
                                    "value": openDocument.mode[colorSpace]
                                },
                                "name": name,
                                "pageNumber": pageNumber,
                                "resolution": unitsIn.density(resolution),
                                "suppressWarnings": suppressWarnings,
                                "selection": {
                                    "enum": "pdfSelection",
                                    "value": pdfSelection
                                }
                            }
                        }
                    }
                };
            } else if (pdfSelection === "image") {
                return {
                    command: "open",
                    descriptor: {
                        "null": sourceRef,
                        "as": {
                            "obj": "PDFGenericFormat",
                            "value": {
                                "pageNumber": pageNumber,
                                "selection": {
                                    "enum": "pdfSelection",
                                    "value": pdfSelection
                                },
                                "suppressWarnings": suppressWarnings
                            }
                        }
                    }
                };
            }
        }
    };

    openDocument.cropTo = {
        bounding: "boundingBox",
        media: "mediaBox",
        crop: "cropBox",
        bleed: "bleedBox",
        trim: "trimBox",
        art: "artBox"
    };

    openDocument.mode = {
        rgb: "RGBColor",
        gray: "grayscaleMode",
        cmyk: "CMYKColorEnum",
        lab: "labColor"
    };

    /**
     * Close a document without saving
     *
     * @param {string} save Whether the document should be saved. "yes", "no"
     *
     * @return {PlayObject}
     *
     * Preconditions:
     * The document should be saved previously and have fileReference path value for saving.
     */
    var closeDocument = function (save) {
        return {
            command: "close",
            "saving": {
                "enum": "yesNo",
                "value": save
            }
        };
    };

    /**
     * Save a document
     *
     *
     */
    var saveDocument = function () {
        //console.log("not implemented.");
    };

    /**
     * Select a document
     *
     * @param {ActionDescriptor} sourceRef document reference
     *
     * @return {PlayObject}
     *
     */
    var selectDocument = function (sourceRef) {
        return {
            command: "select",
            descriptor: {
                "null": sourceRef
            }
        };
    };

    exports.referenceBy = referenceBy;

    exports.open = openDocument;
    exports.close = closeDocument;
    exports.save = saveDocument;
    exports.select = selectDocument;

});
<|endoftext|>


<commit_before>var ElementType = require("domelementtype");
var isTag = exports.isTag = ElementType.isTag;

exports.testElement = function(options, element){
	for(var key in options){
		if(!options.hasOwnProperty(key));
		else if(key === "tag_name"){
			if(!isTag(element) || !options.tag_name(element.name)){
				return false;
			}
		} else if(key === "tag_type"){
			if(!options.tag_type(element.type)) return false;
		} else if(key === "tag_contains"){
			if(isTag(element) || !options.tag_contains(element.data)){
				return false;
			}
		} else if(!element.attribs || !options[key](element.attribs[key])){
			return false;
		}
	}
	return true;
};

var Checks = {
	tag_name: function(name){
		if(typeof name === "function"){
			return function(elem){ return isTag(elem) && name(elem.name); };
		} else if(name === "*"){
			return isTag;
		} else {
			return function(elem) {
				return isTag(elem) && elem.name === name && !elem.ns;
			};
		}
	},
	tag_name_ns: function(href, name){
		return function(elem) {
			if(!elem.doc || !isTag(elem) || elem.name !== name)
				return false;
			if(href == null && elem.ns == null) return true;
			var nsList = elem.doc.namespaces.hrefMap[href];
			if(nsList == null && elem.ns != null) return false;
			var isNSEqual = false;
			for(var i = 0; i < nsList.length; ++i)
				if(nsList[i] === elem.ns) isNSEqual = true;
			return isNSEqual;
		};
	},
	tag_type: function(type){
		if(typeof type === "function"){
			return function(elem){ return type(elem.type); };
		} else {
			return function(elem){ return elem.type === type; };
		}
	},
	tag_contains: function(data){
		if(typeof data === "function"){
			return function(elem){ return !isTag(elem) && data(elem.data); };
		} else {
			return function(elem){ return !isTag(elem) && elem.data === data; };
		}
	}
};

function getAttribCheck(attrib, value){
	if(typeof value === "function"){
		return function(elem){ return elem.attribs && value(elem.attribs[attrib]); };
	} else {
		return function(elem){ return elem.attribs && elem.attribs[attrib] === value; };
	}
}

function combineFuncs(a, b){
	return function(elem){
		return a(elem) || b(elem);
	};
}

exports.getElements = function(options, element, recurse, limit){
	var funcs = Object.keys(options).map(function(key){
		var value = options[key];
		return key in Checks ? Checks[key](value) : getAttribCheck(key, value);
	});

	return funcs.length === 0 ? [] : this.filter(
		funcs.reduce(combineFuncs),
		element, recurse, limit
	);
};

exports.getElementById = function(id, element, recurse){
	if(!Array.isArray(element)) element = [element];
	return this.findOne(getAttribCheck("id", id), element, recurse !== false);
};

exports.getElementsByTagName = function(name, element, recurse, limit){
	return this.filter(Checks.tag_name(name), element, recurse, limit);
};

exports.getElementsByTagNameNS = function(nsHref, name, element, recurse, limit){
	return this.filter(Checks.tag_name_ns(nsHref, name), element, recurse, limit);
};

exports.getElementsByTagType = function(type, element, recurse, limit){
	return this.filter(Checks.tag_type(type), element, recurse, limit);
};
<commit_msg>add element is null judgement<commit_after>var ElementType = require("domelementtype");
var isTag = exports.isTag = ElementType.isTag;

exports.testElement = function(options, element){
	for(var key in options){
		if(!options.hasOwnProperty(key));
		else if(key === "tag_name"){
			if(!isTag(element) || !options.tag_name(element.name)){
				return false;
			}
		} else if(key === "tag_type"){
			if(!options.tag_type(element.type)) return false;
		} else if(key === "tag_contains"){
			if(isTag(element) || !options.tag_contains(element.data)){
				return false;
			}
		} else if(!element.attribs || !options[key](element.attribs[key])){
			return false;
		}
	}
	return true;
};

var Checks = {
	tag_name: function(name){
		if(typeof name === "function"){
			return function(elem){ return isTag(elem) && name(elem.name); };
		} else if(name === "*"){
			return isTag;
		} else {
			return function(elem) {
				return isTag(elem) && elem.name === name && !elem.ns;
			};
		}
	},
	tag_name_ns: function(href, name){
		return function(elem) {
			if(!elem || !elem.doc || !isTag(elem) || elem.name !== name)
				return false;
			if(href == null && elem.ns == null) return true;
			var nsList = elem.doc.namespaces.hrefMap[href];
			if(nsList == null && elem.ns != null) return false;
			var isNSEqual = false;
			for(var i = 0; i < nsList.length; ++i)
				if(nsList[i] === elem.ns) isNSEqual = true;
			return isNSEqual;
		};
	},
	tag_type: function(type){
		if(typeof type === "function"){
			return function(elem){ return type(elem.type); };
		} else {
			return function(elem){ return elem.type === type; };
		}
	},
	tag_contains: function(data){
		if(typeof data === "function"){
			return function(elem){ return !isTag(elem) && data(elem.data); };
		} else {
			return function(elem){ return !isTag(elem) && elem.data === data; };
		}
	}
};

function getAttribCheck(attrib, value){
	if(typeof value === "function"){
		return function(elem){ return elem.attribs && value(elem.attribs[attrib]); };
	} else {
		return function(elem){ return elem.attribs && elem.attribs[attrib] === value; };
	}
}

function combineFuncs(a, b){
	return function(elem){
		return a(elem) || b(elem);
	};
}

exports.getElements = function(options, element, recurse, limit){
	var funcs = Object.keys(options).map(function(key){
		var value = options[key];
		return key in Checks ? Checks[key](value) : getAttribCheck(key, value);
	});

	return funcs.length === 0 ? [] : this.filter(
		funcs.reduce(combineFuncs),
		element, recurse, limit
	);
};

exports.getElementById = function(id, element, recurse){
	if(!Array.isArray(element)) element = [element];
	return this.findOne(getAttribCheck("id", id), element, recurse !== false);
};

exports.getElementsByTagName = function(name, element, recurse, limit){
	return this.filter(Checks.tag_name(name), element, recurse, limit);
};

exports.getElementsByTagNameNS = function(nsHref, name, element, recurse, limit){
	return this.filter(Checks.tag_name_ns(nsHref, name), element, recurse, limit);
};

exports.getElementsByTagType = function(type, element, recurse, limit){
	return this.filter(Checks.tag_type(type), element, recurse, limit);
};
<|endoftext|>


<commit_before>/**
 * Copyright 2017 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {dev} from '../../../src/log';
import {Observable} from '../../../src/observable';

/**
 * This class implements visibility calculations based on the
 * visibility ratio. It's used for documents, embeds and individual element.
 * @implements {../../../src/service.Disposable}
 */
export class VisibilityModel {
  /**
   * @param {!Object<string, *>} spec
   * @param {function():number} calcVisibility
   */
  constructor(spec, calcVisibility) {
    /** @const @private */
    this.calcVisibility_ = calcVisibility;

    /**
     * Spec parameters.
     * @private {{
     *   visiblePercentageMin: number,
     *   visiblePercentageMax: number,
     *   totalTimeMin: number,
     *   totalTimeMax: number,
     *   continuousTimeMin: number,
     *   continuousTimeMax: number,
     * }}
     */
    this.spec_ = {
      visiblePercentageMin: Number(spec['visiblePercentageMin']) / 100 || 0,
      visiblePercentageMax: Number(spec['visiblePercentageMax']) / 100 || 1,
      totalTimeMin: Number(spec['totalTimeMin']) || 0,
      totalTimeMax: Number(spec['totalTimeMax']) || Infinity,
      continuousTimeMin: Number(spec['continuousTimeMin']) || 0,
      continuousTimeMax: Number(spec['continuousTimeMax']) || Infinity,
    };
    // Above, if visiblePercentageMax was not specified, assume 100%.
    // Here, do allow 0% to be the value if that is what was specified.
    if (String(spec['visiblePercentageMax']).trim() === '0') {
      this.spec_.visiblePercentageMax = 0;
    }

    /** @private {boolean} */
    this.repeat_ = spec['repeat'] === true;

    /** @private {?function()} */
    this.eventResolver_ = null;

    /** @private {?Observable} */
    this.onTriggerObservable_ = new Observable();

    /** @private */
    this.eventPromise_ = new Promise(resolve => {
      this.eventResolver_ = resolve;
    });

    this.eventPromise_.then(() => {
      this.onTriggerObservable_.fire();
    });

    /** @private {!Array<!UnlistenDef>} */
    this.unsubscribe_ = [];

    /** @const @private {time} */
    this.createdTime_ = Date.now();

    /** @private {boolean} */
    this.ready_ = true;

    /** @private {boolean} */
    this.reportReady_ = true;

    /** @private {?function():!Promise} */
    this.createReportReadyPromise_ = null;

    /** @private {?number} */
    this.scheduledUpdateTimeoutId_ = null;

    /** @private {boolean} */
    this.matchesVisibility_ = false;

    /** @private {boolean} */
    this.everMatchedVisibility_ = false;

    /** @private {time} duration in milliseconds */
    this.continuousTime_ = 0;

    /** @private {time} duration in milliseconds */
    this.maxContinuousVisibleTime_ = 0;

    /** @private {time} duration in milliseconds */
    this.totalVisibleTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.firstSeenTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.lastSeenTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.firstVisibleTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.lastVisibleTime_ = 0;

    /** @private {time} percent value in a [0, 1] range */
    this.loadTimeVisibility_ = 0;

    /** @private {number} percent value in a [0, 1] range */
    this.minVisiblePercentage_ = 0;

    /** @private {number} percent value in a [0, 1] range */
    this.maxVisiblePercentage_ = 0;

    /** @private {time} milliseconds since epoch */
    this.lastVisibleUpdateTime_ = 0;

    /** @private {boolean} */
    this.waitToReset_ = false;

    /** @private {?number} */
    this.scheduleRepeatId_ = null;
  }

  /**
   * Refresh counter on visible reset.
   * TODO: Right now all state value are scoped state values that gets reset.
   * We may need to add support to global state values,
   * that never reset like globalTotalVisibleTime.
   * Note: loadTimeVisibility is an exception.
   * @private
   */
  reset_() {
    dev().assert(!this.eventResolver_,
        'Attempt to refresh visible event before previous one resolve');
    this.eventPromise_ = new Promise(resolve => {
      this.eventResolver_ = resolve;
    });
    this.eventPromise_.then(() => {
      this.onTriggerObservable_.fire();
    });
    this.scheduleRepeatId_ = null;
    this.everMatchedVisibility_ = false;
    this.matchesVisibility_ = false;
    this.continuousTime_ = 0;
    this.maxContinuousVisibleTime_ = 0;
    this.totalVisibleTime_ = 0;
    this.firstVisibleTime_ = 0;
    this.firstSeenTime_ = 0;
    this.lastSeenTime_ = 0;
    this.lastVisibleTime_ = 0;
    this.minVisiblePercentage_ = 0;
    this.maxVisiblePercentage_ = 0;
    this.lastVisibleUpdateTime_ = 0;
    this.waitToReset_ = false;
  }

  /**
   * Function that visibilityManager can used to dispose model or reset model
   */
  maybeDispose() {
    if (!this.repeat_) {
      this.dispose();
    }
  }

  /** @override */
  dispose() {
    if (this.scheduledUpdateTimeoutId_) {
      clearTimeout(this.scheduledUpdateTimeoutId_);
      this.scheduledUpdateTimeoutId_ = null;
    }
    if (this.scheduleRepeatId_) {
      clearTimeout(this.scheduleRepeatId_);
      this.scheduleRepeatId_ = null;
    }
    this.unsubscribe_.forEach(unsubscribe => {
      unsubscribe();
    });
    this.unsubscribe_.length = 0;
    this.eventResolver_ = null;
    // TODO(jonkeller): Investigate why dispose() can be called twice,
    // necessitating this "if"
    if (this.onTriggerObservable_) {
      this.onTriggerObservable_.removeAll();
      this.onTriggerObservable_ = null;
    }
  }

  /**
   * Adds the unsubscribe handler that will be called when this visibility
   * model is destroyed.
   * @param {!UnlistenDef} handler
   */
  unsubscribe(handler) {
    this.unsubscribe_.push(handler);
  }

  /**
   * Adds the event handler that will be called when all visibility conditions
   * have been met.
   * @param {function()} handler
   */
  onTriggerEvent(handler) {
    this.onTriggerObservable_.add(handler);
    if (this.eventPromise_ && !this.eventResolver_) {
      // If eventPromise has already resolved, need to call handler manually.
      handler();
    }
  }

  /**
   * Sets whether this object is ready. Ready means that visibility is
   * ready to be calculated, e.g. because an element has been
   * sufficiently rendered.
   * @param {boolean} ready
   */
  setReady(ready) {
    this.ready_ = ready;
    this.update();
  }

  /**
   * Sets that the model needs to wait on extra report ready promise
   * after all visibility conditions have been met to call report handler
   * @param {!function():!Promise} callback
   */
  setReportReady(callback) {
    this.reportReady_ = false;
    this.createReportReadyPromise_ = callback;
  }

  /**
   * @return {number}
   * @private
   */
  getVisibility_() {
    return this.ready_ ? this.calcVisibility_() : 0;
  }

  /**
   * Runs the calculation cycle.
   */
  update() {
    this.update_(this.getVisibility_());
  }

  /**
   * Returns the calculated state of visibility.
   * @param {time} startTime
   * @return {!Object<string, string|number>}
   */
  getState(startTime) {
    return {
      // Observed times, relative to the `startTime`.
      firstSeenTime: timeBase(this.firstSeenTime_, startTime),
      lastSeenTime: timeBase(this.lastSeenTime_, startTime),
      lastVisibleTime: timeBase(this.lastVisibleTime_, startTime),
      firstVisibleTime: timeBase(this.firstVisibleTime_, startTime),

      // Durations.
      maxContinuousVisibleTime: this.maxContinuousVisibleTime_,
      totalVisibleTime: this.totalVisibleTime_,

      // Visibility percents.
      loadTimeVisibility: this.loadTimeVisibility_ * 100 || 0,
      minVisiblePercentage: this.minVisiblePercentage_ * 100,
      maxVisiblePercentage: this.maxVisiblePercentage_ * 100,
    };
  }

  /**
   * @param {number} visibility
   * @private
   */
  update_(visibility) {
    // Update state and check if all conditions are satisfied
    if (this.waitToReset_) {
      if (!this.isVisibilityMatch_(visibility)) {
        // We were waiting for a condition to become unmet, and now it has
        this.reset_();
      }
      return;
    }
    if (!this.eventResolver_) {
      return;
    }
    const conditionsMet = this.updateCounters_(visibility);
    if (conditionsMet) {
      if (this.scheduledUpdateTimeoutId_) {
        clearTimeout(this.scheduledUpdateTimeoutId_);
        this.scheduledUpdateTimeoutId_ = null;
      }
      if (this.reportReady_) {
        // TODO(jonkeller): Can we eliminate eventResolver_?
        this.eventResolver_();
        this.eventResolver_ = null;
        if (this.repeat_) {
          this.waitToReset_ = true;
          this.continuousTime_ = 0;
        }
      } else if (this.createReportReadyPromise_) {
        // Report when report ready promise resolve
        const reportReadyPromise = this.createReportReadyPromise_();
        this.createReportReadyPromise_ = null;
        reportReadyPromise.then(() => {
          this.reportReady_ = true;
          // Need to update one more time in case time exceeds
          // maxContinuousVisibleTime.
          this.update();
        });
      }
    } else if (this.matchesVisibility_ && !this.scheduledUpdateTimeoutId_) {
      // There is unmet duration condition, schedule a check
      const timeToWait = this.computeTimeToWait_();
      if (timeToWait > 0) {
        this.scheduledUpdateTimeoutId_ = setTimeout(() => {
          this.update();
          if (!this.eventResolver_) {
            this.reset_();
            this.setReady(true);
          }
          this.scheduledUpdateTimeoutId_ = null;
        }, timeToWait);
      }
    } else if (!this.matchesVisibility_ && this.scheduledUpdateTimeoutId_) {
      clearTimeout(this.scheduledUpdateTimeoutId_);
      this.scheduledUpdateTimeoutId_ = null;
    }
  }

  /**
   * Check if visibility fall into the percentage range
   * @param {number} visibility
   * @return {boolean}
   */
  isVisibilityMatch_(visibility) {
    dev().assert(visibility >= 0 && visibility <= 1,
        'invalid visibility value: %s', visibility);
    // Special case: If visiblePercentageMin is 100%, then it doesn't make
    // sense to do the usual (min, max] since that would never be true.
    if (this.spec_.visiblePercentageMin == 1) {
      return visibility == 1;
    }
    // Special case: If visiblePercentageMax is 0%, then we
    // want to ping when the creative becomes not visible.
    if (this.spec_.visiblePercentageMax == 0) {
      return visibility == 0;
    }
    return visibility > this.spec_.visiblePercentageMin &&
        visibility <= this.spec_.visiblePercentageMax;
  }

  /**
   * @param {number} visibility
   * @return {boolean} true
   * @private
   */
  updateCounters_(visibility) {
    dev().assert(visibility >= 0 && visibility <= 1,
        'invalid visibility value: %s', visibility);
    const now = Date.now();

    if (visibility > 0) {
      this.firstSeenTime_ = this.firstSeenTime_ || now;
      this.lastSeenTime_ = now;
      // Consider it as load time visibility if this happens within 300ms of
      // page load.
      if (!this.loadTimeVisibility_ && (now - this.createdTime_) < 300) {
        this.loadTimeVisibility_ = visibility;
      }
    }

    const prevMatchesVisibility = this.matchesVisibility_;
    const timeSinceLastUpdate =
        this.lastVisibleUpdateTime_ ? now - this.lastVisibleUpdateTime_ : 0;
    this.matchesVisibility_ = this.isVisibilityMatch_(visibility);
    if (this.matchesVisibility_) {
      this.everMatchedVisibility_ = true;
      if (prevMatchesVisibility) {
        // Keep counting.
        this.totalVisibleTime_ += timeSinceLastUpdate;
        this.continuousTime_ += timeSinceLastUpdate;
        this.maxContinuousVisibleTime_ =
            Math.max(this.maxContinuousVisibleTime_, this.continuousTime_);
      } else {
        // The resource came into view: start counting.
        dev().assert(!this.lastVisibleUpdateTime_);
        this.firstVisibleTime_ = this.firstVisibleTime_ || now;
      }
      this.lastVisibleUpdateTime_ = now;
      this.minVisiblePercentage_ =
          this.minVisiblePercentage_ > 0 ?
            Math.min(this.minVisiblePercentage_, visibility) :
            visibility;
      this.maxVisiblePercentage_ =
          Math.max(this.maxVisiblePercentage_, visibility);
      this.lastVisibleTime_ = now;
    } else if (prevMatchesVisibility) {
      // The resource went out of view. Do final calculations and reset state.
      dev().assert(this.lastVisibleUpdateTime_ > 0);

      this.maxContinuousVisibleTime_ = Math.max(
          this.maxContinuousVisibleTime_,
          this.continuousTime_ + timeSinceLastUpdate);

      // Reset for next visibility event.
      this.lastVisibleUpdateTime_ = 0;
      this.totalVisibleTime_ += timeSinceLastUpdate;
      this.continuousTime_ = 0; // Clear only after max is calculated above.
      this.lastVisibleTime_ = now;
    }

    return this.everMatchedVisibility_ &&
        (this.totalVisibleTime_ >= this.spec_.totalTimeMin) &&
        (this.totalVisibleTime_ <= this.spec_.totalTimeMax) &&
        (this.maxContinuousVisibleTime_ >= this.spec_.continuousTimeMin) &&
        (this.maxContinuousVisibleTime_ <= this.spec_.continuousTimeMax);
  }

  /**
   * Computes time, assuming the object is currently visible, that it'd take
   * it to match all timing requirements.
   * @return {time}
   * @private
   */
  computeTimeToWait_() {
    const waitForContinuousTime = Math.max(
        this.spec_.continuousTimeMin - this.continuousTime_, 0);
    const waitForTotalTime = Math.max(
        this.spec_.totalTimeMin - this.totalVisibleTime_, 0);
    const maxWaitTime = Math.max(waitForContinuousTime, waitForTotalTime);
    return Math.min(
        maxWaitTime,
        waitForContinuousTime || Infinity,
        waitForTotalTime || Infinity);
  }
}

/**
 * Calculates the specified time based on the given `baseTime`.
 * @param {time} time
 * @param {time} baseTime
 * @return {time}
 */
function timeBase(time, baseTime) {
  return time >= baseTime ? time - baseTime : 0;
}
<commit_msg>Guards all uses of onTriggerObservable until we can figure out more detail (#12547)<commit_after>/**
 * Copyright 2017 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {dev} from '../../../src/log';
import {Observable} from '../../../src/observable';

/** @private @const {string} */
const TAG_ = 'amp-analytics.VisibilityModel';

/**
 * This class implements visibility calculations based on the
 * visibility ratio. It's used for documents, embeds and individual element.
 * @implements {../../../src/service.Disposable}
 */
export class VisibilityModel {
  /**
   * @param {!Object<string, *>} spec
   * @param {function():number} calcVisibility
   */
  constructor(spec, calcVisibility) {
    /** @const @private */
    this.calcVisibility_ = calcVisibility;

    /**
     * Spec parameters.
     * @private {{
     *   visiblePercentageMin: number,
     *   visiblePercentageMax: number,
     *   totalTimeMin: number,
     *   totalTimeMax: number,
     *   continuousTimeMin: number,
     *   continuousTimeMax: number,
     * }}
     */
    this.spec_ = {
      visiblePercentageMin: Number(spec['visiblePercentageMin']) / 100 || 0,
      visiblePercentageMax: Number(spec['visiblePercentageMax']) / 100 || 1,
      totalTimeMin: Number(spec['totalTimeMin']) || 0,
      totalTimeMax: Number(spec['totalTimeMax']) || Infinity,
      continuousTimeMin: Number(spec['continuousTimeMin']) || 0,
      continuousTimeMax: Number(spec['continuousTimeMax']) || Infinity,
    };
    // Above, if visiblePercentageMax was not specified, assume 100%.
    // Here, do allow 0% to be the value if that is what was specified.
    if (String(spec['visiblePercentageMax']).trim() === '0') {
      this.spec_.visiblePercentageMax = 0;
    }

    /** @private {boolean} */
    this.repeat_ = spec['repeat'] === true;

    /** @private {?function()} */
    this.eventResolver_ = null;

    /** @private {?Observable} */
    this.onTriggerObservable_ = new Observable();

    /** @private */
    this.eventPromise_ = new Promise(resolve => {
      this.eventResolver_ = resolve;
    });

    this.eventPromise_.then(() => {
      if (!this.onTriggerObservable_) {
        dev().warn(TAG_, 'onTriggerObservable_ is unexpectedly null.');
        return;
      }
      this.onTriggerObservable_.fire();
    });

    /** @private {!Array<!UnlistenDef>} */
    this.unsubscribe_ = [];

    /** @const @private {time} */
    this.createdTime_ = Date.now();

    /** @private {boolean} */
    this.ready_ = true;

    /** @private {boolean} */
    this.reportReady_ = true;

    /** @private {?function():!Promise} */
    this.createReportReadyPromise_ = null;

    /** @private {?number} */
    this.scheduledUpdateTimeoutId_ = null;

    /** @private {boolean} */
    this.matchesVisibility_ = false;

    /** @private {boolean} */
    this.everMatchedVisibility_ = false;

    /** @private {time} duration in milliseconds */
    this.continuousTime_ = 0;

    /** @private {time} duration in milliseconds */
    this.maxContinuousVisibleTime_ = 0;

    /** @private {time} duration in milliseconds */
    this.totalVisibleTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.firstSeenTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.lastSeenTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.firstVisibleTime_ = 0;

    /** @private {time} milliseconds since epoch */
    this.lastVisibleTime_ = 0;

    /** @private {time} percent value in a [0, 1] range */
    this.loadTimeVisibility_ = 0;

    /** @private {number} percent value in a [0, 1] range */
    this.minVisiblePercentage_ = 0;

    /** @private {number} percent value in a [0, 1] range */
    this.maxVisiblePercentage_ = 0;

    /** @private {time} milliseconds since epoch */
    this.lastVisibleUpdateTime_ = 0;

    /** @private {boolean} */
    this.waitToReset_ = false;

    /** @private {?number} */
    this.scheduleRepeatId_ = null;
  }

  /**
   * Refresh counter on visible reset.
   * TODO: Right now all state value are scoped state values that gets reset.
   * We may need to add support to global state values,
   * that never reset like globalTotalVisibleTime.
   * Note: loadTimeVisibility is an exception.
   * @private
   */
  reset_() {
    dev().assert(!this.eventResolver_,
        'Attempt to refresh visible event before previous one resolve');
    this.eventPromise_ = new Promise(resolve => {
      this.eventResolver_ = resolve;
    });
    this.eventPromise_.then(() => {
      if (!this.onTriggerObservable_) {
        dev().warn(TAG_, 'onTriggerObservable_ is unexpectedly null.');
        return;
      }
      this.onTriggerObservable_.fire();
    });
    this.scheduleRepeatId_ = null;
    this.everMatchedVisibility_ = false;
    this.matchesVisibility_ = false;
    this.continuousTime_ = 0;
    this.maxContinuousVisibleTime_ = 0;
    this.totalVisibleTime_ = 0;
    this.firstVisibleTime_ = 0;
    this.firstSeenTime_ = 0;
    this.lastSeenTime_ = 0;
    this.lastVisibleTime_ = 0;
    this.minVisiblePercentage_ = 0;
    this.maxVisiblePercentage_ = 0;
    this.lastVisibleUpdateTime_ = 0;
    this.waitToReset_ = false;
  }

  /**
   * Function that visibilityManager can used to dispose model or reset model
   */
  maybeDispose() {
    if (!this.repeat_) {
      this.dispose();
    }
  }

  /** @override */
  dispose() {
    if (this.scheduledUpdateTimeoutId_) {
      clearTimeout(this.scheduledUpdateTimeoutId_);
      this.scheduledUpdateTimeoutId_ = null;
    }
    if (this.scheduleRepeatId_) {
      clearTimeout(this.scheduleRepeatId_);
      this.scheduleRepeatId_ = null;
    }
    this.unsubscribe_.forEach(unsubscribe => {
      unsubscribe();
    });
    this.unsubscribe_.length = 0;
    this.eventResolver_ = null;
    // TODO(jonkeller): Investigate why dispose() can be called twice,
    // necessitating this "if", and the same "if" elsewhere in this file.
    if (!this.onTriggerObservable_) {
      dev().warn(TAG_,
          'dispose() called when onTriggerObservable_ already null.');
      return;
    }
    this.onTriggerObservable_.removeAll();
    this.onTriggerObservable_ = null;
  }

  /**
   * Adds the unsubscribe handler that will be called when this visibility
   * model is destroyed.
   * @param {!UnlistenDef} handler
   */
  unsubscribe(handler) {
    this.unsubscribe_.push(handler);
  }

  /**
   * Adds the event handler that will be called when all visibility conditions
   * have been met.
   * @param {function()} handler
   */
  onTriggerEvent(handler) {
    if (this.onTriggerObservable_) {
      this.onTriggerObservable_.add(handler);
    } else {
      dev().warn(TAG_, 'onTriggerObservable_ is unexpectedly null.');
    }
    if (this.eventPromise_ && !this.eventResolver_) {
      // If eventPromise has already resolved, need to call handler manually.
      handler();
    }
  }

  /**
   * Sets whether this object is ready. Ready means that visibility is
   * ready to be calculated, e.g. because an element has been
   * sufficiently rendered.
   * @param {boolean} ready
   */
  setReady(ready) {
    this.ready_ = ready;
    this.update();
  }

  /**
   * Sets that the model needs to wait on extra report ready promise
   * after all visibility conditions have been met to call report handler
   * @param {!function():!Promise} callback
   */
  setReportReady(callback) {
    this.reportReady_ = false;
    this.createReportReadyPromise_ = callback;
  }

  /**
   * @return {number}
   * @private
   */
  getVisibility_() {
    return this.ready_ ? this.calcVisibility_() : 0;
  }

  /**
   * Runs the calculation cycle.
   */
  update() {
    this.update_(this.getVisibility_());
  }

  /**
   * Returns the calculated state of visibility.
   * @param {time} startTime
   * @return {!Object<string, string|number>}
   */
  getState(startTime) {
    return {
      // Observed times, relative to the `startTime`.
      firstSeenTime: timeBase(this.firstSeenTime_, startTime),
      lastSeenTime: timeBase(this.lastSeenTime_, startTime),
      lastVisibleTime: timeBase(this.lastVisibleTime_, startTime),
      firstVisibleTime: timeBase(this.firstVisibleTime_, startTime),

      // Durations.
      maxContinuousVisibleTime: this.maxContinuousVisibleTime_,
      totalVisibleTime: this.totalVisibleTime_,

      // Visibility percents.
      loadTimeVisibility: this.loadTimeVisibility_ * 100 || 0,
      minVisiblePercentage: this.minVisiblePercentage_ * 100,
      maxVisiblePercentage: this.maxVisiblePercentage_ * 100,
    };
  }

  /**
   * @param {number} visibility
   * @private
   */
  update_(visibility) {
    // Update state and check if all conditions are satisfied
    if (this.waitToReset_) {
      if (!this.isVisibilityMatch_(visibility)) {
        // We were waiting for a condition to become unmet, and now it has
        this.reset_();
      }
      return;
    }
    if (!this.eventResolver_) {
      return;
    }
    const conditionsMet = this.updateCounters_(visibility);
    if (conditionsMet) {
      if (this.scheduledUpdateTimeoutId_) {
        clearTimeout(this.scheduledUpdateTimeoutId_);
        this.scheduledUpdateTimeoutId_ = null;
      }
      if (this.reportReady_) {
        // TODO(jonkeller): Can we eliminate eventResolver_?
        this.eventResolver_();
        this.eventResolver_ = null;
        if (this.repeat_) {
          this.waitToReset_ = true;
          this.continuousTime_ = 0;
        }
      } else if (this.createReportReadyPromise_) {
        // Report when report ready promise resolve
        const reportReadyPromise = this.createReportReadyPromise_();
        this.createReportReadyPromise_ = null;
        reportReadyPromise.then(() => {
          this.reportReady_ = true;
          // Need to update one more time in case time exceeds
          // maxContinuousVisibleTime.
          this.update();
        });
      }
    } else if (this.matchesVisibility_ && !this.scheduledUpdateTimeoutId_) {
      // There is unmet duration condition, schedule a check
      const timeToWait = this.computeTimeToWait_();
      if (timeToWait > 0) {
        this.scheduledUpdateTimeoutId_ = setTimeout(() => {
          this.update();
          if (!this.eventResolver_) {
            this.reset_();
            this.setReady(true);
          }
          this.scheduledUpdateTimeoutId_ = null;
        }, timeToWait);
      }
    } else if (!this.matchesVisibility_ && this.scheduledUpdateTimeoutId_) {
      clearTimeout(this.scheduledUpdateTimeoutId_);
      this.scheduledUpdateTimeoutId_ = null;
    }
  }

  /**
   * Check if visibility fall into the percentage range
   * @param {number} visibility
   * @return {boolean}
   */
  isVisibilityMatch_(visibility) {
    dev().assert(visibility >= 0 && visibility <= 1,
        'invalid visibility value: %s', visibility);
    // Special case: If visiblePercentageMin is 100%, then it doesn't make
    // sense to do the usual (min, max] since that would never be true.
    if (this.spec_.visiblePercentageMin == 1) {
      return visibility == 1;
    }
    // Special case: If visiblePercentageMax is 0%, then we
    // want to ping when the creative becomes not visible.
    if (this.spec_.visiblePercentageMax == 0) {
      return visibility == 0;
    }
    return visibility > this.spec_.visiblePercentageMin &&
        visibility <= this.spec_.visiblePercentageMax;
  }

  /**
   * @param {number} visibility
   * @return {boolean} true
   * @private
   */
  updateCounters_(visibility) {
    dev().assert(visibility >= 0 && visibility <= 1,
        'invalid visibility value: %s', visibility);
    const now = Date.now();

    if (visibility > 0) {
      this.firstSeenTime_ = this.firstSeenTime_ || now;
      this.lastSeenTime_ = now;
      // Consider it as load time visibility if this happens within 300ms of
      // page load.
      if (!this.loadTimeVisibility_ && (now - this.createdTime_) < 300) {
        this.loadTimeVisibility_ = visibility;
      }
    }

    const prevMatchesVisibility = this.matchesVisibility_;
    const timeSinceLastUpdate =
        this.lastVisibleUpdateTime_ ? now - this.lastVisibleUpdateTime_ : 0;
    this.matchesVisibility_ = this.isVisibilityMatch_(visibility);
    if (this.matchesVisibility_) {
      this.everMatchedVisibility_ = true;
      if (prevMatchesVisibility) {
        // Keep counting.
        this.totalVisibleTime_ += timeSinceLastUpdate;
        this.continuousTime_ += timeSinceLastUpdate;
        this.maxContinuousVisibleTime_ =
            Math.max(this.maxContinuousVisibleTime_, this.continuousTime_);
      } else {
        // The resource came into view: start counting.
        dev().assert(!this.lastVisibleUpdateTime_);
        this.firstVisibleTime_ = this.firstVisibleTime_ || now;
      }
      this.lastVisibleUpdateTime_ = now;
      this.minVisiblePercentage_ =
          this.minVisiblePercentage_ > 0 ?
            Math.min(this.minVisiblePercentage_, visibility) :
            visibility;
      this.maxVisiblePercentage_ =
          Math.max(this.maxVisiblePercentage_, visibility);
      this.lastVisibleTime_ = now;
    } else if (prevMatchesVisibility) {
      // The resource went out of view. Do final calculations and reset state.
      dev().assert(this.lastVisibleUpdateTime_ > 0);

      this.maxContinuousVisibleTime_ = Math.max(
          this.maxContinuousVisibleTime_,
          this.continuousTime_ + timeSinceLastUpdate);

      // Reset for next visibility event.
      this.lastVisibleUpdateTime_ = 0;
      this.totalVisibleTime_ += timeSinceLastUpdate;
      this.continuousTime_ = 0; // Clear only after max is calculated above.
      this.lastVisibleTime_ = now;
    }

    return this.everMatchedVisibility_ &&
        (this.totalVisibleTime_ >= this.spec_.totalTimeMin) &&
        (this.totalVisibleTime_ <= this.spec_.totalTimeMax) &&
        (this.maxContinuousVisibleTime_ >= this.spec_.continuousTimeMin) &&
        (this.maxContinuousVisibleTime_ <= this.spec_.continuousTimeMax);
  }

  /**
   * Computes time, assuming the object is currently visible, that it'd take
   * it to match all timing requirements.
   * @return {time}
   * @private
   */
  computeTimeToWait_() {
    const waitForContinuousTime = Math.max(
        this.spec_.continuousTimeMin - this.continuousTime_, 0);
    const waitForTotalTime = Math.max(
        this.spec_.totalTimeMin - this.totalVisibleTime_, 0);
    const maxWaitTime = Math.max(waitForContinuousTime, waitForTotalTime);
    return Math.min(
        maxWaitTime,
        waitForContinuousTime || Infinity,
        waitForTotalTime || Infinity);
  }
}

/**
 * Calculates the specified time based on the given `baseTime`.
 * @param {time} time
 * @param {time} baseTime
 * @return {time}
 */
function timeBase(time, baseTime) {
  return time >= baseTime ? time - baseTime : 0;
}
<|endoftext|>