<commit_before>

import React, { Component } from "react";
import PropTypes from "prop-types";
import { forceSimulation, forceX, forceCollide } from "d3-force";
import { range as d3Range } from "d3-array";

import GenericChartComponent from "../GenericChartComponent";
import { getAxisCanvas } from "../GenericComponent";
import AxisZoomCapture from "./AxisZoomCapture";

import { first, last, hexToRGBA, isNotDefined, isDefined, identity, zipper, strokeDashTypes, getStrokeDasharray } from "../utils";

class Axis extends Component {
	constructor(props) {
		super(props);
		this.renderSVG = this.renderSVG.bind(this);
		this.drawOnCanvas = this.drawOnCanvas.bind(this);
		this.saveNode = this.saveNode.bind(this);
		this.getMoreProps = this.getMoreProps.bind(this);
	}
	saveNode(node) {
		this.node = node;
	}
	getMoreProps() {
		return this.node.getMoreProps();
	}
	drawOnCanvas(ctx, moreProps) {
		const { showDomain, showTicks, transform, range, getScale } = this.props;

		ctx.save();
		ctx.translate(transform[0], transform[1]);

		if (showDomain) drawAxisLine(ctx, this.props, range);
		if (showTicks) {
			const tickProps = tickHelper(this.props, getScale(moreProps));
			drawTicks(ctx, tickProps);
		}

		ctx.restore();
	}
	renderSVG(moreProps) {
		const { className } = this.props;
		const { showDomain, showTicks, range, getScale } = this.props;

		const ticks = showTicks ? axisTicksSVG(this.props, getScale(moreProps)) : null;
		const domain = showDomain ? axisLineSVG(this.props, range) : null;

		return <g className={className}>
			{ticks}
			{domain}
		</g>;
	}
	render() {
		const { bg, axisZoomCallback, zoomCursorClassName, zoomEnabled, getScale, inverted } = this.props;
		const { transform, getMouseDelta, edgeClip } = this.props;
		const { onContextMenu, onDoubleClick } = this.props;

		const zoomCapture = zoomEnabled
			? <AxisZoomCapture
				bg={bg}
				getScale={getScale}
				getMoreProps={this.getMoreProps}
				getMouseDelta={getMouseDelta}
				axisZoomCallback={axisZoomCallback}
				zoomCursorClassName={zoomCursorClassName}
				inverted={inverted}
				onContextMenu={onContextMenu}
				onDoubleClick={onDoubleClick}
			/>
			: null;

		return <g transform={`translate(${ transform[0] }, ${ transform[1] })`}>
			{zoomCapture}
			<GenericChartComponent ref={this.saveNode}
				canvasToDraw={getAxisCanvas}
				clip={false}
				edgeClip={edgeClip}
				svgDraw={this.renderSVG}
				canvasDraw={this.drawOnCanvas}
				drawOn={["pan"]}
			/>
		</g>;
	}
}

Axis.propTypes = {
	innerTickSize: PropTypes.number,
	outerTickSize: PropTypes.number,
	tickFormat: PropTypes.func,
	tickPadding: PropTypes.number,
	tickSize: PropTypes.number,
	ticks: PropTypes.number,
	tickLabelFill: PropTypes.string,
	tickStroke: PropTypes.string,
	tickStrokeOpacity: PropTypes.number,
	tickStrokeWidth: PropTypes.number,
	tickStrokeDasharray: PropTypes.oneOf(strokeDashTypes),
	tickValues: PropTypes.array,
	tickInterval: PropTypes.number,
	showDomain: PropTypes.bool,
	showTicks: PropTypes.bool,
	className: PropTypes.string,
	axisZoomCallback: PropTypes.func,
	zoomEnabled: PropTypes.bool,
	inverted: PropTypes.bool,
	zoomCursorClassName: PropTypes.string,
	transform: PropTypes.arrayOf(PropTypes.number).isRequired,
	range: PropTypes.arrayOf(PropTypes.number).isRequired,
	getMouseDelta: PropTypes.func.isRequired,
	getScale: PropTypes.func.isRequired,
	bg: PropTypes.object.isRequired,
	edgeClip: PropTypes.bool.isRequired,
	onContextMenu: PropTypes.func,
	onDoubleClick: PropTypes.func,
};

Axis.defaultProps = {
	zoomEnabled: false,
	zoomCursorClassName: "",
	edgeClip: false,
};

function tickHelper(props, scale) {
	const {
		orient, innerTickSize, tickFormat, tickPadding,
		tickLabelFill, tickStrokeWidth, tickStrokeDasharray,
		fontSize, fontFamily, fontWeight, showTicks, flexTicks,
		showTickLabel,
	} = props;
	const {
		ticks: tickArguments, tickValues: tickValuesProp,
		tickStroke, tickStrokeOpacity, tickInterval
	} = props;

	// if (tickArguments) tickArguments = [tickArguments];

	let tickValues;
	if (isDefined(tickValuesProp)) {
		tickValues = tickValuesProp;
	} else if (isDefined(tickInterval)) {
		const [min, max] = scale.domain();
		tickValues = d3Range(min, max, (max - min) / tickInterval);
	} else if (isDefined(scale.ticks)) {
		tickValues = scale.ticks(tickArguments, flexTicks);
	} else {
		tickValues = scale.domain();
	}

	const baseFormat = scale.tickFormat
		? scale.tickFormat(tickArguments)
		: identity;

	const format = isNotDefined(tickFormat)
		? baseFormat
		: d => tickFormat(d) || "";

	const sign = orient === "top" || orient === "left" ? -1 : 1;
	const tickSpacing = Math.max(innerTickSize, 0) + tickPadding;

	let ticks, dy, canvas_dy, textAnchor;

	if (orient === "bottom" || orient === "top") {
		dy = sign < 0 ? "0em" : ".71em";
		canvas_dy = sign < 0 ? 0 : (fontSize * .71);
		textAnchor = "middle";

		ticks = tickValues.map(d => {
			const x = Math.round(scale(d));
			return {
				value: d,
				x1: x,
				y1: 0,
				x2: x,
				y2: sign * innerTickSize,
				labelX: x,
				labelY: sign * tickSpacing,
			};
		});

		if (showTicks && flexTicks) {
			// console.log(ticks, showTicks);

			const nodes = ticks.map(d => ({ id: d.value, value: d.value, fy: d.y2, origX: d.x1 }));

			const simulation = forceSimulation(nodes)
				.force("x", forceX(d => d.origX).strength(1))
				.force("collide", forceCollide(22))
				// .force("center", forceCenter())
				.stop();

			for (let i = 0; i < 100; ++i) simulation.tick();
			// console.log(nodes);

			const zip = zipper()
				.combine((a, b) => {
					if (Math.abs(b.x - b.origX) > 0.01) {
						return {
							...a,
							x2: b.x,
							labelX: b.x
						};
					}
					return a;
				});

			ticks = zip(ticks, nodes);
		}

	} else {
		ticks = tickValues.map(d => {
			const y = Math.round(scale(d));
			return {
				value: d,
				x1: 0,
				y1: y,
				x2: sign * innerTickSize,
				y2: y,
				labelX: sign * tickSpacing,
				labelY: y,
			};
		});

		dy = ".32em";
		canvas_dy = (fontSize * .32);
		textAnchor = sign < 0 ? "end" : "start";
	}

	return {
		ticks, scale, tickStroke,
		tickLabelFill: (tickLabelFill || tickStroke),
		tickStrokeOpacity,
		tickStrokeWidth,
		tickStrokeDasharray,
		dy,
		canvas_dy,
		textAnchor,
		fontSize,
		fontFamily,
		fontWeight,
		format,
		showTickLabel,
	};
}

/* eslint-disable react/prop-types */
function axisLineSVG(props, range) {
	const { orient, outerTickSize } = props;
	const { domainClassName, fill, stroke, strokeWidth, opacity } = props;

	const sign = orient === "top" || orient === "left" ? -1 : 1;

	let d;

	if (orient === "bottom" || orient === "top") {
		d = "M" + range[0] + "," + sign * outerTickSize + "V0H" + range[1] + "V" + sign * outerTickSize;
	} else {
		d = "M" + sign * outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + sign * outerTickSize;
	}

	return (
		<path
			className={domainClassName}
			d={d}
			fill={fill}
			opacity={opacity}
			stroke={stroke}
			strokeWidth={strokeWidth} >
		</path>
	);
}
/* eslint-enable react/prop-types */

function drawAxisLine(ctx, props, range) {
	// props = { ...AxisLine.defaultProps, ...props };

	const { orient, outerTickSize, stroke, strokeWidth, opacity } = props;

	const sign = orient === "top" || orient === "left" ? -1 : 1;
	const xAxis = (orient === "bottom" || orient === "top");

	// var range = d3_scaleRange(xAxis ? xScale : yScale);

	ctx.lineWidth = strokeWidth;
	ctx.strokeStyle = hexToRGBA(stroke, opacity);

	ctx.beginPath();

	if (xAxis) {
		ctx.moveTo(first(range), sign * outerTickSize);
		ctx.lineTo(first(range), 0);
		ctx.lineTo(last(range), 0);
		ctx.lineTo(last(range), sign * outerTickSize);
	} else {
		ctx.moveTo(sign * outerTickSize, first(range));
		ctx.lineTo(0, first(range));
		ctx.lineTo(0, last(range));
		ctx.lineTo(sign * outerTickSize, last(range));
	}
	ctx.stroke();
}

function Tick(props) {
	const { tickLabelFill, tickStroke, tickStrokeOpacity, tickStrokeDasharray, tickStrokeWidth, textAnchor, fontSize, fontFamily, fontWeight } = props;
	const { x1, y1, x2, y2, labelX, labelY, dy } = props;
	return (
		<g className="tick">
			<line
				shapeRendering="crispEdges"
				opacity={tickStrokeOpacity}
				stroke={tickStroke}
				strokeWidth={tickStrokeWidth}
				strokeDasharray={getStrokeDasharray(tickStrokeDasharray)}
				x1={x1} y1={y1}
				x2={x2} y2={y2} />
			<text
				dy={dy} x={labelX} y={labelY}
				fill={tickLabelFill}
				fontSize={fontSize}
				fontWeight={fontWeight}
				fontFamily={fontFamily}
				textAnchor={textAnchor}>
				{props.children}
			</text>
		</g>
	);
}

Tick.propTypes = {
	children: PropTypes.string.isRequired,
	x1: PropTypes.number.isRequired,
	y1: PropTypes.number.isRequired,
	x2: PropTypes.number.isRequired,
	y2: PropTypes.number.isRequired,
	labelX: PropTypes.number.isRequired,
	labelY: PropTypes.number.isRequired,
	dy: PropTypes.string.isRequired,
	tickStroke: PropTypes.string,
	tickLabelFill: PropTypes.string,
	tickStrokeWidth: PropTypes.number,
	tickStrokeOpacity: PropTypes.number,
	tickStrokeDasharray: PropTypes.oneOf(strokeDashTypes),
	textAnchor: PropTypes.string,
	fontSize: PropTypes.number,
	fontFamily: PropTypes.string,
	fontWeight: PropTypes.oneOfType([
	  PropTypes.string,
	  PropTypes.number
	]),
};

function axisTicksSVG(props, scale) {
	const result = tickHelper(props, scale);

	const { tickLabelFill, tickStroke, tickStrokeOpacity, tickStrokeWidth, tickStrokeDasharray, textAnchor } = result;
	const { fontSize, fontFamily, fontWeight, ticks, format } = result;

	const { dy } = result;

	return (
		<g>
			{ticks.map((tick, idx) => {
				return (
					<Tick key={idx}
						tickStroke={tickStroke}
						tickLabelFill={tickLabelFill}
						tickStrokeWidth={tickStrokeWidth}
						tickStrokeOpacity={tickStrokeOpacity}
						tickStrokeDasharray={tickStrokeDasharray}
						dy={dy}
						x1={tick.x1} y1={tick.y1}
						x2={tick.x2} y2={tick.y2}
						labelX={tick.labelX} labelY={tick.labelY}
						textAnchor={textAnchor}
						fontSize={fontSize}
						fontWeight={fontWeight}
						fontFamily={fontFamily}>{format(tick.value)}</Tick>
				);
			})}
		</g>
	);
}

function drawTicks(ctx, result) {

	const { tickStroke, tickStrokeOpacity, tickLabelFill } = result;
	const { textAnchor, fontSize, fontFamily, fontWeight, ticks, showTickLabel } = result;

	ctx.strokeStyle = hexToRGBA(tickStroke, tickStrokeOpacity);

	ctx.fillStyle = tickStroke;
	// ctx.textBaseline = 'middle';

	ticks.forEach((tick) => {
		drawEachTick(ctx, tick, result);
	});

	ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
	ctx.fillStyle = tickLabelFill;
	ctx.textAlign = textAnchor === "middle" ? "center" : textAnchor;

	if (showTickLabel) {
		ticks.forEach((tick) => {
			drawEachTickLabel(ctx, tick, result);
		});
	}
}

function drawEachTick(ctx, tick, result) {
	const { tickStrokeWidth, tickStrokeDasharray } = result;

	ctx.beginPath();

	ctx.moveTo(tick.x1, tick.y1);
	ctx.lineTo(tick.x2, tick.y2);
	ctx.lineWidth = tickStrokeWidth;
	ctx.setLineDash(getStrokeDasharray(tickStrokeDasharray).split(","));
	ctx.stroke();
}

function drawEachTickLabel(ctx, tick, result) {
	const { canvas_dy, format } = result;

	ctx.beginPath();
	ctx.fillText(format(tick.value), tick.labelX, tick.labelY + canvas_dy);
}

export default Axis;
<commit_msg>Update Axis.js<commit_after>"use strict";

import React, { Component } from "react";
import PropTypes from "prop-types";
import { forceSimulation, forceX, forceCollide } from "d3-force";
import { range as d3Range } from "d3-array";

import GenericChartComponent from "../GenericChartComponent";
import { getAxisCanvas } from "../GenericComponent";
import AxisZoomCapture from "./AxisZoomCapture";

import { first, last, hexToRGBA, isNotDefined, isDefined, identity, zipper, strokeDashTypes, getStrokeDasharray } from "../utils";

class Axis extends Component {
	constructor(props) {		super(props);
		this.renderSVG = this.renderSVG.bind(this);
		this.drawOnCanvas = this.drawOnCanvas.bind(this);
		this.saveNode = this.saveNode.bind(this);
		this.getMoreProps = this.getMoreProps.bind(this);
	}
	saveNode(node) {
		this.node = node;
	}
	getMoreProps() {
		return this.node.getMoreProps();
	}
	drawOnCanvas(ctx, moreProps) {
		const { showDomain, showTicks, transform, range, getScale } = this.props;

		ctx.save();
		ctx.translate(transform[0], transform[1]);

		if (showDomain) drawAxisLine(ctx, this.props, range);
		if (showTicks) {
			const tickProps = tickHelper(this.props, getScale(moreProps));
			drawTicks(ctx, tickProps);
		}

		ctx.restore();
	}
	renderSVG(moreProps) {
		const { className } = this.props;
		const { showDomain, showTicks, range, getScale } = this.props;

		const ticks = showTicks ? axisTicksSVG(this.props, getScale(moreProps)) : null;
		const domain = showDomain ? axisLineSVG(this.props, range) : null;

		return <g className={className}>
			{ticks}
			{domain}
		</g>;
	}
	render() {
		const { bg, axisZoomCallback, zoomCursorClassName, zoomEnabled, getScale, inverted } = this.props;
		const { transform, getMouseDelta, edgeClip } = this.props;
		const { onContextMenu, onDoubleClick } = this.props;

		const zoomCapture = zoomEnabled
			? <AxisZoomCapture
				bg={bg}
				getScale={getScale}
				getMoreProps={this.getMoreProps}
				getMouseDelta={getMouseDelta}
				axisZoomCallback={axisZoomCallback}
				zoomCursorClassName={zoomCursorClassName}
				inverted={inverted}
				onContextMenu={onContextMenu}
				onDoubleClick={onDoubleClick}
			/>
			: null;

		return <g transform={`translate(${ transform[0] }, ${ transform[1] })`}>
			{zoomCapture}
			<GenericChartComponent ref={this.saveNode}
				canvasToDraw={getAxisCanvas}
				clip={false}
				edgeClip={edgeClip}
				svgDraw={this.renderSVG}
				canvasDraw={this.drawOnCanvas}
				drawOn={["pan"]}
			/>
		</g>;
	}
}

Axis.propTypes = {
	innerTickSize: PropTypes.number,
	outerTickSize: PropTypes.number,
	tickFormat: PropTypes.func,
	tickPadding: PropTypes.number,
	tickSize: PropTypes.number,
	ticks: PropTypes.number,
	tickLabelFill: PropTypes.string,
	tickStroke: PropTypes.string,
	tickStrokeOpacity: PropTypes.number,
	tickStrokeWidth: PropTypes.number,
	tickStrokeDasharray: PropTypes.oneOf(strokeDashTypes),
	tickValues: PropTypes.array,
	tickInterval: PropTypes.number,
  tickIntervalFunction: PropTypes.func,
	showDomain: PropTypes.bool,
	showTicks: PropTypes.bool,
	className: PropTypes.string,
	axisZoomCallback: PropTypes.func,
	zoomEnabled: PropTypes.bool,
	inverted: PropTypes.bool,
	zoomCursorClassName: PropTypes.string,
	transform: PropTypes.arrayOf(PropTypes.number).isRequired,
	range: PropTypes.arrayOf(PropTypes.number).isRequired,
	getMouseDelta: PropTypes.func.isRequired,
	getScale: PropTypes.func.isRequired,
	bg: PropTypes.object.isRequired,
	edgeClip: PropTypes.bool.isRequired,
	onContextMenu: PropTypes.func,
	onDoubleClick: PropTypes.func,
};

Axis.defaultProps = {
	zoomEnabled: false,
	zoomCursorClassName: "",
	edgeClip: false,
};

function tickHelper(props, scale) {
	const {
		orient, innerTickSize, tickFormat, tickPadding,
		tickLabelFill, tickStrokeWidth, tickStrokeDasharray,
		fontSize, fontFamily, fontWeight, showTicks, flexTicks,
		showTickLabel
	} = props;
	const {
		ticks: tickArguments, tickValues: tickValuesProp,
		tickStroke, tickStrokeOpacity, tickInterval, tickIntervalFunction
	} = props;

	// if (tickArguments) tickArguments = [tickArguments];

	let tickValues;
	if (isDefined(tickValuesProp)) {
		tickValues = tickValuesProp;
	} else if (isDefined(tickInterval)) {
    const [min, max] = scale.domain();
    const baseTickValues = d3Range(min, max, (max - min) / tickInterval);

    tickValues = tickIntervalFunction
      ? tickIntervalFunction(min, max, tickInterval)
      : baseTickValues
	} else if (isDefined(scale.ticks)) {
		tickValues = scale.ticks(tickArguments, flexTicks);
	} else {
		tickValues = scale.domain();
	}

	const baseFormat = scale.tickFormat
		? scale.tickFormat(tickArguments)
		: identity;

	const format = isNotDefined(tickFormat)
		? baseFormat
		: d => tickFormat(d) || "";

	const sign = orient === "top" || orient === "left" ? -1 : 1;
	const tickSpacing = Math.max(innerTickSize, 0) + tickPadding;

	let ticks, dy, canvas_dy, textAnchor;

	if (orient === "bottom" || orient === "top") {
		dy = sign < 0 ? "0em" : ".71em";
		canvas_dy = sign < 0 ? 0 : (fontSize * .71);
		textAnchor = "middle";

		ticks = tickValues.map(d => {
			const x = Math.round(scale(d));
			return {
				value: d,
				x1: x,
				y1: 0,
				x2: x,
				y2: sign * innerTickSize,
				labelX: x,
				labelY: sign * tickSpacing,
			};
		});

		if (showTicks && flexTicks) {
			// console.log(ticks, showTicks);

			const nodes = ticks.map(d => ({ id: d.value, value: d.value, fy: d.y2, origX: d.x1 }));

			const simulation = forceSimulation(nodes)
				.force("x", forceX(d => d.origX).strength(1))
				.force("collide", forceCollide(22))
				// .force("center", forceCenter())
				.stop();

			for (let i = 0; i < 100; ++i) simulation.tick();
			// console.log(nodes);

			const zip = zipper()
				.combine((a, b) => {
					if (Math.abs(b.x - b.origX) > 0.01) {
						return {
							...a,
							x2: b.x,
							labelX: b.x
						};
					}
					return a;
				});

			ticks = zip(ticks, nodes);
		}

	} else {
		ticks = tickValues.map(d => {
			const y = Math.round(scale(d));
			return {
				value: d,
				x1: 0,
				y1: y,
				x2: sign * innerTickSize,
				y2: y,
				labelX: sign * tickSpacing,
				labelY: y,
			};
		});

		dy = ".32em";
		canvas_dy = (fontSize * .32);
		textAnchor = sign < 0 ? "end" : "start";
	}

	return {
		ticks, scale, tickStroke,
		tickLabelFill: (tickLabelFill || tickStroke),
		tickStrokeOpacity,
		tickStrokeWidth,
		tickStrokeDasharray,
		dy,
		canvas_dy,
		textAnchor,
		fontSize,
		fontFamily,
		fontWeight,
		format,
		showTickLabel,
	};
}

/* eslint-disable react/prop-types */
function axisLineSVG(props, range) {
	const { orient, outerTickSize } = props;
	const { domainClassName, fill, stroke, strokeWidth, opacity } = props;

	const sign = orient === "top" || orient === "left" ? -1 : 1;

	let d;

	if (orient === "bottom" || orient === "top") {
		d = "M" + range[0] + "," + sign * outerTickSize + "V0H" + range[1] + "V" + sign * outerTickSize;
	} else {
		d = "M" + sign * outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + sign * outerTickSize;
	}

	return (
		<path
			className={domainClassName}
			d={d}
			fill={fill}
			opacity={opacity}
			stroke={stroke}
			strokeWidth={strokeWidth} >
		</path>
	);
}
/* eslint-enable react/prop-types */

function drawAxisLine(ctx, props, range) {
	// props = { ...AxisLine.defaultProps, ...props };

	const { orient, outerTickSize, stroke, strokeWidth, opacity } = props;

	const sign = orient === "top" || orient === "left" ? -1 : 1;
	const xAxis = (orient === "bottom" || orient === "top");

	// var range = d3_scaleRange(xAxis ? xScale : yScale);

	ctx.lineWidth = strokeWidth;
	ctx.strokeStyle = hexToRGBA(stroke, opacity);

	ctx.beginPath();

	if (xAxis) {
		ctx.moveTo(first(range), sign * outerTickSize);
		ctx.lineTo(first(range), 0);
		ctx.lineTo(last(range), 0);
		ctx.lineTo(last(range), sign * outerTickSize);
	} else {
		ctx.moveTo(sign * outerTickSize, first(range));
		ctx.lineTo(0, first(range));
		ctx.lineTo(0, last(range));
		ctx.lineTo(sign * outerTickSize, last(range));
	}
	ctx.stroke();
}

function Tick(props) {
	const { tickLabelFill, tickStroke, tickStrokeOpacity, tickStrokeDasharray, tickStrokeWidth, textAnchor, fontSize, fontFamily, fontWeight } = props;
	const { x1, y1, x2, y2, labelX, labelY, dy } = props;
	return (
		<g className="tick">
			<line
				shapeRendering="crispEdges"
				opacity={tickStrokeOpacity}
				stroke={tickStroke}
				strokeWidth={tickStrokeWidth}
				strokeDasharray={getStrokeDasharray(tickStrokeDasharray)}
				x1={x1} y1={y1}
				x2={x2} y2={y2} />
			<text
				dy={dy} x={labelX} y={labelY}
				fill={tickLabelFill}
				fontSize={fontSize}
				fontWeight={fontWeight}
				fontFamily={fontFamily}
				textAnchor={textAnchor}>
				{props.children}
			</text>
		</g>
	);
}

Tick.propTypes = {
	children: PropTypes.string.isRequired,
	x1: PropTypes.number.isRequired,
	y1: PropTypes.number.isRequired,
	x2: PropTypes.number.isRequired,
	y2: PropTypes.number.isRequired,
	labelX: PropTypes.number.isRequired,
	labelY: PropTypes.number.isRequired,
	dy: PropTypes.string.isRequired,
	tickStroke: PropTypes.string,
	tickLabelFill: PropTypes.string,
	tickStrokeWidth: PropTypes.number,
	tickStrokeOpacity: PropTypes.number,
	tickStrokeDasharray: PropTypes.oneOf(strokeDashTypes),
	textAnchor: PropTypes.string,
	fontSize: PropTypes.number,
	fontFamily: PropTypes.string,
	fontWeight: PropTypes.oneOfType([
	  PropTypes.string,
	  PropTypes.number
	]),
};

function axisTicksSVG(props, scale) {
	const result = tickHelper(props, scale);

	const { tickLabelFill, tickStroke, tickStrokeOpacity, tickStrokeWidth, tickStrokeDasharray, textAnchor } = result;
	const { fontSize, fontFamily, fontWeight, ticks, format } = result;

	const { dy } = result;

	return (
		<g>
			{ticks.map((tick, idx) => {
				return (
					<Tick key={idx}
						tickStroke={tickStroke}
						tickLabelFill={tickLabelFill}
						tickStrokeWidth={tickStrokeWidth}
						tickStrokeOpacity={tickStrokeOpacity}
						tickStrokeDasharray={tickStrokeDasharray}
						dy={dy}
						x1={tick.x1} y1={tick.y1}
						x2={tick.x2} y2={tick.y2}
						labelX={tick.labelX} labelY={tick.labelY}
						textAnchor={textAnchor}
						fontSize={fontSize}
						fontWeight={fontWeight}
						fontFamily={fontFamily}>{format(tick.value)}</Tick>
				);
			})}
		</g>
	);
}

function drawTicks(ctx, result) {

	const { tickStroke, tickStrokeOpacity, tickLabelFill } = result;
	const { textAnchor, fontSize, fontFamily, fontWeight, ticks, showTickLabel } = result;

	ctx.strokeStyle = hexToRGBA(tickStroke, tickStrokeOpacity);

	ctx.fillStyle = tickStroke;
	// ctx.textBaseline = 'middle';

	ticks.forEach((tick) => {
		drawEachTick(ctx, tick, result);
	});

	ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
	ctx.fillStyle = tickLabelFill;
	ctx.textAlign = textAnchor === "middle" ? "center" : textAnchor;

	if (showTickLabel) {
		ticks.forEach((tick) => {
			drawEachTickLabel(ctx, tick, result);
		});
	}
}

function drawEachTick(ctx, tick, result) {
	const { tickStrokeWidth, tickStrokeDasharray } = result;

	ctx.beginPath();

	ctx.moveTo(tick.x1, tick.y1);
	ctx.lineTo(tick.x2, tick.y2);
	ctx.lineWidth = tickStrokeWidth;
	ctx.setLineDash(getStrokeDasharray(tickStrokeDasharray).split(","));
	ctx.stroke();
}

function drawEachTickLabel(ctx, tick, result) {
	const { canvas_dy, format } = result;

	ctx.beginPath();
	ctx.fillText(format(tick.value), tick.labelX, tick.labelY + canvas_dy);
}

export default Axis;
<|endoftext|>


<commit_before>process.mixin(require('html5/constants'));

PHASES = {
	initial: require('html5/parser/initial_phase'),
	beforeHTML: require('html5/parser/before_html_phase'),
	beforeHead: require('html5/parser/before_head_phase'),
	inHead: require('html5/parser/in_head_phase'),
	afterHead: require('html5/parser/after_head_phase'),
	inBody: require('html5/parser/in_body_phase'),
	inTable: require('html5/parser/in_table_phase'),
	inCaption: require('html5/parser/in_caption_phase'),
	inColumnGroup: require('html5/parser/in_column_group_phase'),
	inTableBody: require('html5/parser/in_table_body_phase'),
	inRow: require('html5/parser/in_row_phase'),
	inCell: require('html5/parser/in_cell_phase'),
	inSelect: require('html5/parser/in_select_phase'),
	inSelectInTable: require('html5/parser/in_select_in_table_phase'),
	afterBody: require('html5/parser/after_body_phase'),
	inFrameset: require('html5/parser/in_frameset_phase'),
	afterFrameset: require('html5/parser/after_frameset_phase'),
	afterAfterBody: require('html5/parser/after_after_body_phase'),
	afterAfterFrameset: require('html5/parser/after_after_frameset_phase'),
	inForeignContent: require('html5/parser/in_foreign_content_phase')
};

TAGMODES = {
	select: PHASES.inSelect,
	td: PHASES.inCell,
	th: PHASES.inCell,
	tr: PHASES.inRow,
	tbody: PHASES.inTableBody,
	thead: PHASES.inTableBody,
	tfoot: PHASES.inTableBody,
	caption: PHASES.inCaption,
	colgroup: PHASES.inColumnGroup,
	table: PHASES.inTable,
	head: PHASES.inBody,
	body: PHASES.inBody,
	frameset: PHASES.inFrameset
};

exports.HTML5Parser = HTML5Parser = function(options) {
	this.strict = false;
	this.errors = [];
	this.tokenizer = require('html5/tokenizer').HTMLTokenizer;
	this.tree = 'FIXME: TreeBuilder';

	for(o in options) {
		this[o] = options[o];
	}

	this.tree = new this.tree;

}

HTML5Parser.prototype._parse = function(stream, inner_html, encoding, container) {
	container = container || 'div';

	this.tree.reset();
	this.first_start_tag = false;
	this.errors = [];

	// FIXME: instantiate tokenizer and plumb. Pass lowercasing options.

	if(inner_html) {
		this.inner_html = container.toLowercase();
		switch(this.inner_html) {
		case 'title':
		case 'textarea':
			this.tokenizer.content_model = HTML5Tokenizer.Models.RCDATA;
			break;
		case 'style':
		case 'script':
		case 'xmp':
		case 'iframe':
		case 'noembed':
		case 'noframes':
		case 'noscript':
			this.tokenizer.content_model = HTML5Tokenizer.Models.CDATA;
			break;
		case 'plaintext':
			this.tokenizer.content_model = HTML5Tokenizer.Models.PLAINTEXT;
			break;
		default:
			this.tokenizer.content_model = HTML5Tokenizer.Models.PCDATA;
		}
		this.phase = PHASES.beforeHTML;
		this.phase.insert_html_element();
		this.reset_insertion_mode();
	} else {
		this.inner_html = false;
		this.phase = PHASES.initial;
	}

	this.last_phase = null;

	tokenizer.addListener('token', function(token) {
		token = normalize_token(token);
		method = 'process' + token.type;

		switch(token.type) {
		case Tokens.CHARACTERS:
		case Tokens.SPACE:
		case Tokens.COMMENT:
			this.phase[method](token.data);
			break;
		case Tokens.START_TAG:
			this.phase[method](token.name, token.data, token.self_closing);
			break;
		case Tokens.END_TAG:
			this.phase[method](token.name);
			break;
		case Tokens.DOCTYPE:
			this.phase[method](token.name, token.publicId, token.systemId, token.correct);
			break;
		default:
			this.parse_error(token.data, token.datavars)
		}
	});

	tokenizer.addListener('eof', function() {
		this.process_eof();
	});
}

HTML5Parser.prototype.parse_error = function(code, data) {
	this.errors.push([this.tokenizer.position, code, data]);
	if(this.strict) throw(this.errors[this.errors.length]);
}

HTML5Parser.prototype.normalize_token = function(token) {
	if(token.type == Tokens.EMPTY_TAG) {
		if(VOID_ELEMENTS.indexOf(token.name) == -1) {
			parse_error('incorrectly-placed-solidus');
		}
		token.type = Tokens.START_TAG;
	}

	if(token.type == Tokens.START_TAG) {
		token.name = token.name.toLowercase();
		if(token.data.length != 0) {
			var data = {};
			token.data.reverse().forEach(function(e) {
				data[e[0].toLowercase()] = e[1];
			});
			token.data = data;
		}
	} else if(token.type = Tokens.END_TAG) {
		if(token.data.length != 0) parse_error('attributes-in-end-tag');
		token.name = token.name.toLowercase();
	}

	return token;
}
<commit_msg>- add _ and reset_insertion_mode functions<commit_after>process.mixin(require('html5/constants'));

PHASES = {
	initial: require('html5/parser/initial_phase'),
	beforeHTML: require('html5/parser/before_html_phase'),
	beforeHead: require('html5/parser/before_head_phase'),
	inHead: require('html5/parser/in_head_phase'),
	afterHead: require('html5/parser/after_head_phase'),
	inBody: require('html5/parser/in_body_phase'),
	inTable: require('html5/parser/in_table_phase'),
	inCaption: require('html5/parser/in_caption_phase'),
	inColumnGroup: require('html5/parser/in_column_group_phase'),
	inTableBody: require('html5/parser/in_table_body_phase'),
	inRow: require('html5/parser/in_row_phase'),
	inCell: require('html5/parser/in_cell_phase'),
	inSelect: require('html5/parser/in_select_phase'),
	inSelectInTable: require('html5/parser/in_select_in_table_phase'),
	afterBody: require('html5/parser/after_body_phase'),
	inFrameset: require('html5/parser/in_frameset_phase'),
	afterFrameset: require('html5/parser/after_frameset_phase'),
	afterAfterBody: require('html5/parser/after_after_body_phase'),
	afterAfterFrameset: require('html5/parser/after_after_frameset_phase'),
	inForeignContent: require('html5/parser/in_foreign_content_phase')
};

TAGMODES = {
	select: PHASES.inSelect,
	td: PHASES.inCell,
	th: PHASES.inCell,
	tr: PHASES.inRow,
	tbody: PHASES.inTableBody,
	thead: PHASES.inTableBody,
	tfoot: PHASES.inTableBody,
	caption: PHASES.inCaption,
	colgroup: PHASES.inColumnGroup,
	table: PHASES.inTable,
	head: PHASES.inBody,
	body: PHASES.inBody,
	frameset: PHASES.inFrameset
};

exports.HTML5Parser = HTML5Parser = function(options) {
	this.strict = false;
	this.errors = [];
	this.tokenizer = require('html5/tokenizer').HTMLTokenizer;
	this.tree = 'FIXME: TreeBuilder';

	for(o in options) {
		this[o] = options[o];
	}

	this.tree = new this.tree;

}

HTML5Parser.prototype._parse = function(stream, inner_html, encoding, container) {
	container = container || 'div';

	this.tree.reset();
	this.first_start_tag = false;
	this.errors = [];

	// FIXME: instantiate tokenizer and plumb. Pass lowercasing options.

	if(inner_html) {
		this.inner_html = container.toLowercase();
		switch(this.inner_html) {
		case 'title':
		case 'textarea':
			this.tokenizer.content_model = HTML5Tokenizer.Models.RCDATA;
			break;
		case 'style':
		case 'script':
		case 'xmp':
		case 'iframe':
		case 'noembed':
		case 'noframes':
		case 'noscript':
			this.tokenizer.content_model = HTML5Tokenizer.Models.CDATA;
			break;
		case 'plaintext':
			this.tokenizer.content_model = HTML5Tokenizer.Models.PLAINTEXT;
			break;
		default:
			this.tokenizer.content_model = HTML5Tokenizer.Models.PCDATA;
		}
		this.phase = PHASES.beforeHTML;
		this.phase.insert_html_element();
		this.reset_insertion_mode();
	} else {
		this.inner_html = false;
		this.phase = PHASES.initial;
	}

	this.last_phase = null;

	tokenizer.addListener('token', function(token) {
		token = normalize_token(token);
		method = 'process' + token.type;

		switch(token.type) {
		case Tokens.CHARACTERS:
		case Tokens.SPACE:
		case Tokens.COMMENT:
			this.phase[method](token.data);
			break;
		case Tokens.START_TAG:
			this.phase[method](token.name, token.data, token.self_closing);
			break;
		case Tokens.END_TAG:
			this.phase[method](token.name);
			break;
		case Tokens.DOCTYPE:
			this.phase[method](token.name, token.publicId, token.systemId, token.correct);
			break;
		default:
			this.parse_error(token.data, token.datavars)
		}
	});

	tokenizer.addListener('eof', function() {
		this.process_eof();
	});
}

HTML5Parser.prototype.parse_error = function(code, data) {
	this.errors.push([this.tokenizer.position, code, data]);
	if(this.strict) throw(this.errors[this.errors.length]);
}

HTML5Parser.prototype.normalize_token = function(token) {
	if(token.type == Tokens.EMPTY_TAG) {
		if(VOID_ELEMENTS.indexOf(token.name) == -1) {
			parse_error('incorrectly-placed-solidus');
		}
		token.type = Tokens.START_TAG;
	}

	if(token.type == Tokens.START_TAG) {
		token.name = token.name.toLowercase();
		if(token.data.length != 0) {
			var data = {};
			token.data.reverse().forEach(function(e) {
				data[e[0].toLowercase()] = e[1];
			});
			token.data = data;
		}
	} else if(token.type = Tokens.END_TAG) {
		if(token.data.length != 0) parse_error('attributes-in-end-tag');
		token.name = token.name.toLowercase();
	}

	return token;
}

HTML5Parser.prototype.reset_insert_mode = function() {
	var last = false;
	for(node in this.tee.open_elements.reverse()) {
		var node_name = node.name;
		if(node == this.tree.open_elements[0]) {
			last = true;
			if(node_name == 'th' || node_name == 'td') {
				// XXX: assert this.inner_html
				node_name = this.inner_html;
			}
		}

		if(node_name == 'select' || node_name =='colgroup' || node_name == 'head' || node_name == 'frameset') {
			// XXX: assert this.inner_html
		}

		if(TAGMODES[node_name]) {
			this.phase = TAGMODES[node_name];
		} else if(node_name == 'html') {
			this.phase = PHASES[(this.tree.head_pointer == null ? 'beforeHead' : 'afterHead')];
		} else if(last) {
			this.phase = PHASES.inBody;
		} else {
			continue;
		}

		break;
	}
}

HTML5Parser.prototype._ = function(str) {
	return(str);
}
<|endoftext|>


<commit_before>var timeago = require('../../');

module.exports = function(t) {
  var tm = timeago('2016-06-20 12:30:00', 'be');
  tm.register('be', require('../../locales/be'));

  // test second
  t.equal(tm.format('2016-06-20 12:30:00'), 'толькі што');
  t.equal(tm.format('2016-06-20 12:29:50'), '10 секунд таму');
  t.equal(tm.format('2016-06-20 12:29:39'), '21 секунду таму');
  t.equal(tm.format('2016-06-20 12:29:38'), '22 секунды таму');
  t.equal(tm.format('2016-06-20 12:30:01'), 'праз некалькі секунд');
  t.equal(tm.format('2016-06-20 12:30:10'), 'праз 10 секунд');
  t.equal(tm.format('2016-06-20 12:30:21'), 'праз 21 секунду');
  t.equal(tm.format('2016-06-20 12:30:22'), 'праз 22 секунды');

  // test minute
  t.equal(tm.format('2016-06-20 12:29:00'), 'хвіліну таму');
  t.equal(tm.format('2016-06-20 12:28:00'), '2 хвіліны таму');
  t.equal(tm.format('2016-06-20 12:25:00'), '5 хвілін таму');
  t.equal(tm.format('2016-06-20 12:9:00'), '21 хвіліну таму');
  t.equal(tm.format('2016-06-20 12:31:00'), 'праз хвіліну');
  t.equal(tm.format('2016-06-20 12:32:00'), 'праз 2 хвіліны');
  t.equal(tm.format('2016-06-20 12:35:00'), 'праз 5 хвілін');
  t.equal(tm.format('2016-06-20 12:51:00'), 'праз 21 хвіліну');

  // test hour
  t.equal(tm.format('2016-06-20 11:30:00'), 'гадзіну таму');
  t.equal(tm.format('2016-06-20 10:30:00'), '2 гадзіны таму');
  t.equal(tm.format('2016-06-20 7:30:00'), '5 гадзін таму');
  t.equal(tm.format('2016-06-19 15:30:00'), '21 гадзіну таму');
  t.equal(tm.format('2016-06-20 13:30:00'), 'праз гадзіну');
  t.equal(tm.format('2016-06-20 14:30:00'), 'праз 2 гадзіны');
  t.equal(tm.format('2016-06-20 17:30:00'), 'праз 5 гадзін');
  t.equal(tm.format('2016-06-21 9:30:00'), 'праз 21 гадзіну');

  // test day
  t.equal(tm.format('2016-06-19 12:30:00'), 'дзень таму');
  t.equal(tm.format('2016-06-18 12:30:00'), '2 дні таму');
  t.equal(tm.format('2016-06-15 12:30:00'), '5 дзён таму');
  t.equal(tm.format('2016-06-21 12:30:00'), 'праз дзень');
  t.equal(tm.format('2016-06-22 12:30:00'), 'праз 2 дні');
  t.equal(tm.format('2016-06-25 12:30:00'), 'праз 5 дзён');

  // test week
  t.equal(tm.format('2016-06-13 12:30:00'), 'тыдзень таму');
  t.equal(tm.format('2016-06-06 12:30:00'), '2 тыдні таму');
  t.equal(tm.format('2016-06-27 12:30:00'), 'праз тыдзень');
  t.equal(tm.format('2016-07-04 12:30:00'), 'праз 2 тыдні');

  // test month
  t.equal(tm.format('2016-05-20 12:30:00'), 'месяц таму');
  t.equal(tm.format('2016-04-20 12:30:00'), '2 месяцы таму');
  t.equal(tm.format('2016-01-19 12:30:00'), '5 месяцаў таму');
  t.equal(tm.format('2016-07-21 12:30:00'), 'праз месяц');
  t.equal(tm.format('2016-08-20 12:30:00'), 'праз 2 месяцы');
  t.equal(tm.format('2016-11-20 12:30:00'), 'праз 5 месяцаў');

  // test year
  t.equal(tm.format('2015-06-20 12:30:00'), 'год таму');
  t.equal(tm.format('2014-06-20 12:30:00'), '2 гады таму');
  t.equal(tm.format('2011-06-20 12:30:00'), '5 гадоў таму');
  t.equal(tm.format('1995-06-20 12:30:00'), '21 год таму');
  t.equal(tm.format('2017-06-20 12:30:00'), 'праз год');
  t.equal(tm.format('2018-06-20 12:30:00'), 'праз 2 гады');
  t.equal(tm.format('2021-06-20 12:30:00'), 'праз 5 гадоў');
  t.equal(tm.format('2037-06-20 12:30:00'), 'праз 21 год');
};
<commit_msg>update be tests (#51)<commit_after>module.exports = function(t, tb) {
  // test second
  t.equal(tb.subSeconds(1), 'толькі што');
  t.equal(tb.subSeconds(10), '10 секунд таму');
  t.equal(tb.subSeconds(21), '21 секунду таму');
  t.equal(tb.subSeconds(22), '22 секунды таму');
  t.equal(tb.addSeconds(1), 'праз некалькі секунд');
  t.equal(tb.addSeconds(10), 'праз 10 секунд');
  t.equal(tb.addSeconds(21), 'праз 21 секунду');
  t.equal(tb.addSeconds(22), 'праз 22 секунды');

  // test minute
  t.equal(tb.subMinutes(1), 'хвіліну таму');
  t.equal(tb.subMinutes(2), '2 хвіліны таму');
  t.equal(tb.subMinutes(5), '5 хвілін таму');
  t.equal(tb.subMinutes(21), '21 хвіліну таму');
  t.equal(tb.addMinutes(1), 'праз хвіліну');
  t.equal(tb.addMinutes(2), 'праз 2 хвіліны');
  t.equal(tb.addMinutes(5), 'праз 5 хвілін');
  t.equal(tb.addMinutes(21), 'праз 21 хвіліну');

  // test hour
  t.equal(tb.subHours(1), 'гадзіну таму');
  t.equal(tb.subHours(2), '2 гадзіны таму');
  t.equal(tb.subHours(5), '5 гадзін таму');
  t.equal(tb.subHours(21), '21 гадзіну таму');
  t.equal(tb.addHours(1), 'праз гадзіну');
  t.equal(tb.addHours(2), 'праз 2 гадзіны');
  t.equal(tb.addHours(5), 'праз 5 гадзін');
  t.equal(tb.addHours(21), 'праз 21 гадзіну');

  // test day
  t.equal(tb.subDays(1), 'дзень таму');
  t.equal(tb.subDays(2), '2 дні таму');
  t.equal(tb.subDays(5), '5 дзён таму');
  t.equal(tb.addDays(1), 'праз дзень');
  t.equal(tb.addDays(2), 'праз 2 дні');
  t.equal(tb.addDays(5), 'праз 5 дзён');

  // test week
  t.equal(tb.subWeeks(1), 'тыдзень таму');
  t.equal(tb.subWeeks(2), '2 тыдні таму');
  t.equal(tb.addWeeks(1), 'праз тыдзень');
  t.equal(tb.addWeeks(2), 'праз 2 тыдні');

  // test month
  t.equal(tb.subMonths(1), 'месяц таму');
  t.equal(tb.subMonths(2), '2 месяцы таму');
  t.equal(tb.subMonths(5), '5 месяцаў таму');
  t.equal(tb.addMonths(1), 'праз месяц');
  t.equal(tb.addMonths(2), 'праз 2 месяцы');
  t.equal(tb.addMonths(5), 'праз 5 месяцаў');

  // test year
  t.equal(tb.subYears(1), 'год таму');
  t.equal(tb.subYears(2), '2 гады таму');
  t.equal(tb.subYears(5), '5 гадоў таму');
  t.equal(tb.subYears(21), '21 год таму');
  t.equal(tb.addYears(1), 'праз год');
  t.equal(tb.addYears(2), 'праз 2 гады');
  t.equal(tb.addYears(5), 'праз 5 гадоў');
  t.equal(tb.addYears(21), 'праз 21 год');
};
<|endoftext|>


<commit_before>/*
 * moleculer
 * Copyright (c) 2019 MoleculerJS (https://github.com/moleculerjs/moleculer)
 * MIT Licensed
 */

"use strict";

/**
 * Node class
 *
 * @class Node
 */
class Node {
	/**
	 * Creates an instance of Node.
	 *
	 * @param {String} id
	 *
	 * @memberof Node
	 */
	constructor(id) {
		this.id = id;
		this.instanceID = null;
		this.available = true;
		this.local = false;
		this.lastHeartbeatTime = Date.now();
		this.config = {};
		this.client = {};
		this.metadata = null;

		this.ipList = null;
		this.port = null;
		this.hostname = null;
		this.udpAddress = null;

		this.rawInfo = null;
		this.services = [];

		this.cpu = null;
		this.cpuSeq = null;

		this.seq = 0;
		this.offlineSince = null;
	}

	/**
	 * Update properties
	 *
	 * @param {object} payload
	 * @param {boolean} isReconnected
	 * @memberof Node
	 */
	update(payload, isReconnected) {
		// Update properties
		this.metadata = payload.metadata;
		this.ipList = payload.ipList;
		this.hostname = payload.hostname;
		this.port = payload.port;
		this.client = payload.client || {};
		this.instanceID = payload.instanceID;

		// Process services & events
		this.services = payload.services;
		this.rawInfo = payload;

		const newSeq = payload.seq || 1;
		if (newSeq > this.seq || isReconnected) {
			this.seq = newSeq;
			return true;
		}
	}

	/**
	 * Update local properties
	 *
	 * @memberof Node
	 */
	updateLocalInfo(cpuUsage) {
		return cpuUsage().then(res => {
			const newVal = Math.round(res.avg);
			if (this.cpu != newVal) {
				this.cpu = Math.round(res.avg);
				this.cpuSeq++;
			}
		}).catch(() => { /* silent */ });
	}

	/**
	 * Update heartbeat properties
	 *
	 * @param {any} payload
	 * @memberof Node
	 */
	heartbeat(payload) {
		if (!this.available) {
			this.available = true;
			this.offlineSince = null;
		}

		this.cpu = payload.cpu;
		this.cpuSeq = payload.cpuSeq || 1;

		this.lastHeartbeatTime = Date.now();
	}

	/**
	 * Node disconnected
	 *
	 * @memberof Node
	 */
	disconnected() {
		if (this.available) {
			this.offlineSince = Date.now();
			this.seq++;
		}

		this.available = false;
	}
}

module.exports = Node;
<commit_msg>add missing config property<commit_after>/*
 * moleculer
 * Copyright (c) 2019 MoleculerJS (https://github.com/moleculerjs/moleculer)
 * MIT Licensed
 */

"use strict";

/**
 * Node class
 *
 * @class Node
 */
class Node {
	/**
	 * Creates an instance of Node.
	 *
	 * @param {String} id
	 *
	 * @memberof Node
	 */
	constructor(id) {
		this.id = id;
		this.instanceID = null;
		this.available = true;
		this.local = false;
		this.lastHeartbeatTime = Date.now();
		this.config = {};
		this.client = {};
		this.metadata = null;

		this.ipList = null;
		this.port = null;
		this.hostname = null;
		this.udpAddress = null;

		this.rawInfo = null;
		this.services = [];

		this.cpu = null;
		this.cpuSeq = null;

		this.seq = 0;
		this.offlineSince = null;
	}

	/**
	 * Update properties
	 *
	 * @param {object} payload
	 * @param {boolean} isReconnected
	 * @memberof Node
	 */
	update(payload, isReconnected) {
		// Update properties
		this.metadata = payload.metadata;
		this.ipList = payload.ipList;
		this.hostname = payload.hostname;
		this.port = payload.port;
		this.client = payload.client || {};
		this.config = payload.config || {};
		this.instanceID = payload.instanceID;

		// Process services & events
		this.services = payload.services;
		this.rawInfo = payload;

		const newSeq = payload.seq || 1;
		if (newSeq > this.seq || isReconnected) {
			this.seq = newSeq;
			return true;
		}
	}

	/**
	 * Update local properties
	 *
	 * @memberof Node
	 */
	updateLocalInfo(cpuUsage) {
		return cpuUsage().then(res => {
			const newVal = Math.round(res.avg);
			if (this.cpu != newVal) {
				this.cpu = Math.round(res.avg);
				this.cpuSeq++;
			}
		}).catch(() => { /* silent */ });
	}

	/**
	 * Update heartbeat properties
	 *
	 * @param {any} payload
	 * @memberof Node
	 */
	heartbeat(payload) {
		if (!this.available) {
			this.available = true;
			this.offlineSince = null;
		}

		this.cpu = payload.cpu;
		this.cpuSeq = payload.cpuSeq || 1;

		this.lastHeartbeatTime = Date.now();
	}

	/**
	 * Node disconnected
	 *
	 * @memberof Node
	 */
	disconnected() {
		if (this.available) {
			this.offlineSince = Date.now();
			this.seq++;
		}

		this.available = false;
	}
}

module.exports = Node;
<|endoftext|>


<commit_before>define([
  'require',
  './lib/widget',
  './lib/store',
  './lib/sync',
  './lib/wireClient',
  './lib/nodeConnect',
  './lib/util',
  './lib/webfinger',
  './lib/foreignClient',
  './lib/baseClient',
  './lib/schedule',
  './lib/i18n'
], function(require, widget, store, sync, wireClient, nodeConnect, util, webfinger, foreignClient, BaseClient, schedule, i18n) {

  "use strict";

  var claimedModules = {}, modules = {}, moduleNameRE = /^[a-z\-]+$/;

  var logger = util.getLogger('base');

  util.silenceAllLoggers();
  util.unsilenceLogger('base', 'getputdelete');

  // Namespace: remoteStorage
  //
  // Main remoteStorage object, primary namespace.
  //
  // Provides an interface for defining modules, controlling the widget and configuring
  // access.
  //
  // Most methods here return promises.  See the guide for and introduction: <Promises>
  var remoteStorage =  {

    //
    // Method: defineModule
    //
    // Define a new module, with given name.
    // Module names MUST be unique. The given builder will be called
    // immediately, with two arguments, which are both instances of
    // <BaseClient>. The first accesses the private section of a modules
    // storage space, the second the public one. The public area can
    // be read by any client (not just an authenticated one), while
    // it can only be written by an authenticated client with read-write
    // access claimed on it.
    //
    // The builder is expected to return an object, as described under
    // <getModuleInfo>.
    //
    // Parameter:
    //   moduleName - Name of the module to define. MUST be a-z and all lowercase.
    //   builder    - Builder function that holds the module definition.
    //
    // Example:
    //   (start code)
    //
    //   remoteStorage.defineModule('beers', function(privateClient, publicClient) {
    //
    //     function nameToKey(name) {
    //       return name.replace(/\s/, '-');
    //     }
    //
    //     return {
    //       exports: {
    //
    //         addBeer: function(name) {
    //           return privateClient.storeObject('beer', nameToKey(name), {
    //             name: name,
    //             drinkCount: 0
    //           });
    //         },
    //
    //         logDrink: function(name) {
    //           var key = nameToKey(name);
    //           return privateClient.getObject(key).then(function(beer) {
    //             beer.drinkCount++;
    //             return privateClient.storeObject('beer', key, beer);
    //           });
    //         },
    //
    //         publishBeer: function(name) {
    //           var key = nameToKey(name);
    //           return privateClient.getObject(key).then(function(beer) {
    //             return publicClient.storeObject('beer', key, beer);
    //           });
    //         }
    //
    //       }
    //     }
    //   });
    //
    //   // to use that code from an app, you need to add:
    //
    //   remoteStorage.claimAccess('beers', 'rw').then(function() {
    //     remoteStorage.displayWidget()
    //
    //     remoteStorage.beers.addBeer('<replace-with-favourite-beer-kind>');
    //   });
    //
    //   (end code)
    //
    // See also:
    //   <BaseClient>
    //
    defineModule: function(moduleName, builder) {

      if(! moduleNameRE.test(moduleName)) {
        throw 'Invalid moduleName: "'+moduleName+'", only a-z lowercase allowed.';
      }

      var module = builder(
        // private client:
        new BaseClient(moduleName, false),
        // public client:
        new BaseClient(moduleName, true)
      );
      modules[moduleName] = module;
      if(typeof(module) !== 'object' || typeof(module.exports) !== 'object') {
        throw new Error("Invalid module format for module '" + moduleName + "', no 'exports' object returned!");
      }
      this[moduleName] = module.exports;
    },

    claimedModules: claimedModules,

    //
    // Method: getModuleList
    //
    // list known module names
    //
    // Returns:
    //   Array of module names.
    //
    getModuleList: function() {
      return Object.keys(modules);
    },

    // Method: getClaimedModuleList
    //
    // list of modules currently claimed access on
    //
    // Returns:
    //   Array of module names.
    //
    getClaimedModuleList: function() {
      return Object.keys(claimedModules);
    },

    //
    // Method: getModuleInfo
    //
    // Retrieve meta-information about a given module.
    //
    // If the module doesn't exist, the result will be undefined.
    //
    // Parameters:
    //   moduleName - name of the module
    //
    // Returns:
    //   An object, usually containing the following keys,
    //   * exports - don't ever use this. it's basically the module's instance.
    //   * name - the name of the module, but you knew that already.
    //   * dataHints - an object, describing internas about the module.
    //
    //
    //   Some of the dataHints used are:
    //
    //     objectType <type> - description of an object
    //                         type implemented by the module
    //     "objectType message" - (example)
    //
    //     <attributeType> <objectType>#<attribute> - description of an attribute
    //
    //     "string message#subject" - (example)
    //
    //     directory <path> - description of a path's purpose
    //
    //     "directory documents/notes/" - (example)
    //
    //     item <path> - description of a specific item
    //
    //     "item documents/notes/calendar" - (example)
    //
    //   Hope this helps.
    //
    getModuleInfo: function(moduleName) {
      var moduleInfo = modules[moduleName];
      if(moduleInfo) {
        return util.extend({
          name: moduleName,
          dataHints: {}
        }, moduleInfo);
      }
    },

    //
    // Method: claimAccess
    //
    // Either:
    //   <claimAccess(moduleName, claim)>
    //
    // Or:
    //   <claimAccess(moduleClaimMap)>
    //
    //
    //
    // Method: claimAccess(moduleName, claim)
    //
    // Claim access on a single module
    //
    // You need to claim access to a module before you can
    // access data from it.
    //
    // Parameters:
    //   moduleName - name of the module. For a list of defined modules, use <getModuleList>
    //   claim      - permission to claim, either *r* (read-only) or *rw* (read-write)
    //
    // Example:
    //   > remoteStorage.claimAccess('contacts', 'r').then(remoteStorage.displayWidget).then(initApp);
    //
    // Returns:
    //   a Promise, fulfilled when the access has been claimed.
    //
    // FIXME: this method is currently asynchronous due to internal design issues, but it doesn't need to be.
    //
    // Method: claimAccess(moduleClaimMap)
    //
    // Claim access to multiple modules.
    //
    // Parameters:
    //   moduleClaimMap - a JSON object with module names as keys and claims as values.
    //
    // Example:
    //   > remoteStorage.claimAccess({
    //   >   contacts: 'r',
    //   >   documents: 'rw',
    //   >   money: 'r'
    //   > }).then(remoteStorage.displayWidget).then(initApp);
    //
    // Returns:
    //   a Promise, fulfilled when the access has been claimed.
    //
    // FIXME: this method is currently asynchronous due to internal design issues, but it doesn't need to be.
    //
    claimAccess: function(moduleName, mode) {

      var modeTestRegex = /^rw?$/;
      function testMode(moduleName, mode) {
        if(!modeTestRegex.test(mode)) {
          throw "Claimed access to module '" + moduleName + "' but mode not correctly specified ('" + mode + "').";
        }
      }

      var moduleObj;
      if(typeof moduleName === 'object') {
        moduleObj = moduleName;
      } else {
        testMode(moduleName, mode);
        moduleObj = {};
        moduleObj[moduleName] = mode;
      }

      return util.asyncEach(Object.keys(moduleObj), function(_moduleName) {
        var _mode = moduleObj[_moduleName];
        testMode(_moduleName, _mode);
        return this.claimModuleAccess(_moduleName, _mode);
      }.bind(this));
    },

    // PRIVATE
    claimModuleAccess: function(moduleName, mode) {
      logger.debug('claimModuleAccess', moduleName, mode);
      if(!(moduleName in modules)) {
        throw "Module not defined: " + moduleName;
      }

      if(moduleName in claimedModules) {
        return;
      }

      claimedModules[moduleName] = mode;

      if(moduleName === 'root') {
        moduleName = '';
        return store.setNodeAccess('/', mode).
          then(util.curry(store.setNodeForce, '/', true, true));
      } else {
        var privPath = '/'+moduleName+'/';
        var pubPath = '/public/'+moduleName+'/';
        return store.setNodeAccess(privPath, mode).
          then(util.curry(store.setNodeForce, privPath, true, true)).
          then(util.curry(store.setNodeAccess, pubPath, mode)).
          then(util.curry(store.setNodeForce, pubPath, true, true));
      }
    },

    // PRIVATE
    setBearerToken: function(bearerToken, claimedScopes) {
      wireClient.setBearerToken(bearerToken);
    },

    getBearerToken: function() {
      return wireClient.getBearerToken();
    },

    disconnectRemote : wireClient.disconnectRemote,

    //
    // Method: flushLocal()
    //
    // Forget this ever happened.
    //
    // Delete all locally stored data.
    // This doesn't clear localStorage, just removes everything
    // remoteStorage.js saved there. Other data your app might
    // have put into localStorage stays there.
    //
    // Call this method to implement "logout".
    //
    // If you are using the widget (which you should!), you don't need this.
    //
    // Example:
    //   > remoteStorage.flushLocal();
    //
    flushLocal       : function() {
      return util.makePromise(function(promise) {
        logger.info('flushLocal');
        store.forgetAll().then(promise.fulfill.bind(promise));
        sync.clearSettings();
        widget.clearSettings();
        schedule.reset();
        wireClient.disconnectRemote();
        i18n.clearSettings();
        claimedModules = {};
      });
    },

    //
    // Method: fullSync
    //
    // Synchronize local <-> remote storage.
    //
    // Syncing starts at the access roots (the once you claimed using claimAccess)
    // and moves down the directory tree.
    // Only nodes with a 'force' flag on themselves or one of their ancestors will
    // be synchronized. Use <BaseClient.use> and <BaseClient.release> to set / unset
    // force flags.
    // The actual changes to either local or remote storage happen in the
    // future, so you should attach change handlers on the modules you're
    // interested in.
    //
    // Parameters:
    //   callback - (optional) callback to be notified when synchronization has finished or failed.
    //
    // Example:
    //   >
    //   > remoteStorage.claimAccess('money', 'rw');
    //   >
    //   > remoteStorage.money.on('change', function(changeEvent) {
    //   >   // handle change event (update UI etc)
    //   > });
    //   >
    //   > remoteStorage.fullSync(function(errors) {
    //   >   // handle errors, if any.
    //   > });
    //
    // Yields:
    //   Array of error messages - when errors occured. When fullSync is called and the user is not connected, this is also considered an error.
    //   null - no error occured, synchronization finished gracefully.
    //
    fullSync: sync.fullSync,

    setWidgetView: widget.setView,

    //
    // Method: displayWidget
    //
    // Add the remoteStorage widget to the page.
    //
    // *Note* call <claimAccess> before calling displayWidget, otherwise you can't access any actual data.
    //
    // Parameters:
    //   element - (optional) DOM element to append the widget element, defaults to BODY element. May also be the ID of a DOM element.
    //   options - (optional) Options, as described below.
    //   options.locale - Locale to use for the widget. Currently ignored.
    //
    displayWidget: function(element, options) {
      widget.display(remoteStorage, element, util.extend({}, options));
    },

    //
    // Method: onWidget
    //
    // Add event handler to the widget.
    // See <widget.Events> for available Events.
    //
    // Parameters:
    //   eventType - type of event to add handler to
    //   handler   - handler function
    //
    onWidget: widget.on,

    //
    getSyncState: sync.getState,
    //
    setStorageInfo: wireClient.setStorageInfo,

    getStorageHref: wireClient.getStorageHref,

    getStorageType: wireClient.getStorageType,

    getStorageInfo: wireClient.getStorageInfo,

    disableSyncThrottling: sync.disableThrottling,

    nodeConnect: nodeConnect,

    util: util,

    // Method: getForeignClient
    //
    // Get a <ForeignClient> instance for a given user.
    //
    // Parameters:
    //   userAddress - a user address in the form user@host
    //   callback - a callback to receive the client
    //
    // If there is no storageInfo cached for this userAddress, this will trigger
    // a webfinger discovery and when that succeeded, return the client through
    // the callback.
    //
    // Example:
    //   (start code)
    //   var client = remoteStorage.getForeignClient('alice@wonderland.lit', function(error, client) {
    //     if(error) {
    //       console.error("Discovery failed: ", error);
    //     } else {
    //       client.getPublishedObjects(function(objects) {
    //         console.log('public stuff', objects);
    //       });
    //     }
    //   });
    //   (end code)
    getForeignClient: function(userAddress, callback) {
      var client = foreignClient.getClient(userAddress);
      if(wireClient.hasStorageInfo(userAddress)) {
        callback(null, client);
      } else {
        webfinger.getStorageInfo(
          userAddress, { timeout: 5000 }, function(err, storageInfo) {
            if(err) {
              callback(err);
            } else {
              wireClient.addStorageInfo(userAddress, storageInfo);
              callback(null, client);
            }
          }
        );
      }
    },

    getClient: function(scope) {
      return new BaseClient(scope);
    },

    // Property: store
    // Public access to <store>
    store: store,

    sync: sync,
    widget: widget,

    i18n: i18n,

    schedule: schedule,

    // for tests only.
    // FIXME: get rid of this by making internas more accessible. can probably be
    //        done with some general redesign of this file.
    _clearModules: function() {
      modules = {};
    }

  };

  return remoteStorage;
});
<commit_msg>remoteStorage doc: localStorage is not the only possible backend<commit_after>define([
  'require',
  './lib/widget',
  './lib/store',
  './lib/sync',
  './lib/wireClient',
  './lib/nodeConnect',
  './lib/util',
  './lib/webfinger',
  './lib/foreignClient',
  './lib/baseClient',
  './lib/schedule',
  './lib/i18n'
], function(require, widget, store, sync, wireClient, nodeConnect, util, webfinger, foreignClient, BaseClient, schedule, i18n) {

  "use strict";

  var claimedModules = {}, modules = {}, moduleNameRE = /^[a-z\-]+$/;

  var logger = util.getLogger('base');

  util.silenceAllLoggers();
  util.unsilenceLogger('base', 'getputdelete');

  // Namespace: remoteStorage
  //
  // Main remoteStorage object, primary namespace.
  //
  // Provides an interface for defining modules, controlling the widget and configuring
  // access.
  //
  // Most methods here return promises.  See the guide for and introduction: <Promises>
  var remoteStorage =  {

    //
    // Method: defineModule
    //
    // Define a new module, with given name.
    // Module names MUST be unique. The given builder will be called
    // immediately, with two arguments, which are both instances of
    // <BaseClient>. The first accesses the private section of a modules
    // storage space, the second the public one. The public area can
    // be read by any client (not just an authenticated one), while
    // it can only be written by an authenticated client with read-write
    // access claimed on it.
    //
    // The builder is expected to return an object, as described under
    // <getModuleInfo>.
    //
    // Parameter:
    //   moduleName - Name of the module to define. MUST be a-z and all lowercase.
    //   builder    - Builder function that holds the module definition.
    //
    // Example:
    //   (start code)
    //
    //   remoteStorage.defineModule('beers', function(privateClient, publicClient) {
    //
    //     function nameToKey(name) {
    //       return name.replace(/\s/, '-');
    //     }
    //
    //     return {
    //       exports: {
    //
    //         addBeer: function(name) {
    //           return privateClient.storeObject('beer', nameToKey(name), {
    //             name: name,
    //             drinkCount: 0
    //           });
    //         },
    //
    //         logDrink: function(name) {
    //           var key = nameToKey(name);
    //           return privateClient.getObject(key).then(function(beer) {
    //             beer.drinkCount++;
    //             return privateClient.storeObject('beer', key, beer);
    //           });
    //         },
    //
    //         publishBeer: function(name) {
    //           var key = nameToKey(name);
    //           return privateClient.getObject(key).then(function(beer) {
    //             return publicClient.storeObject('beer', key, beer);
    //           });
    //         }
    //
    //       }
    //     }
    //   });
    //
    //   // to use that code from an app, you need to add:
    //
    //   remoteStorage.claimAccess('beers', 'rw').then(function() {
    //     remoteStorage.displayWidget()
    //
    //     remoteStorage.beers.addBeer('<replace-with-favourite-beer-kind>');
    //   });
    //
    //   (end code)
    //
    // See also:
    //   <BaseClient>
    //
    defineModule: function(moduleName, builder) {

      if(! moduleNameRE.test(moduleName)) {
        throw 'Invalid moduleName: "'+moduleName+'", only a-z lowercase allowed.';
      }

      var module = builder(
        // private client:
        new BaseClient(moduleName, false),
        // public client:
        new BaseClient(moduleName, true)
      );
      modules[moduleName] = module;
      if(typeof(module) !== 'object' || typeof(module.exports) !== 'object') {
        throw new Error("Invalid module format for module '" + moduleName + "', no 'exports' object returned!");
      }
      this[moduleName] = module.exports;
    },

    claimedModules: claimedModules,

    //
    // Method: getModuleList
    //
    // list known module names
    //
    // Returns:
    //   Array of module names.
    //
    getModuleList: function() {
      return Object.keys(modules);
    },

    // Method: getClaimedModuleList
    //
    // list of modules currently claimed access on
    //
    // Returns:
    //   Array of module names.
    //
    getClaimedModuleList: function() {
      return Object.keys(claimedModules);
    },

    //
    // Method: getModuleInfo
    //
    // Retrieve meta-information about a given module.
    //
    // If the module doesn't exist, the result will be undefined.
    //
    // Parameters:
    //   moduleName - name of the module
    //
    // Returns:
    //   An object, usually containing the following keys,
    //   * exports - don't ever use this. it's basically the module's instance.
    //   * name - the name of the module, but you knew that already.
    //   * dataHints - an object, describing internas about the module.
    //
    //
    //   Some of the dataHints used are:
    //
    //     objectType <type> - description of an object
    //                         type implemented by the module
    //     "objectType message" - (example)
    //
    //     <attributeType> <objectType>#<attribute> - description of an attribute
    //
    //     "string message#subject" - (example)
    //
    //     directory <path> - description of a path's purpose
    //
    //     "directory documents/notes/" - (example)
    //
    //     item <path> - description of a specific item
    //
    //     "item documents/notes/calendar" - (example)
    //
    //   Hope this helps.
    //
    getModuleInfo: function(moduleName) {
      var moduleInfo = modules[moduleName];
      if(moduleInfo) {
        return util.extend({
          name: moduleName,
          dataHints: {}
        }, moduleInfo);
      }
    },

    //
    // Method: claimAccess
    //
    // Either:
    //   <claimAccess(moduleName, claim)>
    //
    // Or:
    //   <claimAccess(moduleClaimMap)>
    //
    //
    //
    // Method: claimAccess(moduleName, claim)
    //
    // Claim access on a single module
    //
    // You need to claim access to a module before you can
    // access data from it.
    //
    // Parameters:
    //   moduleName - name of the module. For a list of defined modules, use <getModuleList>
    //   claim      - permission to claim, either *r* (read-only) or *rw* (read-write)
    //
    // Example:
    //   > remoteStorage.claimAccess('contacts', 'r').then(remoteStorage.displayWidget).then(initApp);
    //
    // Returns:
    //   a Promise, fulfilled when the access has been claimed.
    //
    // FIXME: this method is currently asynchronous due to internal design issues, but it doesn't need to be.
    //
    // Method: claimAccess(moduleClaimMap)
    //
    // Claim access to multiple modules.
    //
    // Parameters:
    //   moduleClaimMap - a JSON object with module names as keys and claims as values.
    //
    // Example:
    //   > remoteStorage.claimAccess({
    //   >   contacts: 'r',
    //   >   documents: 'rw',
    //   >   money: 'r'
    //   > }).then(remoteStorage.displayWidget).then(initApp);
    //
    // Returns:
    //   a Promise, fulfilled when the access has been claimed.
    //
    // FIXME: this method is currently asynchronous due to internal design issues, but it doesn't need to be.
    //
    claimAccess: function(moduleName, mode) {

      var modeTestRegex = /^rw?$/;
      function testMode(moduleName, mode) {
        if(!modeTestRegex.test(mode)) {
          throw "Claimed access to module '" + moduleName + "' but mode not correctly specified ('" + mode + "').";
        }
      }

      var moduleObj;
      if(typeof moduleName === 'object') {
        moduleObj = moduleName;
      } else {
        testMode(moduleName, mode);
        moduleObj = {};
        moduleObj[moduleName] = mode;
      }

      return util.asyncEach(Object.keys(moduleObj), function(_moduleName) {
        var _mode = moduleObj[_moduleName];
        testMode(_moduleName, _mode);
        return this.claimModuleAccess(_moduleName, _mode);
      }.bind(this));
    },

    // PRIVATE
    claimModuleAccess: function(moduleName, mode) {
      logger.debug('claimModuleAccess', moduleName, mode);
      if(!(moduleName in modules)) {
        throw "Module not defined: " + moduleName;
      }

      if(moduleName in claimedModules) {
        return;
      }

      claimedModules[moduleName] = mode;

      if(moduleName === 'root') {
        moduleName = '';
        return store.setNodeAccess('/', mode).
          then(util.curry(store.setNodeForce, '/', true, true));
      } else {
        var privPath = '/'+moduleName+'/';
        var pubPath = '/public/'+moduleName+'/';
        return store.setNodeAccess(privPath, mode).
          then(util.curry(store.setNodeForce, privPath, true, true)).
          then(util.curry(store.setNodeAccess, pubPath, mode)).
          then(util.curry(store.setNodeForce, pubPath, true, true));
      }
    },

    // PRIVATE
    setBearerToken: function(bearerToken, claimedScopes) {
      wireClient.setBearerToken(bearerToken);
    },

    getBearerToken: function() {
      return wireClient.getBearerToken();
    },

    disconnectRemote : wireClient.disconnectRemote,

    //
    // Method: flushLocal()
    //
    // Forget this ever happened.
    //
    // Delete all locally stored data.
    //
    // Note that if you're using a localStorage backend, this doesn't
    // clear the entire localStorage, but just removes everything
    // remoteStorage.js saved there. Other data your app might
    // have put into localStorage stays there.
    //
    // Call this method to implement "logout".
    //
    // If you are using the widget (which you should!), you don't need this.
    //
    // Example:
    //   > remoteStorage.flushLocal();
    //
    flushLocal       : function() {
      return util.makePromise(function(promise) {
        logger.info('flushLocal');
        store.forgetAll().then(promise.fulfill.bind(promise));
        sync.clearSettings();
        widget.clearSettings();
        schedule.reset();
        wireClient.disconnectRemote();
        i18n.clearSettings();
        claimedModules = {};
      });
    },

    //
    // Method: fullSync
    //
    // Synchronize local <-> remote storage.
    //
    // Syncing starts at the access roots (the once you claimed using claimAccess)
    // and moves down the directory tree.
    // Only nodes with a 'force' flag on themselves or one of their ancestors will
    // be synchronized. Use <BaseClient.use> and <BaseClient.release> to set / unset
    // force flags.
    // The actual changes to either local or remote storage happen in the
    // future, so you should attach change handlers on the modules you're
    // interested in.
    //
    // Parameters:
    //   callback - (optional) callback to be notified when synchronization has finished or failed.
    //
    // Example:
    //   >
    //   > remoteStorage.claimAccess('money', 'rw');
    //   >
    //   > remoteStorage.money.on('change', function(changeEvent) {
    //   >   // handle change event (update UI etc)
    //   > });
    //   >
    //   > remoteStorage.fullSync(function(errors) {
    //   >   // handle errors, if any.
    //   > });
    //
    // Yields:
    //   Array of error messages - when errors occured. When fullSync is called and the user is not connected, this is also considered an error.
    //   null - no error occured, synchronization finished gracefully.
    //
    fullSync: sync.fullSync,

    setWidgetView: widget.setView,

    //
    // Method: displayWidget
    //
    // Add the remoteStorage widget to the page.
    //
    // *Note* call <claimAccess> before calling displayWidget, otherwise you can't access any actual data.
    //
    // Parameters:
    //   element - (optional) DOM element to append the widget element, defaults to BODY element. May also be the ID of a DOM element.
    //   options - (optional) Options, as described below.
    //   options.locale - Locale to use for the widget. Currently ignored.
    //
    displayWidget: function(element, options) {
      widget.display(remoteStorage, element, util.extend({}, options));
    },

    //
    // Method: onWidget
    //
    // Add event handler to the widget.
    // See <widget.Events> for available Events.
    //
    // Parameters:
    //   eventType - type of event to add handler to
    //   handler   - handler function
    //
    onWidget: widget.on,

    //
    getSyncState: sync.getState,
    //
    setStorageInfo: wireClient.setStorageInfo,

    getStorageHref: wireClient.getStorageHref,

    getStorageType: wireClient.getStorageType,

    getStorageInfo: wireClient.getStorageInfo,

    disableSyncThrottling: sync.disableThrottling,

    nodeConnect: nodeConnect,

    util: util,

    // Method: getForeignClient
    //
    // Get a <ForeignClient> instance for a given user.
    //
    // Parameters:
    //   userAddress - a user address in the form user@host
    //   callback - a callback to receive the client
    //
    // If there is no storageInfo cached for this userAddress, this will trigger
    // a webfinger discovery and when that succeeded, return the client through
    // the callback.
    //
    // Example:
    //   (start code)
    //   var client = remoteStorage.getForeignClient('alice@wonderland.lit', function(error, client) {
    //     if(error) {
    //       console.error("Discovery failed: ", error);
    //     } else {
    //       client.getPublishedObjects(function(objects) {
    //         console.log('public stuff', objects);
    //       });
    //     }
    //   });
    //   (end code)
    getForeignClient: function(userAddress, callback) {
      var client = foreignClient.getClient(userAddress);
      if(wireClient.hasStorageInfo(userAddress)) {
        callback(null, client);
      } else {
        webfinger.getStorageInfo(
          userAddress, { timeout: 5000 }, function(err, storageInfo) {
            if(err) {
              callback(err);
            } else {
              wireClient.addStorageInfo(userAddress, storageInfo);
              callback(null, client);
            }
          }
        );
      }
    },

    getClient: function(scope) {
      return new BaseClient(scope);
    },

    // Property: store
    // Public access to <store>
    store: store,

    sync: sync,
    widget: widget,

    i18n: i18n,

    schedule: schedule,

    // for tests only.
    // FIXME: get rid of this by making internas more accessible. can probably be
    //        done with some general redesign of this file.
    _clearModules: function() {
      modules = {};
    }

  };

  return remoteStorage;
});
<|endoftext|>


<commit_before>import {createHashMap} from "../hashmap.js";
import {colors} from "../color.js";
import {clamp, divisors, nthParse, nthCheck} from "../math.js";
import {toArray, isObject, isValid} from "../util.js";

//Default panels props
let defaultProps = {
    "background": "transparent",
    "border": false,
    "borderColor": colors.black,
    "borderWidth": "1px"
};

//Default panels layout
let defaultPanelsLayout = {"rows": 1, "cols": 1, "spacing": 0, "length": 1};

//Calculate panels layout from value
// For example, if a value of 20 is provided, we calculate all divisors and calculate a score
// We will get the divisors pair with the lowest score. If there are more than one with the same score,
// we will get the one with the lowest first divisor (rows).
// 2, 10 ---> score = 12
// 4, 5  ---> score = 9  <<<< this is the best layout
// 5, 4  ---> score = 9
// 10, 2 ---> score = 12
// Result >>> rows=4, cols=5
let buildPanelsLayout = function (value) {
    //Check for negative, 0 or 1 values
    if (value < 2) {
        return {"rows": 1, "cols": 1, "length": 1}; //Return at least one panel
    }
    else if (value === 2 || value === 3) {
        return {"rows": 1, "cols": value, "length": value}; //Return only one row
    }
    let allDivisors = divisors(value); //Get all divisors of the provided value
    //Check if the value is a primer number (only one divisor)
    if (allDivisors.length === 1) {
        allDivisors = divisors(value + 1); //Convert to even number
    }
    //Calculate a score of each divisor
    let scores = allDivisors.map(function (d) {
        return d[0] + d[1];
    });
    let bestScore = Math.min.apply(null, scores); //Get the best score
    let filteredDivisors = allDivisors.filter(function (d, index) {
        return bestScore === scores[index]; //Get only the divisors of the best score
    });
    //Get the best divisor --> the one with the lowest first divisor
    let bestDivisor = filteredDivisors[0]; //Get the first divisor
    //The divisors list are sorted, so the first one has the lowest row value
    //for (let i = 1; i < filteredDivisors.length; i++) {
    //    if (bestDivisor[0] > filteredDivisors[i][0]) {
    //        bestDivisor = filteredDivisors[i]; //Update the best divisor
    //    }
    //}
    //Return the best panels layout
    return {"rows": bestDivisor[0], "cols": bestDivisor[1], "length": value};
};

//Get panels layout
let getPanelsLayout = function (context, props) {
    let newLayout = Object.assign({}, defaultPanelsLayout); //Initialize layout
    //Check for object ---> merge with default props
    if (isObject(props) === true) {
        //Check for row and column values
        if (isValid(props.rows) && isValid(props.cols)) {
            Object.assign(newLayout, {
                "rows": context.value(props.rows, 1, 1),
                "cols": context.value(props.cols, 1, 1)
            });
            newLayout.length = newLayout.rows * newLayout.cols; //Update layout length
        }
        //Check for size value --> calculate from
        else if (isValid(props.size)) {
            Object.assign(newLayout, buildPanelsLayout(context.value(props.size, 1, 1)));
        }
        //Check for spacing value
        if (isValid(props.spacing)) {
            Object.assign(newLayout, {"spacing": context.value(props.spacing, 0, 0)});
        }
    }
    //Check for number --> calculate the number of rows and columns
    else if (typeof props === "number") {
        Object.assign(newLayout, buildPanelsLayout(props)); //Build the layout
    }
    //Return the layout
    return newLayout;
};

//Get panels elements
export function getPanelsElements (context, props) {
    let layout = context.panels.value;
    if (typeof props === "undefined" || props === null || props == "") {
        return [context.panels.elements[0]]; //Return a single panel element
    }
    else if (typeof props === "string" && props === "*") {
        return context.panels.elements; //Return all panels
    }
    //let panels = context.panels;
    //Generate a list with all wanted panels indexes
    let indexes = {};
    toArray(props).forEach(function (value) {
        //Check for number --> as a single index
        if (typeof value === "number") {
            let index = clamp(value - 1, 0, context.panels.elements.length - 1);
            indexes[index] = 1; //Save this index
        }
        //Check for object --> TODO
        else if (isObject(value) && (typeof value.row === "number" || typeof value.col === "number")) {
            //Check for row and column provided
            if (typeof value.row === "number" && typeof value.col === "number") {
                let index = (value.col - 1) + (value.row - 1) * layout.cols;
                if (0 <= index && index < layout.length) {
                    indexes[index] = 1; //Save this single item
                }
            }
            //Check for only row value provided
            else if (typeof value.row === "number") {
                let row = value.row - 1;
                for (let i = 0; i < layout.cols; i++) {
                    let index = i + row * layout.cols;
                    if (0 <= index && index < layout.length) {
                        indexes[index] = 1; //Save this item
                    }
                }
            }
            //Check for only column value provided
            else {
                let col = value.col - 1; //Get column
                for (let i = 0; i < layout.rows; i++) {
                    let index = col + i * layout.cols;
                    if (0 <= index && index < layout.length) {
                        indexes[index] = 1; //Save this item
                    }
                }
            }
        }
        //Check for string ---> parse as a nth expression
        else if (typeof value === "string") {
            let nthExpression = nthParse(value); //Parse as an nth expression
            console.log(nthExpression);
            for (let i = 0; i < context.panels.value.length; i++) {
                console.log("check " + (i+1));
                if (nthCheck(i + 1, nthExpression) === true) {
                    console.log(`${i+1} ---> true`);
                    indexes[i] = 1; //Save this index
                }
            }
        }
        //Ignore other value types
    });
    //Return an array with all panels
    return Object.keys(indexes).map(function (index) {
        return context.panels.elements[index];
    });
}

//Create a panels node
export function createPanelsNode (context, props) {
    let node = context.addNode({
        "id": "panels",
        "type": "panels",
        "value": {"width": 0, "height": 0, "length": 0, "rows": 0, "cols": 0},
        "targets": createHashMap(),
        "elements": [],
        "props": props //parsePanelsProps(props)
    });
    //Add this node as a dependency of draw nodes
    context.draw.width.targets.add(node.id, node);
    context.draw.height.targets.add(node.id, node);
    context.draw.margin.targets.add(node.id, node);
    context.draw.outerMargin.targets.add(node.id, node);
    //Save the reference to the panels node
    context.panels = node;
}

//Update a panels node
export function updatePanelsNode (context, node) {
    let props = (isObject(node.props)) ? node.props : {};
    let layout = getPanelsLayout(context, node.props); //Build layout
    //layout.length = layout.rows * layout.cols; //Get layout total size
    //Check if we need to rebuild all panels groups
    if (node.value.rows !== layout.rows || node.value.cols !== layout.cols) {
        context.target.selectAll("g[data-type='panel']").remove(); //Remove all panels
        node.elements = []; //Reset nodes list
        for (let i = 0; i < layout.length; i++) {
            let row = Math.floor(i / layout.cols); //Get row index
            let col = i % layout.cols; //Get column index
            let element = context.target.append("g");
            element.attr("data-type", "panel"); //Set the group type ---> panel
            element.attr("data-row", row + ""); //Set the panel row
            element.attr("data-col", col + ""); //Set the panel col
            //Create a rectangle for displaying panel background
            let bg = element.append("rect");
            bg.attr("id", "panel-background").attr("x", "0").attr("y", "0").attr("fill", "none");
            //Save the panel group element
            node.elements.push(element);
        }
    }
    //Get the new sizes
    let margin = context.draw.margin.value;
    let outerMargin = context.draw.outerMargin.value;
    layout.width = ((context.draw.width.value - outerMargin.left - outerMargin.right) / layout.cols) - margin.left - margin.right;
    layout.height = ((context.draw.height.value - outerMargin.top - outerMargin.bottom) / layout.rows) - margin.top - margin.bottom;
    //Update the groups positions
    node.elements.forEach(function (element) {
        let row = parseInt(element.attr("data-row")); //Get row index
        let col = parseInt(element.attr("data-col")); //Get column index
        let left = (col * layout.width) + (col + 1) * margin.left + col * margin.right; //Calculate the left translation
        let top = (row * layout.height) + (row + 1) * margin.top + row * margin.bottom; //Calculate the top translation
        element.attr("transform", `translate(${left},${top})`); //Translate the panel group
        //Update the panel background
        let bg = element.selectAll("#panel-background");
        bg.attr("width", layout.width).attr("height", layout.height); //Set background size
        bg.attr("fill", context.value(props.background, null, context.theme.panelBackground));
    });
    //Update the layout config
    node.value = layout;
}

<commit_msg>render: allow negative row and col index<commit_after>import {createHashMap} from "../hashmap.js";
import {colors} from "../color.js";
import {clamp, divisors, nthParse, nthCheck} from "../math.js";
import {toArray, isObject, isValid} from "../util.js";

//Default panels props
let defaultProps = {
    "background": "transparent",
    "border": false,
    "borderColor": colors.black,
    "borderWidth": "1px"
};

//Default panels layout
let defaultPanelsLayout = {"rows": 1, "cols": 1, "spacing": 0, "length": 1};

//Calculate panels layout from value
// For example, if a value of 20 is provided, we calculate all divisors and calculate a score
// We will get the divisors pair with the lowest score. If there are more than one with the same score,
// we will get the one with the lowest first divisor (rows).
// 2, 10 ---> score = 12
// 4, 5  ---> score = 9  <<<< this is the best layout
// 5, 4  ---> score = 9
// 10, 2 ---> score = 12
// Result >>> rows=4, cols=5
let buildPanelsLayout = function (value) {
    //Check for negative, 0 or 1 values
    if (value < 2) {
        return {"rows": 1, "cols": 1, "length": 1}; //Return at least one panel
    }
    else if (value === 2 || value === 3) {
        return {"rows": 1, "cols": value, "length": value}; //Return only one row
    }
    let allDivisors = divisors(value); //Get all divisors of the provided value
    //Check if the value is a primer number (only one divisor)
    if (allDivisors.length === 1) {
        allDivisors = divisors(value + 1); //Convert to even number
    }
    //Calculate a score of each divisor
    let scores = allDivisors.map(function (d) {
        return d[0] + d[1];
    });
    let bestScore = Math.min.apply(null, scores); //Get the best score
    let filteredDivisors = allDivisors.filter(function (d, index) {
        return bestScore === scores[index]; //Get only the divisors of the best score
    });
    //Get the best divisor --> the one with the lowest first divisor
    let bestDivisor = filteredDivisors[0]; //Get the first divisor
    //The divisors list are sorted, so the first one has the lowest row value
    //for (let i = 1; i < filteredDivisors.length; i++) {
    //    if (bestDivisor[0] > filteredDivisors[i][0]) {
    //        bestDivisor = filteredDivisors[i]; //Update the best divisor
    //    }
    //}
    //Return the best panels layout
    return {"rows": bestDivisor[0], "cols": bestDivisor[1], "length": value};
};

//Get panels layout
let getPanelsLayout = function (context, props) {
    let newLayout = Object.assign({}, defaultPanelsLayout); //Initialize layout
    //Check for object ---> merge with default props
    if (isObject(props) === true) {
        //Check for row and column values
        if (isValid(props.rows) && isValid(props.cols)) {
            Object.assign(newLayout, {
                "rows": context.value(props.rows, 1, 1),
                "cols": context.value(props.cols, 1, 1)
            });
            newLayout.length = newLayout.rows * newLayout.cols; //Update layout length
        }
        //Check for size value --> calculate from
        else if (isValid(props.size)) {
            Object.assign(newLayout, buildPanelsLayout(context.value(props.size, 1, 1)));
        }
        //Check for spacing value
        if (isValid(props.spacing)) {
            Object.assign(newLayout, {"spacing": context.value(props.spacing, 0, 0)});
        }
    }
    //Check for number --> calculate the number of rows and columns
    else if (typeof props === "number") {
        Object.assign(newLayout, buildPanelsLayout(props)); //Build the layout
    }
    //Return the layout
    return newLayout;
};

//Get panels elements
export function getPanelsElements (context, props) {
    let layout = context.panels.value;
    if (typeof props === "undefined" || props === null || props == "") {
        return [context.panels.elements[0]]; //Return a single panel element
    }
    else if (typeof props === "string" && props === "*") {
        return context.panels.elements; //Return all panels
    }
    //let panels = context.panels;
    //Generate a list with all wanted panels indexes
    let indexes = {};
    toArray(props).forEach(function (value) {
        //Check for number --> as a single index
        if (typeof value === "number") {
            let index = clamp(value - 1, 0, context.panels.elements.length - 1);
            indexes[index] = 1; //Save this index
        }
        //Check for object --> TODO
        else if (isObject(value) && (typeof value.row === "number" || typeof value.col === "number")) {
            //Check for row and column provided
            if (typeof value.row === "number" && typeof value.col === "number") {
                let index = (value.col - 1) + (value.row - 1) * layout.cols;
                if (0 <= index && index < layout.length) {
                    indexes[index] = 1; //Save this single item
                }
            }
            //Check for only row value provided
            else if (typeof value.row === "number") {
                let row = (value.row < 0) ? layout.rows + value.row : value.row - 1;
                for (let i = 0; i < layout.cols; i++) {
                    let index = i + row * layout.cols;
                    if (0 <= index && index < layout.length) {
                        indexes[index] = 1; //Save this item
                    }
                }
            }
            //Check for only column value provided
            else {
                let col = (value.col < 0) ? layout.cols + value.col : value.col - 1; //Get column
                for (let i = 0; i < layout.rows; i++) {
                    let index = col + i * layout.cols;
                    if (0 <= index && index < layout.length) {
                        indexes[index] = 1; //Save this item
                    }
                }
            }
        }
        //Check for string ---> parse as a nth expression
        else if (typeof value === "string") {
            let nthExpression = nthParse(value); //Parse as an nth expression
            console.log(nthExpression);
            for (let i = 0; i < context.panels.value.length; i++) {
                console.log("check " + (i+1));
                if (nthCheck(i + 1, nthExpression) === true) {
                    console.log(`${i+1} ---> true`);
                    indexes[i] = 1; //Save this index
                }
            }
        }
        //Ignore other value types
    });
    //Return an array with all panels
    return Object.keys(indexes).map(function (index) {
        return context.panels.elements[index];
    });
}

//Create a panels node
export function createPanelsNode (context, props) {
    let node = context.addNode({
        "id": "panels",
        "type": "panels",
        "value": {"width": 0, "height": 0, "length": 0, "rows": 0, "cols": 0},
        "targets": createHashMap(),
        "elements": [],
        "props": props //parsePanelsProps(props)
    });
    //Add this node as a dependency of draw nodes
    context.draw.width.targets.add(node.id, node);
    context.draw.height.targets.add(node.id, node);
    context.draw.margin.targets.add(node.id, node);
    context.draw.outerMargin.targets.add(node.id, node);
    //Save the reference to the panels node
    context.panels = node;
}

//Update a panels node
export function updatePanelsNode (context, node) {
    let props = (isObject(node.props)) ? node.props : {};
    let layout = getPanelsLayout(context, node.props); //Build layout
    //layout.length = layout.rows * layout.cols; //Get layout total size
    //Check if we need to rebuild all panels groups
    if (node.value.rows !== layout.rows || node.value.cols !== layout.cols) {
        context.target.selectAll("g[data-type='panel']").remove(); //Remove all panels
        node.elements = []; //Reset nodes list
        for (let i = 0; i < layout.length; i++) {
            let row = Math.floor(i / layout.cols); //Get row index
            let col = i % layout.cols; //Get column index
            let element = context.target.append("g");
            element.attr("data-type", "panel"); //Set the group type ---> panel
            element.attr("data-row", row + ""); //Set the panel row
            element.attr("data-col", col + ""); //Set the panel col
            //Create a rectangle for displaying panel background
            let bg = element.append("rect");
            bg.attr("id", "panel-background").attr("x", "0").attr("y", "0").attr("fill", "none");
            //Save the panel group element
            node.elements.push(element);
        }
    }
    //Get the new sizes
    let margin = context.draw.margin.value;
    let outerMargin = context.draw.outerMargin.value;
    layout.width = ((context.draw.width.value - outerMargin.left - outerMargin.right) / layout.cols) - margin.left - margin.right;
    layout.height = ((context.draw.height.value - outerMargin.top - outerMargin.bottom) / layout.rows) - margin.top - margin.bottom;
    //Update the groups positions
    node.elements.forEach(function (element) {
        let row = parseInt(element.attr("data-row")); //Get row index
        let col = parseInt(element.attr("data-col")); //Get column index
        let left = (col * layout.width) + (col + 1) * margin.left + col * margin.right; //Calculate the left translation
        let top = (row * layout.height) + (row + 1) * margin.top + row * margin.bottom; //Calculate the top translation
        element.attr("transform", `translate(${left},${top})`); //Translate the panel group
        //Update the panel background
        let bg = element.selectAll("#panel-background");
        bg.attr("width", layout.width).attr("height", layout.height); //Set background size
        bg.attr("fill", context.value(props.background, null, context.theme.panelBackground));
    });
    //Update the layout config
    node.value = layout;
}

<|endoftext|>