<commit_before>import {Map} from 'immutable';
export default (app) => {
    class RasterFoundryRepository {
        constructor(
            $q, $filter,
            authService, datasourceService, sceneService, thumbnailService,
            projectService
        ) {
            this.$q = $q;
            this.$filter = $filter;
            this.authService = authService;
            this.datasourceService = datasourceService;
            this.sceneService = sceneService;
            this.thumbnailService = thumbnailService;
            this.projectService = projectService;
            this.datasourceCache = new Map();
            this.previewOnMap = true;
        }

        initRepository() {
            return this.$q((resolve) => {
                resolve();
            });
        }

        getFilters() {
            return [{
                param: 'datasource',
                label: 'Imagery Sources',
                type: 'search-select',
                getSources: this.getSources.bind(this)
            }, {
                params: {
                    min: 'minAcquisitionDatetime',
                    max: 'maxAcquisitionDatetime'
                },
                label: 'Date Range',
                type: 'daterange',
                default: 'None'
            // TODO enable this filter once the api supports it
            // }, {
            //     type: 'shape',
            //     label: 'Area of Interest',
            //     param: 'shape'
            }, {
                params: {
                    min: 'minCloudCover',
                    max: 'maxCloudCover'
                },
                label: 'Cloud Cover',
                type: 'slider',
                min: 0,
                max: 100,
                ticks: 10,
                step: 10,
                scale: 1
            }, {
                params: {
                    min: 'minSunElevation',
                    max: 'maxSunElevation'
                },
                label: 'Sun Elevation',
                type: 'slider',
                min: 0,
                max: 180,
                ticks: 30,
                step: 10,
                scale: 1
            }, {
                params: {
                    min: 'minSunAzimuth',
                    max: 'maxSunAzimuth'
                },
                label: 'Sun Azimuth',
                type: 'slider',
                min: 0,
                max: 360,
                ticks: 60,
                step: 10,
                scale: 1
            }, {
                param: 'ingested',
                label: 'Ingest Status',
                type: 'tag',
                options: [{
                    label: 'Show all',
                    value: null
                }, {
                    label: 'Uningested Only',
                    value: 'false'
                }, {
                    label: 'Ingested Only',
                    value: 'true'
                }]
            }, {
                param: 'owner',
                label: 'Owner',
                type: 'tag',
                options: [{
                    label: 'All',
                    value: null
                }, {
                    label: 'My Scenes',
                    value: this.authService.getProfile().sub
                }]
            }];
        }

        getSources() {
            return this.$q((resolve, reject) => {
                this.datasourceService.query({
                    sort: 'name,asc'
                }).then(response => {
                    resolve(response.results);
                }, (err) => {
                    reject(err);
                });
            });
        }

        /*
          Returns a function which fetches scenes

          Function chain:
          (filters) => (bbox) => () => Future(next page of scenes)
        */
        fetchScenes(filters) {
            const params = Object.assign({}, filters);
            if (filters.shape) {
                filters.shape = filters.shape.id;
            }

            const fetchForBbox = (bbox) => {
                let hasNext = null;
                let page = 0;
                let requestTime = new Date().toISOString();

                return () => {
                    return this.$q((resolve, reject) => {
                        if (hasNext !== null && !hasNext) {
                            reject('No more scenes to fetch.');
                        }
                        this.sceneService.query(
                            Object.assign({
                                sort: 'acquisitionDatetime,desc',
                                pageSize: '20',
                                page,
                                bbox,
                                maxCreateDatetime: requestTime
                            }, params)
                        ).then((response) => {
                            // We aren't supporting concurrent scene paged requests
                            page = page + 1;
                            hasNext = response.hasNext;
                            resolve({
                                scenes: response.results,
                                hasNext,
                                count: this.$filter('number')(response.count)
                            });
                        }, (error) => {
                            reject({
                                error
                            });
                        });
                    });
                };
            };

            return fetchForBbox;
        }

        getThumbnail(scene) {
            return this.$q((resolve, reject) => {
                if (scene.thumbnails.length) {
                    resolve(this.thumbnailService.getBestFitUrl(scene.thumbnails, 75));
                } else {
                    reject();
                }
            });
        }

        getPreview(scene) {
            return this.$q((resolve, reject) => {
                if (scene.thumbnails.length) {
                    resolve(this.thumbnailService.getBestFitUrl(scene.thumbnails, 1000));
                } else {
                    reject();
                }
            });
        }

        getDatasource(scene) {
            return this.$q((resolve, reject) => {
                if (this.datasourceCache.has(scene.datasource)) {
                    let datasource = this.datasourceCache.get(scene.datasource);
                    if (datasource.then) {
                        datasource.then((d) => {
                            resolve(d);
                        });
                    } else {
                        resolve(datasource);
                    }
                } else {
                    this.datasourceCache = this.datasourceCache.set(
                        scene.datasource,
                        this.datasourceService
                            .get(scene.datasource)
                            .then((datasource) => {
                                resolve(datasource);
                                return datasource;
                            }, (err) => {
                                reject(err);
                            })
                    );
                }
            });
        }

        /*
          Returns a function which adds the given RF scenes to the project
         */
        addToProject(projectId, scenes) {
            return this.projectService.addScenes(projectId, scenes.map(scene => scene.id));
        }

        getScenePermissions(scene) {
            let result = [];
            if (scene) {
                result.push('download');
            }
            return result;
        }
    }

    app.service('RasterFoundryRepository', RasterFoundryRepository);
};
<commit_msg>Get all paginated datasources on scene browse (#3114)<commit_after>/* global _*/

import {Map} from 'immutable';
export default (app) => {
    class RasterFoundryRepository {
        constructor(
            $q, $filter,
            authService, datasourceService, sceneService, thumbnailService,
            projectService
        ) {
            this.$q = $q;
            this.$filter = $filter;
            this.authService = authService;
            this.datasourceService = datasourceService;
            this.sceneService = sceneService;
            this.thumbnailService = thumbnailService;
            this.projectService = projectService;
            this.datasourceCache = new Map();
            this.previewOnMap = true;
        }

        initRepository() {
            return this.$q((resolve) => {
                resolve();
            });
        }

        getFilters() {
            return [{
                param: 'datasource',
                label: 'Imagery Sources',
                type: 'search-select',
                getSources: this.getSources.bind(this)
            }, {
                params: {
                    min: 'minAcquisitionDatetime',
                    max: 'maxAcquisitionDatetime'
                },
                label: 'Date Range',
                type: 'daterange',
                default: 'None'
            // TODO enable this filter once the api supports it
            // }, {
            //     type: 'shape',
            //     label: 'Area of Interest',
            //     param: 'shape'
            }, {
                params: {
                    min: 'minCloudCover',
                    max: 'maxCloudCover'
                },
                label: 'Cloud Cover',
                type: 'slider',
                min: 0,
                max: 100,
                ticks: 10,
                step: 10,
                scale: 1
            }, {
                params: {
                    min: 'minSunElevation',
                    max: 'maxSunElevation'
                },
                label: 'Sun Elevation',
                type: 'slider',
                min: 0,
                max: 180,
                ticks: 30,
                step: 10,
                scale: 1
            }, {
                params: {
                    min: 'minSunAzimuth',
                    max: 'maxSunAzimuth'
                },
                label: 'Sun Azimuth',
                type: 'slider',
                min: 0,
                max: 360,
                ticks: 60,
                step: 10,
                scale: 1
            }, {
                param: 'ingested',
                label: 'Ingest Status',
                type: 'tag',
                options: [{
                    label: 'Show all',
                    value: null
                }, {
                    label: 'Uningested Only',
                    value: 'false'
                }, {
                    label: 'Ingested Only',
                    value: 'true'
                }]
            }, {
                param: 'owner',
                label: 'Owner',
                type: 'tag',
                options: [{
                    label: 'All',
                    value: null
                }, {
                    label: 'My Scenes',
                    value: this.authService.getProfile().sub
                }]
            }];
        }

        getSources() {
            let deferred = this.$q.defer();

            let promise = this.datasourceService.query({sort: 'name,asc'}).then((res) => {
                let pageCount = Math.ceil(res.count / res.pageSize);

                let promises = [promise].concat(_.times(pageCount, (idx) => {
                    return this.datasourceService
                        .query({sort: 'name,asc', page: idx + 1})
                        .then(resp => resp, error => error);
                }));

                this.$q.all(promises).then((reps) => {
                    deferred.resolve(_.flatMap(reps, r => r.results));
                }, error => deferred.reject(error));

                return res;
            }, (err) => deferred.reject(err));

            return deferred.promise;
        }

        /*
          Returns a function which fetches scenes

          Function chain:
          (filters) => (bbox) => () => Future(next page of scenes)
        */
        fetchScenes(filters) {
            const params = Object.assign({}, filters);
            if (filters.shape) {
                filters.shape = filters.shape.id;
            }

            const fetchForBbox = (bbox) => {
                let hasNext = null;
                let page = 0;
                let requestTime = new Date().toISOString();

                return () => {
                    return this.$q((resolve, reject) => {
                        if (hasNext !== null && !hasNext) {
                            reject('No more scenes to fetch.');
                        }
                        this.sceneService.query(
                            Object.assign({
                                sort: 'acquisitionDatetime,desc',
                                pageSize: '20',
                                page,
                                bbox,
                                maxCreateDatetime: requestTime
                            }, params)
                        ).then((response) => {
                            // We aren't supporting concurrent scene paged requests
                            page = page + 1;
                            hasNext = response.hasNext;
                            resolve({
                                scenes: response.results,
                                hasNext,
                                count: this.$filter('number')(response.count)
                            });
                        }, (error) => {
                            reject({
                                error
                            });
                        });
                    });
                };
            };

            return fetchForBbox;
        }

        getThumbnail(scene) {
            return this.$q((resolve, reject) => {
                if (scene.thumbnails.length) {
                    resolve(this.thumbnailService.getBestFitUrl(scene.thumbnails, 75));
                } else {
                    reject();
                }
            });
        }

        getPreview(scene) {
            return this.$q((resolve, reject) => {
                if (scene.thumbnails.length) {
                    resolve(this.thumbnailService.getBestFitUrl(scene.thumbnails, 1000));
                } else {
                    reject();
                }
            });
        }

        getDatasource(scene) {
            return this.$q((resolve, reject) => {
                if (this.datasourceCache.has(scene.datasource)) {
                    let datasource = this.datasourceCache.get(scene.datasource);
                    if (datasource.then) {
                        datasource.then((d) => {
                            resolve(d);
                        });
                    } else {
                        resolve(datasource);
                    }
                } else {
                    this.datasourceCache = this.datasourceCache.set(
                        scene.datasource,
                        this.datasourceService
                            .get(scene.datasource)
                            .then((datasource) => {
                                resolve(datasource);
                                return datasource;
                            }, (err) => {
                                reject(err);
                            })
                    );
                }
            });
        }

        /*
          Returns a function which adds the given RF scenes to the project
         */
        addToProject(projectId, scenes) {
            return this.projectService.addScenes(projectId, scenes.map(scene => scene.id));
        }

        getScenePermissions(scene) {
            let result = [];
            if (scene) {
                result.push('download');
            }
            return result;
        }
    }

    app.service('RasterFoundryRepository', RasterFoundryRepository);
};
<|endoftext|>


<commit_before>/*global describe, it, beforeEach, afterEach*/

import expect from 'expect.js';
import CoachMark from './../src/js/CoachMark';

describe('CoachMark', () => {
	let element = null;
	let footer = null;
	let mark = null;

	beforeEach(() => {
		element = document.createElement('div');
		element.innerText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
		element.style.top = '200px';
		element.style.position = 'absolute';
		element.style.width = '60%';
		element.style.margin = 'auto';
		footer = document.createElement('div');
		footer.innerText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
		footer.style.top = '600px';
		footer.style.width = '60%';
		footer.style.position = 'absolute';
		document.body.appendChild(element);
		document.body.appendChild(footer);
	});

	afterEach(() => {
		document.body.removeChild(element);
		document.body.removeChild(footer);
		if(document.querySelector('.o-coach-mark__container')) {
			document.body.removeChild(
				document.querySelector('.o-coach-mark__container'));
		}
	});

	it('should initialize', () => {
		mark = new CoachMark(element, {
			placement: 'top',
			title: 'bar',
			text: 'baz'
		}, function () {
			console.log('test');
		});
		expect(mark).to.be.an('object');
	});

	it('should throw an Error if no parent element', () => {
		expect(function () {
			new CoachMark(null , {
					placement: 'top',
					title: 'foo',
					text: 'bar'
				}, function () {
					console.log('test');
				})}).to.throwError();
	});

	it('should throw an Error if there is no text', () => {
		expect(function () {
			new CoachMark(element , {
					placement: 'top',
					title: 'bar'
				}, function () {
					console.log('test');
				})
			}).to.throwError();
	});

	it('should default to bottom if there is no placement', () => {
		mark = new CoachMark(element, {
			title: 'foo',
			text: 'bar'
		}, function () {
		});
		expect(mark.opts.placement).to.be('bottom');
	});

	it('should correctly calculate placement if placed above the feature', () => {
		mark = new CoachMark(element, {
			placement: 'top',
			title: 'foo',
			text: 'bar'
		}, function () {});
		const pos = document.querySelector('.o-coach-mark__container')
			.getBoundingClientRect();
		expect(pos.top).to.be(122);
		expect(pos.left).to.be(85);
	});

	it('should correctly calculate placement if placed below the feature', () => {
		mark = new CoachMark(element, {
			placement: 'bottom',
			title: 'foo',
			text: 'bar'
		}, function () {});
		const pos = document.querySelector('.o-coach-mark__container')
			.getBoundingClientRect();
		expect(pos.top).to.be(244);
		expect(pos.left).to.be(85);
	});

	it('should correctly calculate placement if placed left of the feature', () => {
		mark = new CoachMark(element, {
			placement: 'left',
			title: 'foo',
			text: 'bar'
		}, function () {});
		const pos = document.querySelector('.o-coach-mark__container')
			.getBoundingClientRect();
		expect(pos.top).to.be(208);
		expect(pos.left).to.be(64);
	});

	it('should correctly calculate placement if placed right of the feature', () => {
		mark = new CoachMark(element, {
			placement: 'right',
			title: 'foo',
			text: 'bar'
		}, function () {});
		const pos = document.querySelector('.o-coach-mark__container')
			.getBoundingClientRect();
		expect(pos.top).to.be(208);
		expect(pos.left).to.be(315);
	});

	it('should call the callback when dismissed', () => {
		let called = false;
		mark = new CoachMark(element, {
			placement: 'right',
			title: 'foo',
			text: 'bar'
		}, function() { called = true; });
		const link = document.querySelector('.o-coach-mark__container a');

		const ev = document.createEvent("MouseEvent");
	 	ev.initMouseEvent("click", true, true, window);
		link.dispatchEvent(ev);
		expect(called).to.be(true);
	});
});
<commit_msg>chore: fix unit test<commit_after>/*global describe, it, beforeEach, afterEach*/

import expect from 'expect.js';
import CoachMark from './../src/js/CoachMark';

describe('CoachMark', () => {
	let element = null;
	let footer = null;
	let mark = null;

	beforeEach(() => {
		element = document.createElement('div');
		element.innerText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
		element.style.top = '200px';
		element.style.position = 'absolute';
		element.style.width = '60%';
		element.style.margin = 'auto';
		footer = document.createElement('div');
		footer.innerText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
		footer.style.top = '600px';
		footer.style.width = '60%';
		footer.style.position = 'absolute';
		document.body.appendChild(element);
		document.body.appendChild(footer);
	});

	afterEach(() => {
		document.body.removeChild(element);
		document.body.removeChild(footer);
		if(document.querySelector('.o-coach-mark__container')) {
			document.body.removeChild(
				document.querySelector('.o-coach-mark__container'));
		}
	});

	it('should initialize', () => {
		mark = new CoachMark(element, {
			placement: 'top',
			title: 'bar',
			text: 'baz'
		}, function () {
			console.log('test');
		});
		expect(mark).to.be.an('object');
	});

	it('should throw an Error if no parent element', () => {
		expect(function () {
			new CoachMark(null , {
					placement: 'top',
					title: 'foo',
					text: 'bar'
				}, function () {
					console.log('test');
				})}).to.throwError();
	});

	it('should throw an Error if there is no text', () => {
		expect(function () {
			new CoachMark(element , {
					placement: 'top',
					title: 'bar'
				}, function () {
					console.log('test');
				})
			}).to.throwError();
	});

	it('should default to bottom if there is no placement', () => {
		mark = new CoachMark(element, {
			title: 'foo',
			text: 'bar'
		}, function () {
		});
		expect(mark.opts.placement).to.be('bottom');
	});

	it('should correctly calculate placement if placed above the feature', () => {
		mark = new CoachMark(element, {
			placement: 'top',
			title: 'foo',
			text: 'bar'
		}, function () {});
		const pos = document.querySelector('.o-coach-mark__container')
			.getBoundingClientRect();
		expect(pos.top<200).to.be(true);
	});

	it('should correctly calculate placement if placed below the feature', () => {
		mark = new CoachMark(element, {
			placement: 'bottom',
			title: 'foo',
			text: 'bar'
		}, function () {});
		const pos = document.querySelector('.o-coach-mark__container')
			.getBoundingClientRect();
		expect(pos.top>200).to.be(true);
	});

	it('should correctly calculate placement if placed left of the feature', () => {
		mark = new CoachMark(element, {
			placement: 'left',
			title: 'foo',
			text: 'bar'
		}, function () {});
		const pos = document.querySelector('.o-coach-mark__container')
			.getBoundingClientRect();
		expect(pos.left<100).to.be(true);
	});

	it('should correctly calculate placement if placed right of the feature', () => {
		mark = new CoachMark(element, {
			placement: 'right',
			title: 'foo',
			text: 'bar'
		}, function () {});
		const pos = document.querySelector('.o-coach-mark__container')
			.getBoundingClientRect();
		expect(pos.left>300).to.be(true);
	});

	it('should call the callback when dismissed', () => {
		let called = false;
		mark = new CoachMark(element, {
			placement: 'right',
			title: 'foo',
			text: 'bar'
		}, function() { called = true; });
		const link = document.querySelector('.o-coach-mark__container a');

		const ev = document.createEvent("MouseEvent");
	 	ev.initMouseEvent("click", true, true, window);
		link.dispatchEvent(ev);
		expect(called).to.be(true);
	});
});
<|endoftext|>


<commit_before>var script = d3.select('#provisualizer-script').attr('src');
var baseUrl = script.substring(0, script.lastIndexOf('provisualizer.js'));

// add stylesheet
var stylesheet = d3.select('head')
	.append('link')
		.attr('type', 'text/css')
		.attr('rel', 'stylesheet')
		.attr('href', baseUrl + 'provisualizer.css');

var maxLabelLength = 30;
var provisualizer = d3.select("#provisualizer");
var width = provisualizer.node().offsetWidth;
var height = provisualizer.node().offsetHeight;

addSearchForm();

var force = d3.layout.force()
	.size([width, height])
	.charge(-400)
	.chargeDistance(350)
	.linkDistance(50)
	.on("tick", tick);

var drag = force.drag()
	.on("dragstart", dragstart);

var svg = d3.select("#provisualizer").append("svg")
	.attr("width", "100%")
	.attr("height", "100%");

	var linkLines = svg.selectAll(".link");
	var nodeCircles = svg.selectAll("circle.node");
	var nodeLabels = svg.selectAll("text.node");

	// array of rows read from CSV (unfiltered)
	var agencyFunctionRelationships = [];

	// map from node names to node attributes
	var nodeAttributesByNodeName = {};

d3.csv(
	baseUrl + "data/nodes.csv",
	function(error, nodeAttributes) {

		nodeAttributes.forEach(
			function(nodeAttributeSet) {
				nodeAttributesByNodeName[nodeAttributeSet.FUNCTION] = nodeAttributeSet; // though we don't really need to copy the FUNCTION column
			}
		);
		// Now these node attributes can be read below
		// Note: the "FUNCTION" columns is misnamed because it includes both functions AND agencies
		// Also the table should really have included a TYPE column to specify "function" or "agency"
		d3.csv(
			baseUrl + "data/agency-functions.csv",
			function(error, agencyFunctionRelationshipsCSV) {
				agencyFunctionRelationships = agencyFunctionRelationshipsCSV;
				createFilteredGraphFromLinks();
			}
		)
	}
);

function tick() {
	linkLines
		.attr("x1", function(d) { return d.source.x; })
		.attr("y1", function(d) { return d.source.y; })
		.attr("x2", function(d) { return d.target.x; })
		.attr("y2", function(d) { return d.target.y; });
	nodeLabels
		.attr("x", function(d) { return d.x; })
		.attr("y", function(d) { return d.y; });
	nodeCircles
		.attr("cx", function(d) { return d.x; })
		.attr("cy", function(d) { return d.y; });
}

function dblclick(d) {
  d3.select(this).classed("fixed", d.fixed = false);
}

function dragstart(d) {
  d3.select(this).classed("fixed", d.fixed = true);
}

function jump(d) {
	window.location = d.URL
}

function createFilteredGraphFromLinks() {
	// Extract the distinct nodes from the node relationship table.
	var nameFilter = d3.select('#agency-or-function-name-filter').property("value").toUpperCase();

	// arrays containing the node objects, and the link objects, for D3 force layout
	var nodes = [];
	var links = [];
	// map from node names to node array indices
	var nodeIndicesByNodeName = {};

	agencyFunctionRelationships.forEach(
		function(agencyFunctionRow) {

			if (agencyFunctionRow.FUNCTION.toUpperCase().indexOf(nameFilter) != -1 || agencyFunctionRow.AGENCY.toUpperCase().indexOf(nameFilter) != -1) {

				if (! (agencyFunctionRow.AGENCY in nodeIndicesByNodeName)) {
					// no node with that name yet
					var newNodeIndex = nodes.length;
					var agencyNode =  {
						name: agencyFunctionRow.AGENCY,
						type: "agency"
					};
					var nodeAttributes = nodeAttributesByNodeName[agencyFunctionRow.AGENCY];
					for(var p in nodeAttributes) agencyNode[p]=nodeAttributes[p];
					nodes[newNodeIndex] = agencyNode;
					nodeIndicesByNodeName[agencyFunctionRow.AGENCY] = newNodeIndex;
					addNodeLabel(agencyNode, nodes, links);
				}
				if (! (agencyFunctionRow.FUNCTION in nodeIndicesByNodeName)) {
					// no node with that name yet
					var newNodeIndex = nodes.length;
					var functionNode = {
						name: agencyFunctionRow.FUNCTION,
						type: "function"
					};
					var nodeAttributes = nodeAttributesByNodeName[agencyFunctionRow.FUNCTION];
					for(var p in nodeAttributes) functionNode[p]=nodeAttributes[p];
					nodes[newNodeIndex] = functionNode;
					nodeIndicesByNodeName[agencyFunctionRow.FUNCTION] = newNodeIndex;
					addNodeLabel(functionNode, nodes, links);
				}
				links.push(
					{
						source: nodes[nodeIndicesByNodeName[agencyFunctionRow.AGENCY]],
						target: nodes[nodeIndicesByNodeName[agencyFunctionRow.FUNCTION]]
					}
				)
			}
		}
	);

	// create force layout
	force
		.nodes(nodes)
		.links(links)
		.start();

	// (re-)populate force layout

	linkLines = linkLines.data(
		links,
		function(d) {
			return d.source.name + "->" + d.target.name;
		}
	);
	linkLines.exit().remove();
	linkLines
		.enter().append("line")
		.attr("class", "link");

	nodeCircles = nodeCircles.data(
		nodes,
		function(d) {
			return d.name + d.type;
		}
	);

	nodeCircles.exit().remove();

	nodeCircles.enter()
		.append("circle")
			.attr(
				"class",
				function(n) {
					return n.type + " node";
				}
			)
			.attr(
				"title",
				function(n) {
					return n.name;
				}
			)
			.attr(
				"r",
				function(n) {
					//return 10;
					return Math.sqrt(n.weight) * 10;
				}
			)
			// opacity depends on the node degree - node with infinite degree would be solid, degree 1 would be almost transparent
			.style(
				"fill-opacity",
				function(n) {
					return 1 - (.8/ n.weight);
				}
			)
			.on("dblclick", dblclick)
			.call(drag);

	nodeLabels = nodeLabels.data(
		nodes,
		function(d) {
			return d.name + d.type;
		}
	);

	nodeLabels.exit().remove();

	nodeLabels.enter()
		.append("text")
			.attr(
				"class",
				function(n) {
					return n.type + " node label";
				}
			)
			//.attr("x", function(d) { return d.cx; })
			//.attr("y", function(d) { return d.cy; })
			.text(
				function (n) {
					if (n.name.length > maxLabelLength)
						return n.name.substring(0, maxLabelLength) + "..."
					else
						return n.name;
				}
			)
			.attr(
				"title",
				function(n) {
					return n.name;
				}
			)
			.on("click", jump);

}

// experiment to add agency and function labels as nodes in their own right, linked to the circles representing agencies and functions
function addNodeLabel(node, nodes, links) {
	return; // disabled
	// attach a new label node to the specified node
	var nodeIndex = nodes.length;
	var labelNode = {
		name: node.name,
		type: "label"
	};
	nodes[nodeIndex] = labelNode;
	links.push(
		{
			source: nodes[nodeIndex - 1],
			target: nodes[nodeIndex]
		}
	)
}

function addSearchForm() {
	// search for the word specified in the URL fragment identifier
	var searchPhrase = "road";
	var fragment = window.location.hash;
	if (fragment) {
		// trim the leading # and decode the fragment identifier
		searchPhrase = decodeURIComponent(fragment.substring(1));
	}
	var searchForm = provisualizer.append("form");
	var textSearch = searchForm.append("input")
		.attr("id", "agency-or-function-name-filter")
		.attr("type", "text")
		.attr("width", "180px")
		.attr("height", "80px")
		.property("value", searchPhrase);

	searchForm
		.on(
			"submit",
			function(d, i) {
				// this event is now handled
				d3.event.preventDefault();
				createFilteredGraphFromLinks();
				window.location.hash = "#" + encodeURIComponent(textSearch.property("value"));
			}
		);
}
<commit_msg>refactoring towards date filtering<commit_after>var script = d3.select('#provisualizer-script').attr('src');
var baseUrl = script.substring(0, script.lastIndexOf('provisualizer.js'));

// add stylesheet
var stylesheet = d3.select('head')
	.append('link')
		.attr('type', 'text/css')
		.attr('rel', 'stylesheet')
		.attr('href', baseUrl + 'provisualizer.css');

var maxLabelLength = 30;
var provisualizer = d3.select("#provisualizer");
var width = provisualizer.node().offsetWidth;
var height = provisualizer.node().offsetHeight;

addSearchForm();

var force = d3.layout.force()
	.size([width, height])
	.charge(-400)
	.chargeDistance(350)
	.linkDistance(50)
	.on("tick", tick);

var drag = force.drag()
	.on("dragstart", dragstart);

var svg = d3.select("#provisualizer").append("svg")
	.attr("width", "100%")
	.attr("height", "100%");

	var linkLines = svg.selectAll(".link");
	var nodeCircles = svg.selectAll("circle.node");
	var nodeLabels = svg.selectAll("text.node");

	// array of rows read from CSV (unfiltered)
	var agencyFunctionRelationships = [];

	// map from node names to node attributes
	var nodeAttributesByNodeName = {};

d3.csv(
	baseUrl + "data/nodes.csv",
	function(error, nodeAttributes) {

		nodeAttributes.forEach(
			function(nodeAttributeSet) {
				nodeAttributesByNodeName[nodeAttributeSet.FUNCTION] = nodeAttributeSet; // though we don't really need to copy the FUNCTION column
			}
		);
		// Now these node attributes can be read below
		// Note: the "FUNCTION" columns is misnamed because it includes both functions AND agencies
		// Also the table should really have included a TYPE column to specify "function" or "agency"
		d3.csv(
			baseUrl + "data/agency-functions.csv",
			function(error, agencyFunctionRelationshipsCSV) {
				agencyFunctionRelationships = agencyFunctionRelationshipsCSV;
				createFilteredGraphFromLinks();
			}
		)
	}
);

function tick() {
	linkLines
		.attr("x1", function(d) { return d.source.x; })
		.attr("y1", function(d) { return d.source.y; })
		.attr("x2", function(d) { return d.target.x; })
		.attr("y2", function(d) { return d.target.y; });
	nodeLabels
		.attr("x", function(d) { return d.x; })
		.attr("y", function(d) { return d.y; });
	nodeCircles
		.attr("cx", function(d) { return d.x; })
		.attr("cy", function(d) { return d.y; });
}

function dblclick(d) {
  d3.select(this).classed("fixed", d.fixed = false);
}

function dragstart(d) {
  d3.select(this).classed("fixed", d.fixed = true);
}

function jump(d) {
	window.location = d.URL
}

function createFilteredGraphFromLinks() {
	// Extract the distinct nodes from the node relationship table.

	// arrays containing the node objects, and the link objects, for D3 force layout
	var nodes = [];
	var links = [];
	// map from node names to node array indices
	var nodeIndicesByNodeName = {};

	agencyFunctionRelationships.forEach(
		function(agencyFunctionRow) {

			if (matchesTextFilter(agencyFunctionRow.FUNCTION) || matchesTextFilter(agencyFunctionRow.AGENCY)) {

				if (! (agencyFunctionRow.AGENCY in nodeIndicesByNodeName)) {
					// no node with that name yet
					var newNodeIndex = nodes.length;
					var agencyNode =  {
						name: agencyFunctionRow.AGENCY,
						type: "agency"
					};
					var nodeAttributes = nodeAttributesByNodeName[agencyFunctionRow.AGENCY];
					for(var p in nodeAttributes) agencyNode[p]=nodeAttributes[p];
					nodes[newNodeIndex] = agencyNode;
					nodeIndicesByNodeName[agencyFunctionRow.AGENCY] = newNodeIndex;
					addNodeLabel(agencyNode, nodes, links);
				}
				if (! (agencyFunctionRow.FUNCTION in nodeIndicesByNodeName)) {
					// no node with that name yet
					var newNodeIndex = nodes.length;
					var functionNode = {
						name: agencyFunctionRow.FUNCTION,
						type: "function"
					};
					var nodeAttributes = nodeAttributesByNodeName[agencyFunctionRow.FUNCTION];
					for(var p in nodeAttributes) functionNode[p]=nodeAttributes[p];
					nodes[newNodeIndex] = functionNode;
					nodeIndicesByNodeName[agencyFunctionRow.FUNCTION] = newNodeIndex;
					addNodeLabel(functionNode, nodes, links);
				}
				links.push(
					{
						source: nodes[nodeIndicesByNodeName[agencyFunctionRow.AGENCY]],
						target: nodes[nodeIndicesByNodeName[agencyFunctionRow.FUNCTION]]
					}
				)
			}
		}
	);

	// create force layout
	force
		.nodes(nodes)
		.links(links)
		.start();

	// (re-)populate force layout

	linkLines = linkLines.data(
		links,
		function(d) {
			return d.source.name + "->" + d.target.name;
		}
	);
	linkLines.exit().remove();
	linkLines
		.enter().append("line")
		.attr("class", "link");

	nodeCircles = nodeCircles.data(
		nodes,
		function(d) {
			return d.name + d.type;
		}
	);

	nodeCircles.exit().remove();

	nodeCircles.enter()
		.append("circle")
			.attr(
				"class",
				function(n) {
					return n.type + " node";
				}
			)
			.attr(
				"title",
				function(n) {
					return n.name;
				}
			)
			.attr(
				"r",
				function(n) {
					//return 10;
					return Math.sqrt(n.weight) * 10;
				}
			)
			// opacity depends on the node degree - node with infinite degree would be solid, degree 1 would be almost transparent
			.style(
				"fill-opacity",
				function(n) {
					return 1 - (.8/ n.weight);
				}
			)
			.on("dblclick", dblclick)
			.call(drag);

	nodeLabels = nodeLabels.data(
		nodes,
		function(d) {
			return d.name + d.type;
		}
	);

	nodeLabels.exit().remove();

	nodeLabels.enter()
		.append("text")
			.attr(
				"class",
				function(n) {
					return n.type + " node label";
				}
			)
			//.attr("x", function(d) { return d.cx; })
			//.attr("y", function(d) { return d.cy; })
			.text(
				function (n) {
					if (n.name.length > maxLabelLength)
						return n.name.substring(0, maxLabelLength) + "..."
					else
						return n.name;
				}
			)
			.attr(
				"title",
				function(n) {
					return n.name;
				}
			)
			.on("click", jump);

}

// experiment to add agency and function labels as nodes in their own right, linked to the circles representing agencies and functions
function addNodeLabel(node, nodes, links) {
	return; // disabled
	// attach a new label node to the specified node
	var nodeIndex = nodes.length;
	var labelNode = {
		name: node.name,
		type: "label"
	};
	nodes[nodeIndex] = labelNode;
	links.push(
		{
			source: nodes[nodeIndex - 1],
			target: nodes[nodeIndex]
		}
	)
}

function addSearchForm() {
	// search for the word specified in the URL fragment identifier
	var searchPhrase = "road";
	var fragment = window.location.hash;
	if (fragment) {
		// trim the leading # and decode the fragment identifier
		searchPhrase = decodeURIComponent(fragment.substring(1));
	}
	var searchForm = provisualizer.append("form");
	var textSearch = searchForm.append("input")
		.attr("id", "agency-or-function-name-filter")
		.attr("type", "text")
		.attr("width", "180px")
		.attr("height", "80px")
		.property("value", searchPhrase);

	searchForm
		.on(
			"submit",
			function(d, i) {
				// this event is now handled
				d3.event.preventDefault();
				createFilteredGraphFromLinks();
				window.location.hash = "#" + encodeURIComponent(textSearch.property("value"));
			}
		);
}

function matchesTextFilter(text) {
	var textFilter = d3.select('#agency-or-function-name-filter').property("value").toUpperCase();
	return text.toUpperCase().indexOf(textFilter) != -1
}
<|endoftext|>


<commit_before>(function ($) {
  function allFieldsValid() {
    var fields = [
      'BatchVol',
      'GBill',
      'HBill',
      'DHop',
      'BoilTime',
      'BoilRate',
      'TempGrain',
      'TempMash',
      'VolSparge',
      'PotSize',
      'KettleID',
      'LossTrub',
      'LossFermTrub',
      'HChilled',
      'VolPackaged',
      'Gabs',
      'Habs',
      'EBoil',
    ];
    for (i = 0; i < fields.length; i++) {
      if (!$('#' + fields[i]).val().match(/^d*(.\d+)?/)) {
        return false;
      }
    }
    return true;
  }
  function updateCalc() {
    if (!allFieldsValid()) {
      return;
    }
    var BatchVol = parseFloat($('#BatchVol').val()),
    GBill = parseFloat($('#GBill').val()),
    HBill = parseFloat($('#HBill').val()),
    DHop = parseFloat($('#DHop').val()),
    BoilTime = parseFloat($('#BoilTime').val()),
    BoilRate = parseFloat($('#BoilRate').val()),
    TempGrain = parseFloat($('#TempGrain').val()),
    TempMash = parseFloat($('#TempMash').val()),
    VolSparge = parseFloat($('#VolSparge').val()),
    PotSize = parseFloat($('#PotSize').val()),
    KettleID = parseFloat($('#KettleID').val()),
    LossTrub = parseFloat($('#LossTrub').val()),
    Gabs = parseFloat($('#Gabs').val()),
    Habs = parseFloat($('#Habs').val()),
    MashAdj = parseFloat($('#MashAdj').val()),
    StrikeAdj = parseFloat($('#StrikeAdj').val()),
    LossFermTrub = parseFloat($('#LossFermTrub').val()),
    LossBoil = BoilTime * BoilRate / 60,
    LossHop = HBill * Habs,
    LossGrain = GBill * Gabs,
    LossTot = LossGrain + LossHop + LossBoil + LossTrub,
    WaterTot = BatchVol + LossTot,
    VolStart = (WaterTot - VolSparge),
    TempStrike = TempMash + (0.05 * GBill / VolStart) * (TempMash - TempGrain),
    MashAdj = 1.022494888,
    //( 4.13643 * Math.pow(10,-16) * Math.pow($('#TempMash').val(),6) - 4.05998 * Math.pow(10,-13) * Math.pow($('#TempMash').val(),5) + 1.61536 * Math.pow(10,-10) * Math.pow($('#TempMash').val(),4) - 3.44854 * Math.pow(10,-8) * Math.pow($('#TempMash').val(),3) + 0.00000532769 * Math.pow($('#TempMash').val(),2) - 0.000292675 * $('#TempMash').val() + 1.00493),
    StrikeAdj = 1.025641026,
    //( 4.13643 * Math.pow(10,-16) * Math.pow($('#TempStrike').val(),6) - 4.05998 * Math.pow(10,-13) * Math.pow($('#TempStrike').val(),5) + 1.61536 * Math.pow(10,-10) * Math.pow($('#TempStrike').val(),4) - 3.44854 * Math.pow(10,-8) * Math.pow($('#TempStrike').val(),3) + 0.00000532769 * Math.pow($('#TempStrike').val(),2) - 0.000292675 * $('#TempStrike').val() + 1.00493),
    VolStrike = VolStart * StrikeAdj,
    LossHop = HBill * Habs,
    LossGrain = GBill * Gabs,
    VolMash = (VolStart + GBill * 0.08) * MashAdj,
    VolPre = (WaterTot - LossGrain) * 1.043841336,
    VolPost = (WaterTot - LossTot + LossTrub) * 1.043841336,
    VolChilled = ( VolPost / 1.043841336 ) - LossTrub,
    VolPackaged = VolChilled - LossFermTrub - ( DHop * Gabs ),
    GalH = 294.118334834 / (KettleID * KettleID),
    HTot = GalH * WaterTot,
    HStart = GalH * VolStart,
    HStrike = GalH * VolStrike,
    HMash = GalH * VolMash,
    HPre = GalH * VolPre,
    HChilled = GalH * VolChilled,
    MashThick = VolStart * 4 / GBill,
    VolMinSparge = Math.max(0,((WaterTot + GBill * 0.08) * MashAdj) - (PotSize - 0.01 )),
    HPost = GalH * VolPost,
    EBoil = (0.0058 * KettleID * KettleID) - (0.0009 * KettleID) + 0.0038;

    // console.log(VolStrike, WaterTot, MashThick, TempStrike);
    $('#WaterTot').text(WaterTot.toFixed(2));
    $('#VolStrike').text(VolStrike.toFixed(2));
    $('#LossBoil').text(LossBoil.toFixed(2));
    $('#LossHop').text(LossHop.toFixed(2));
    $('#LossGrain').text(LossGrain.toFixed(2));
    $('#LossTot').text(LossTot.toFixed(2));
    $('#LossFermTrub').text(LossFermTrub.toFixed(2));
    $('#VolStart').text(VolStart.toFixed(2));
    $('#VolMash').text(VolMash.toFixed(2));
    $('#VolPre').text(VolPre.toFixed(2));
    $('#VolPost').text(VolPost.toFixed(2));
    $('#TempStrike').text(TempStrike.toFixed(2));
    $('#MashAdj').text(MashAdj.toFixed(12));
    $('#Strikeadj').text(StrikeAdj.toFixed(12));
    $('#GalH').text(GalH.toFixed(3));
    $('#HTot').text(HTot.toFixed(2));
    $('#HStart').text(HStart.toFixed(2));
    $('#HStrike').text(HStrike.toFixed(2));
    $('#HMash').text(HMash.toFixed(2));
    $('#HPre').text(HPre.toFixed(2));
    $('#HPost').text(HPost.toFixed(2));
    $('#DHop').text(DHop.toFixed(2));
    $('#HChilled').text(HChilled.toFixed(2));
    $('#MashThick').text(MashThick.toFixed(2));
    $('#VolMinSparge').text(VolMinSparge.toFixed(2));
    $('#VolChilled').text(VolChilled.toFixed(2));
    $('#VolPackaged').text(VolPackaged.toFixed(2));
    $('#EBoil').text(VolPackaged.toFixed(2));
  }
  function validateField(field) {
    $field = $(field);
    if ($field.val().match(/^d*(.\d+)?/)) {
      $field.parents('div.control-group:first').removeClass('error');
      return true;
    }
    $field.parents('div.control-group:first').addClass('error');
    return false;
  }
  $(document).ready(function () {
    $('#calcForm').delegate('input[type=text]', 'change', function () {
      if (validateField(this)) {
        updateCalc();
      }
    }).submit(function (e) {
      e.defaultPrevented;
      updateCalc();
    });
    updateCalc();
    $('#GBill').focus();
  });
}) (jQuery);
<commit_msg>Update Testing1.js<commit_after>(function ($) {
  function allFieldsValid() {
    var fields = [
      'BatchVol',
      'GBill',
      'HBill',
      'DHop',
      'BoilTime',
      'BoilRate',
      'TempGrain',
      'TempMash',
      'VolSparge',
      'PotSize',
      'KettleID',
      'LossTrub',
      'LossFermTrub',
      'HChilled',
      'VolPackaged',
      'Gabs',
      'Habs',
      'EBoil',
    ];
    for (i = 0; i < fields.length; i++) {
      if (!$('#' + fields[i]).val().match(/^d*(.\d+)?/)) {
        return false;
      }
    }
    return true;
  }
  function updateCalc() {
    if (!allFieldsValid()) {
      return;
    }
    var BatchVol = parseFloat($('#BatchVol').val()),
    GBill = parseFloat($('#GBill').val()),
    HBill = parseFloat($('#HBill').val()),
    DHop = parseFloat($('#DHop').val()),
    BoilTime = parseFloat($('#BoilTime').val()),
    BoilRate = parseFloat($('#BoilRate').val()),
    TempGrain = parseFloat($('#TempGrain').val()),
    TempMash = parseFloat($('#TempMash').val()),
    VolSparge = parseFloat($('#VolSparge').val()),
    PotSize = parseFloat($('#PotSize').val()),
    KettleID = parseFloat($('#KettleID').val()),
    LossTrub = parseFloat($('#LossTrub').val()),
    Gabs = parseFloat($('#Gabs').val()),
    Habs = parseFloat($('#Habs').val()),
    MashAdj = parseFloat($('#MashAdj').val()),
    StrikeAdj = parseFloat($('#StrikeAdj').val()),
    LossFermTrub = parseFloat($('#LossFermTrub').val()),
    LossBoil = BoilTime * BoilRate / 60,
    LossHop = HBill * Habs,
    LossGrain = GBill * Gabs,
    LossTot = LossGrain + LossHop + LossBoil + LossTrub,
    WaterTot = BatchVol + LossTot,
    VolStart = (WaterTot - VolSparge),
    TempStrike = TempMash + (0.05 * GBill / VolStart) * (TempMash - TempGrain),
    MashAdj = 1.022494888,
    //( 4.13643 * Math.pow(10,-16) * Math.pow($('#TempMash').val(),6) - 4.05998 * Math.pow(10,-13) * Math.pow($('#TempMash').val(),5) + 1.61536 * Math.pow(10,-10) * Math.pow($('#TempMash').val(),4) - 3.44854 * Math.pow(10,-8) * Math.pow($('#TempMash').val(),3) + 0.00000532769 * Math.pow($('#TempMash').val(),2) - 0.000292675 * $('#TempMash').val() + 1.00493),
    StrikeAdj = 1.025641026,
    //( 4.13643 * Math.pow(10,-16) * Math.pow($('#TempStrike').val(),6) - 4.05998 * Math.pow(10,-13) * Math.pow($('#TempStrike').val(),5) + 1.61536 * Math.pow(10,-10) * Math.pow($('#TempStrike').val(),4) - 3.44854 * Math.pow(10,-8) * Math.pow($('#TempStrike').val(),3) + 0.00000532769 * Math.pow($('#TempStrike').val(),2) - 0.000292675 * $('#TempStrike').val() + 1.00493),
    VolStrike = VolStart * StrikeAdj,
    LossHop = HBill * Habs,
    LossGrain = GBill * Gabs,
    VolMash = (VolStart + GBill * 0.08) * MashAdj,
    VolPre = (WaterTot - LossGrain) * 1.043841336,
    VolPost = (WaterTot - LossTot + LossTrub) * 1.043841336,
    VolChilled = ( VolPost / 1.043841336 ) - LossTrub,
    VolPackaged = VolChilled - LossFermTrub - ( DHop * Gabs ),
    GalH = 294.118334834 / (KettleID * KettleID),
    HTot = GalH * WaterTot,
    HStart = GalH * VolStart,
    HStrike = GalH * VolStrike,
    HMash = GalH * VolMash,
    HPre = GalH * VolPre,
    HChilled = GalH * VolChilled,
    MashThick = VolStart * 4 / GBill,
    VolMinSparge = Math.max(0,((WaterTot + GBill * 0.08) * MashAdj) - (PotSize - 0.01 )),
    HPost = GalH * VolPost,
    EBoil = (0.0058 * KettleID * KettleID) - (0.0009 * KettleID) + 0.0038;

    // console.log(VolStrike, WaterTot, MashThick, TempStrike);
    $('#WaterTot').text(WaterTot.toFixed(2));
    $('#VolStrike').text(VolStrike.toFixed(2));
    $('#LossBoil').text(LossBoil.toFixed(2));
    $('#LossHop').text(LossHop.toFixed(2));
    $('#LossGrain').text(LossGrain.toFixed(2));
    $('#LossTot').text(LossTot.toFixed(2));
    $('#LossFermTrub').text(LossFermTrub.toFixed(2));
    $('#VolStart').text(VolStart.toFixed(2));
    $('#VolMash').text(VolMash.toFixed(2));
    $('#VolPre').text(VolPre.toFixed(2));
    $('#VolPost').text(VolPost.toFixed(2));
    $('#TempStrike').text(TempStrike.toFixed(2));
    $('#MashAdj').text(MashAdj.toFixed(12));
    $('#Strikeadj').text(StrikeAdj.toFixed(12));
    $('#GalH').text(GalH.toFixed(3));
    $('#HTot').text(HTot.toFixed(2));
    $('#HStart').text(HStart.toFixed(2));
    $('#HStrike').text(HStrike.toFixed(2));
    $('#HMash').text(HMash.toFixed(2));
    $('#HPre').text(HPre.toFixed(2));
    $('#HPost').text(HPost.toFixed(2));
    $('#DHop').text(DHop.toFixed(2));
    $('#HChilled').text(HChilled.toFixed(2));
    $('#MashThick').text(MashThick.toFixed(2));
    $('#VolMinSparge').text(VolMinSparge.toFixed(2));
    $('#VolChilled').text(VolChilled.toFixed(2));
    $('#VolPackaged').text(VolPackaged.toFixed(2));
    $('#EBoil').text(EBoil.toFixed(2));
  }
  function validateField(field) {
    $field = $(field);
    if ($field.val().match(/^d*(.\d+)?/)) {
      $field.parents('div.control-group:first').removeClass('error');
      return true;
    }
    $field.parents('div.control-group:first').addClass('error');
    return false;
  }
  $(document).ready(function () {
    $('#calcForm').delegate('input[type=text]', 'change', function () {
      if (validateField(this)) {
        updateCalc();
      }
    }).submit(function (e) {
      e.defaultPrevented;
      updateCalc();
    });
    updateCalc();
    $('#GBill').focus();
  });
}) (jQuery);
<|endoftext|>


<commit_before>/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, brackets, clearTimeout, setTimeout */

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

    /**
     * Adds automatic saving to all documents in the editor. Autosave happens
     * in two cases:
     *
     * 1) A document gets queued for saving when it is marked as dirty by the
     * DocumentManager. We then wait a certain number of ms until we trigger
     * a save for the document. You can change `SAVE_DELAY_MS` below to whatever
     * number you think reasonable.
     *
     * 2) A document gets saved right away if it is dirty and the user switches
     * to another document in the editor. You can disable this by changing
     * `SAVE_ON_EDITOR_CHANGE` below to false.
     */

    // Time in ms to wait after a dirtyFlagChange event before autosaving file
    var SAVE_DELAY_MS = 30 * 1000;

    // Whether or not to autosave immediately when the user switches away from
    // a dirty editor document.
    var SAVE_ON_EDITOR_CHANGE = true;

    var FILE_SAVE       = brackets.getModule("command/Commands").FILE_SAVE,
        SaveCommand     = brackets.getModule("command/CommandManager").get(FILE_SAVE),
        DocumentManager = brackets.getModule("document/DocumentManager"),
        EditorManager   = brackets.getModule("editor/EditorManager");

    // Save operations that are pending
    var pending = {};

    // Schedule a save to happen.  If `immediate` is true, we do the save
    // right now, otherwise we wait a while.
    function scheduleSave(doc, immediate) {
        var path = doc.file.fullPath;

        // Skip subsequent attempts to save, if we're already waiting on a save
        if(pending[path] && !immediate) {
            return;
        }

        // We need to hold a ref to this document for a while so addRef() it.
        doc.addRef();

        function finish() {
            doc.releaseRef();
            clearTimeout(pending[path]);
            delete pending[path];
        }

        function doSave() {
            // If the file has been saved since we queued this save operation
            // (e.g., user did it), bail right now.
            if(!doc.isDirty) {
                finish();
                return;
            }

            SaveCommand.execute({doc: doc})
                .done(finish)
                .fail(function(err) {
                    console.error('[Autosave] failed write for `' + path + '`', err);
                    finish();
                });
        }

        if(immediate) {
            doSave();
        } else {
            pending[path] = setTimeout(doSave, SAVE_DELAY_MS);
        }
    }

    // When the editor's document is flagged as having changes, schedule a save
    DocumentManager.on("dirtyFlagChange", function(evt, doc) {
        if(doc.isDirty) {
            scheduleSave(doc);
        }
    });

    // When the user navigates away from a dirty editor document, save
    EditorManager.on("activeEditorChange", function(evt, current, previous){
        if(previous && previous.document.isDirty && SAVE_ON_EDITOR_CHANGE) {
            // Don't wait to do this save, do it right now
            scheduleSave(previous.document, true);
        }
    });
});
<commit_msg>Autosave after 5s vs. 30s of making an editor dirty<commit_after>/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, brackets, clearTimeout, setTimeout */

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

    /**
     * Adds automatic saving to all documents in the editor. Autosave happens
     * in two cases:
     *
     * 1) A document gets queued for saving when it is marked as dirty by the
     * DocumentManager. We then wait a certain number of ms until we trigger
     * a save for the document. You can change `SAVE_DELAY_MS` below to whatever
     * number you think reasonable.
     *
     * 2) A document gets saved right away if it is dirty and the user switches
     * to another document in the editor. You can disable this by changing
     * `SAVE_ON_EDITOR_CHANGE` below to false.
     */

    // Time in ms to wait after a dirtyFlagChange event before autosaving file
    var SAVE_DELAY_MS = 5 * 1000;

    // Whether or not to autosave immediately when the user switches away from
    // a dirty editor document.
    var SAVE_ON_EDITOR_CHANGE = true;

    var FILE_SAVE       = brackets.getModule("command/Commands").FILE_SAVE,
        SaveCommand     = brackets.getModule("command/CommandManager").get(FILE_SAVE),
        DocumentManager = brackets.getModule("document/DocumentManager"),
        EditorManager   = brackets.getModule("editor/EditorManager");

    // Save operations that are pending
    var pending = {};

    // Schedule a save to happen.  If `immediate` is true, we do the save
    // right now, otherwise we wait a while.
    function scheduleSave(doc, immediate) {
        var path = doc.file.fullPath;

        // Skip subsequent attempts to save, if we're already waiting on a save
        if(pending[path] && !immediate) {
            return;
        }

        // We need to hold a ref to this document for a while so addRef() it.
        doc.addRef();

        function finish() {
            doc.releaseRef();
            clearTimeout(pending[path]);
            delete pending[path];
        }

        function doSave() {
            // If the file has been saved since we queued this save operation
            // (e.g., user did it), bail right now.
            if(!doc.isDirty) {
                finish();
                return;
            }

            SaveCommand.execute({doc: doc})
                .done(finish)
                .fail(function(err) {
                    console.error('[Autosave] failed write for `' + path + '`', err);
                    finish();
                });
        }

        if(immediate) {
            doSave();
        } else {
            pending[path] = setTimeout(doSave, SAVE_DELAY_MS);
        }
    }

    // When the editor's document is flagged as having changes, schedule a save
    DocumentManager.on("dirtyFlagChange", function(evt, doc) {
        if(doc.isDirty) {
            scheduleSave(doc);
        }
    });

    // When the user navigates away from a dirty editor document, save
    EditorManager.on("activeEditorChange", function(evt, current, previous){
        if(previous && previous.document.isDirty && SAVE_ON_EDITOR_CHANGE) {
            // Don't wait to do this save, do it right now
            scheduleSave(previous.document, true);
        }
    });
});
<|endoftext|>


<commit_before>/**
 * @module App
 * @class EmberDropletController
 * @type Ember.Mixin
 * @extends Ember.Mixin
 */
window.DropletController = Ember.Mixin.create({

    /**
     * @property mimeTypes
     * @type {Array}
     */
    mimeTypes: ['image/jpeg', 'image/jpg', 'image/gif', 'image/png', 'text/plain'],

    /**
     * @property files
     * @type {Array}
     * Contains a list of files, both valid, deleted, and invalid.
     */
    files: [],

    /**
     * @property uploadStatus
     * @type {Object}
     */
    uploadStatus: { uploading: false, percentComplete: 0 },

    /**
     * @constructor
     * @method init
     * Clears the file array for each instantiation.
     * @return {void}
     */
    init: function() {
        Ember.set(this, 'files', []);
        this._super();
    },

    /**
     * @method addValidFile
     * @param file {File}
     * Adds a valid file to the collection.
     * @return {Object}
     */
    addValidFile: function(file) {
        return this._addFile(file, true);
    },

    /**
     * @method addInvalidFile
     * @param file {File}
     * Adds an invalid file to the collection.
     * @return {Object}
     */
    addInvalidFile: function(file) {
        return this._addFile(file, false);
    },

    /**
     * @method deleteFile
     * @param file
     * Deletes a file from the collection.
     * @return {Object}
     */
    deleteFile: function(file) {
        Ember.set(file, 'deleted', true);
        return file;
    },

    /**
     * @method clearAllFiles
     * Clears all of the files from the collection.
     * @return {void}
     */
    clearAllFiles: function() {
        Ember.set(this, 'files', []);
    },

    /**
     * @method uploadAllFiles
     * Uploads all of the files that haven't been uploaded yet, but are valid files.
     * @return {Object} jQuery promise.
     */
    uploadAllFiles: function() {

        // Find the URL, set the uploading status, and create our promise.
        var url         = Ember.get(this, 'dropletUrl'),
            deferred    = new jQuery.Deferred();
        Ember.set(this, 'uploadStatus.uploading', true);

        // Assert that we have the URL specified in the controller that implements the mixin.
        Ember.assert('You must specify the `dropletUrl` parameter in order to upload files.', !!url);

        // Create a new XHR request object.
        var request = new XMLHttpRequest();
        request.open('post', url, true);

        // Create a new form data instance.
        var formData = new FormData();

        // Iterate over each file, and upload it.
        Ember.EnumerableUtils.forEach(Ember.get(this, 'validFiles'), function(file) {
            formData.append('file', file.file);
        }, this);

        // Add all of the event listeners.
        this._addProgressListener(request.upload);
        this._addSuccessListener(request.upload, deferred);
        this._addErrorListener(request.upload, deferred);

        // Set the request size, and then we can upload the files!
        request.setRequestHeader('X-File-Size', this._getSize());
        request.send(formData);

        // Return the promise.
        return deferred.promise();

    },

    /**
     * Determine the size of the request.
     * @return {Number}
     * @private
     */
    _getSize: function() {
        var size = 0;
        Ember.EnumerableUtils.forEach(Ember.get(this, 'validFiles'), function(file) {
            size += file.file.size;
        });
        return size;
    },

    /**
     * @method _addSuccessListener
     * @param request
     * @param [deferred = null]
     * @private
     */
    _addSuccessListener: function(request, deferred) {

        // Once the files have been successfully uploaded.
        request.addEventListener('load', function() {

            // Set the `uploaded` parameter to true once we've successfully // uploaded the files.
            Ember.EnumerableUtils.forEach(Ember.get(this, 'validFiles'), function(file) {
                Ember.set(file, 'uploaded', true);
            });

            // We want to revert the upload status.
            Ember.set(this, 'uploadStatus.uploading', false);

            if (deferred) {
                // Last of all we can resolve the promise if it exists!
                deferred.resolve();
            }

        }.bind(this), false);

    },

    /**
     * @method _addErrorListener
     * @param request
     * @param [deferred = null]
     * @return {void}
     * @private
     */
    _addErrorListener: function(request, deferred) {

        request.addEventListener('error', function() {

            // As an error occurred, we need to revert everything.
            Ember.set(this, 'uploadStatus.uploading', false);

            if (deferred) {
                // Reject the promise if we have one.
                deferred.reject();
            }

        }.bind(this));

    },

    /**
     * @method _addProgressListener
     * @param request
     * @return {void}
     * @private
     */
    _addProgressListener: function(request) {

        request.addEventListener('progress', function (event) {

            if (!event.lengthComputable) {
                // There's not much we can do if the request is not computable.
                return;
            }

            // Calculate the percentage remaining.
            var percentageLoaded = (event.loaded / this._getSize()) * 100;
            Ember.set(this, 'uploadStatus.percentComplete', Math.round(percentageLoaded));

        }.bind(this), false);

    },

    /**
     * @method _addFile
     * @param file {File}
     * @param valid {Boolean}
     * Adds a file based on whether it's valid or invalid.
     * @return {Object}
     * @private
     */
    _addFile: function(file, valid) {

        // Extract the file's extension which allows us to style accordingly.
        var className = 'extension-%@'.fmt(file.name.match(/\.(.+)$/i)[1]);

        // Create the record with its default parameters, and then add it to the collection.
        var record = { file: file, valid: valid, uploaded: false, deleted: false, className: className };
        Ember.get(this, 'files').pushObject(record);

        // Voila!
        return record;

    },

    /**
     * @property validFiles
     * Finds a list of files that aren't deleted, and are of a valid MIME type.
     * @return {Array}
     */
    validFiles: Ember.computed(function() {
        return this._filesByProperties({ valid: true, deleted: false, uploaded: false });
    }).property('files.length', 'files.@each.deleted', 'files.@each.uploaded'),

    /**
     * @property invalidFiles
     * Finds a list of files that have an unsupported MIME type.
     * @return {Array}
     */
    invalidFiles: Ember.computed(function() {
        return this._filesByProperties({ valid: false });
    }).property('files.length', 'files.@each.deleted'),

    /**
     * @property uploadedFiles
     * Finds a list of files that have been successfully uploaded.
     * @return {Array}
     */
    uploadedFiles: Ember.computed(function() {
        return this._filesByProperties({ uploaded: true });
    }).property('files.length', 'files.@each.uploaded'),

    /**
     * @property deletedFiles
     * Finds a list of files that have been deleted by the user.
     * @return {Array}
     */
    deletedFiles: Ember.computed(function() {
        return this._filesByProperties({ deleted: true });
    }).property('files.length', 'files.@each.deleted'),

    /**
     * @method _filesByProperties
     * @param maps {Object}
     * Accepts a map of properties that each file must have.
     * @return {Array}
     * @private
     */
    _filesByProperties: function(maps) {

        // Iterate over each of the files.
        return Ember.get(this, 'files').filter(function(file) {

            for (var property in maps) {

                // If the current property doesn't match what we're after from the map,
                // then the file is invalid.
                if (file[property] !== maps[property]) {
                    return false;
                }

            }

            // Voila! We have a good file that matches our criteria.
            return true;

        });
    }

});<commit_msg>Moved private/protected methods to the bottom<commit_after>/**
 * @module App
 * @class EmberDropletController
 * @type Ember.Mixin
 * @extends Ember.Mixin
 */
window.DropletController = Ember.Mixin.create({

    /**
     * @property mimeTypes
     * @type {Array}
     */
    mimeTypes: ['image/jpeg', 'image/jpg', 'image/gif', 'image/png', 'text/plain'],

    /**
     * @property files
     * @type {Array}
     * Contains a list of files, both valid, deleted, and invalid.
     */
    files: [],

    /**
     * @property uploadStatus
     * @type {Object}
     */
    uploadStatus: { uploading: false, percentComplete: 0 },

    /**
     * @constructor
     * @method init
     * Clears the file array for each instantiation.
     * @return {void}
     */
    init: function() {
        Ember.set(this, 'files', []);
        this._super();
    },

    /**
     * @method addValidFile
     * @param file {File}
     * Adds a valid file to the collection.
     * @return {Object}
     */
    addValidFile: function(file) {
        return this._addFile(file, true);
    },

    /**
     * @method addInvalidFile
     * @param file {File}
     * Adds an invalid file to the collection.
     * @return {Object}
     */
    addInvalidFile: function(file) {
        return this._addFile(file, false);
    },

    /**
     * @method deleteFile
     * @param file
     * Deletes a file from the collection.
     * @return {Object}
     */
    deleteFile: function(file) {
        Ember.set(file, 'deleted', true);
        return file;
    },

    /**
     * @method clearAllFiles
     * Clears all of the files from the collection.
     * @return {void}
     */
    clearAllFiles: function() {
        Ember.set(this, 'files', []);
    },

    /**
     * @method uploadAllFiles
     * Uploads all of the files that haven't been uploaded yet, but are valid files.
     * @return {Object} jQuery promise.
     */
    uploadAllFiles: function() {

        // Find the URL, set the uploading status, and create our promise.
        var url         = Ember.get(this, 'dropletUrl'),
            deferred    = new jQuery.Deferred();
        Ember.set(this, 'uploadStatus.uploading', true);

        // Assert that we have the URL specified in the controller that implements the mixin.
        Ember.assert('You must specify the `dropletUrl` parameter in order to upload files.', !!url);

        // Create a new XHR request object.
        var request = new XMLHttpRequest();
        request.open('post', url, true);

        // Create a new form data instance.
        var formData = new FormData();

        // Iterate over each file, and upload it.
        Ember.EnumerableUtils.forEach(Ember.get(this, 'validFiles'), function(file) {
            formData.append('file', file.file);
        }, this);

        // Add all of the event listeners.
        this._addProgressListener(request.upload);
        this._addSuccessListener(request.upload, deferred);
        this._addErrorListener(request.upload, deferred);

        // Set the request size, and then we can upload the files!
        request.setRequestHeader('X-File-Size', this._getSize());
        request.send(formData);

        // Return the promise.
        return deferred.promise();

    },

    /**
     * @property validFiles
     * Finds a list of files that aren't deleted, and are of a valid MIME type.
     * @return {Array}
     */
    validFiles: Ember.computed(function() {
        return this._filesByProperties({ valid: true, deleted: false, uploaded: false });
    }).property('files.length', 'files.@each.deleted', 'files.@each.uploaded'),

    /**
     * @property invalidFiles
     * Finds a list of files that have an unsupported MIME type.
     * @return {Array}
     */
    invalidFiles: Ember.computed(function() {
        return this._filesByProperties({ valid: false });
    }).property('files.length', 'files.@each.deleted'),

    /**
     * @property uploadedFiles
     * Finds a list of files that have been successfully uploaded.
     * @return {Array}
     */
    uploadedFiles: Ember.computed(function() {
        return this._filesByProperties({ uploaded: true });
    }).property('files.length', 'files.@each.uploaded'),

    /**
     * @property deletedFiles
     * Finds a list of files that have been deleted by the user.
     * @return {Array}
     */
    deletedFiles: Ember.computed(function() {
        return this._filesByProperties({ deleted: true });
    }).property('files.length', 'files.@each.deleted'),

    /**
     * @method _filesByProperties
     * @param maps {Object}
     * Accepts a map of properties that each file must have.
     * @return {Array}
     * @private
     */
    _filesByProperties: function(maps) {

        // Iterate over each of the files.
        return Ember.get(this, 'files').filter(function(file) {

            for (var property in maps) {

                // If the current property doesn't match what we're after from the map,
                // then the file is invalid.
                if (file[property] !== maps[property]) {
                    return false;
                }

            }

            // Voila! We have a good file that matches our criteria.
            return true;

        });
    },

    /**
     * Determine the size of the request.
     * @return {Number}
     * @private
     */
    _getSize: function() {
        var size = 0;
        Ember.EnumerableUtils.forEach(Ember.get(this, 'validFiles'), function(file) {
            size += file.file.size;
        });
        return size;
    },

    /**
     * @method _addSuccessListener
     * @param request
     * @param [deferred = null]
     * @private
     */
    _addSuccessListener: function(request, deferred) {

        // Once the files have been successfully uploaded.
        request.addEventListener('load', function() {

            // Set the `uploaded` parameter to true once we've successfully // uploaded the files.
            Ember.EnumerableUtils.forEach(Ember.get(this, 'validFiles'), function(file) {
                Ember.set(file, 'uploaded', true);
            });

            // We want to revert the upload status.
            Ember.set(this, 'uploadStatus.uploading', false);

            if (deferred) {
                // Last of all we can resolve the promise if it exists!
                deferred.resolve();
            }

        }.bind(this), false);

    },

    /**
     * @method _addErrorListener
     * @param request
     * @param [deferred = null]
     * @return {void}
     * @private
     */
    _addErrorListener: function(request, deferred) {

        request.addEventListener('error', function() {

            // As an error occurred, we need to revert everything.
            Ember.set(this, 'uploadStatus.uploading', false);

            if (deferred) {
                // Reject the promise if we have one.
                deferred.reject();
            }

        }.bind(this));

    },

    /**
     * @method _addProgressListener
     * @param request
     * @return {void}
     * @private
     */
    _addProgressListener: function(request) {

        request.addEventListener('progress', function (event) {

            if (!event.lengthComputable) {
                // There's not much we can do if the request is not computable.
                return;
            }

            // Calculate the percentage remaining.
            var percentageLoaded = (event.loaded / this._getSize()) * 100;
            Ember.set(this, 'uploadStatus.percentComplete', Math.round(percentageLoaded));

        }.bind(this), false);

    },

    /**
     * @method _addFile
     * @param file {File}
     * @param valid {Boolean}
     * Adds a file based on whether it's valid or invalid.
     * @return {Object}
     * @private
     */
    _addFile: function(file, valid) {

        // Extract the file's extension which allows us to style accordingly.
        var className = 'extension-%@'.fmt(file.name.match(/\.(.+)$/i)[1]);

        // Create the record with its default parameters, and then add it to the collection.
        var record = { file: file, valid: valid, uploaded: false, deleted: false, className: className };
        Ember.get(this, 'files').pushObject(record);

        // Voila!
        return record;

    }

});<|endoftext|>