busy-web/ember-date-time

View on GitHub
addon/utils/clock.js

Summary

Maintainability
A
3 hrs
Test Coverage
/**
 * @module Utils
 *
 */
import { isNone } from '@ember/utils';
import { assert } from '@ember/debug';
import { get, set } from '@ember/object';
import Base from './clock/base';
import { HOUR_FLAG } from './constant';
import dataArray, { /*getHourMinute,*/ getAttrs, createPoints } from './clock/data';
import render from './clock/render';
import SVG from './clock/svg';
import { metaName, elementName } from './clock/string';
import { onMoveStart, onMove, onMoveStop } from './clock/movement';
import {
    angleOfLine,
    getSliceDegree,
    getLineFromDegree,
    getSliceFromDegree,
    getBoundsCenter,
    createSVGPath
} from './clock/math';

/**
 * @class Clock
 */
class Clock extends dataArray(render(Base)) {
    /**
     * @public
     * @constructor
     * @params component {class} instance of the component class (this) reference
     * @params key {string} the key to reference this clocks meta
     * @params start {number} the number the clock should start with 0, 1...
     * @params end {number} the number the clock should end with 12, 59...
     * @params rounder {number} a number to round the clock digits by. 0 to render all numbers. 5 would render 5, 10, 15...
     */
    constructor(component, key, start=0, end=0, min, max, rounder=0, selectRounder=0) {
        assert("component is required to create a Clock meta object", !isNone(component));
        let { points, renderSize } = createPoints(start, end, rounder, selectRounder);
        super(points);

        this.setProps({
            component, key,
            name: metaName(key),
            elname: elementName(key),
            start, end, min, max,
            rounder, selectRounder,
            renderSize,
            lastTime: null,
            lastGroup: null
        });
    }

    get type() { return this.__key; }
    get points() { return this.__data; }
    get elementName() { return this.__elname; }
    get length() { return this.__renderSize; }

    get container() {
        if (isNone(this.__container) || !this.__container.length) {
            if (!isNone(this.__component.$()) && this.__component.$().length) {
                this.__container = this.__component.$(this.__elname);
            }
        }
        return this.__container;
    }

    get svg() {
        if (!this.__snap) {
            const el = this.container.find('svg');
            this.__snap = new SVG(el.get(0), { rounder: this.__rounder, selectRounder: this.__selectRounder, min: this.__min, max: this.__max });
        }
        return this.__snap;
    }

    renderDOM(timestamp) {
        if (!isNone(this.container)) {
            this.__lastTime = timestamp;
            const height = this.container.height();
            const width = this.container.width();
            this.setBounds(0, 0, height, width);

            this.filterEach('isRender', p => {
                this.svg.minMaxHandler(this.type, p.num, this.__min, this.__max, timestamp);
            });
        }
    }

    click(cb) {
        this.__clickHandler = (evt) => this.clickHandler(evt, cb);
        this.svg.snap.click(this.__clickHandler);
    }

    unclick() {
        this.svg.snap.unclick(this.__clickHandler);
    }

    clickHandler(evt, cb=function(){}) {
        evt = evt || window.event;
        // use the svg element as our target element
        const target = this.container.find('svg').get(0);

        // get the main clock center x and y
        const faceAttrs = getAttrs(this.svg.face, ['cx', 'cy']);
        const cx = parseInt(faceAttrs.cx, 10);
        const cy = parseInt(faceAttrs.cy, 10);

        // calculate offset because any browser other than chrome
        // is not caluculating it correctly
        const rect = target.getBoundingClientRect();
        const offsetX = evt.clientX - rect.left;
        const offsetY = evt.clientY - rect.top;

        // use offsetX and offsetY to get local point to clock
        let angle = angleOfLine(offsetX, offsetY, cx, cy);
        let point = this.getPointFromDegree(angle);

        // call the callback with the number selected
        cb(point.num);
    }

    cleanup() {
        if (!isNone(this.container)) {
            this.svg.clean(this.__start, this.__end);
        }
    }

    selectPlot(num) {
        if (!isNone(this.container)) {
            this.svg.selectPlot(num);
        }
    }

    unselectPlot(num) {
        if (!isNone(this.container)) {
            this.svg.unselectPlot(num);
        }
    }

    setBounds(minX, minY, width, height) {
        // hide box until calculations have been made
        // safari fix
        this.svg.__el.style.display = "none";
        this.svg.snap.attr({ viewBox: `${minX} ${minY} ${width} ${height}` });

        const faceCoords = getBoundsCenter(width, height);

        this.svg.face.attr({ cx: faceCoords.cx, cy: faceCoords.cy, r: faceCoords.r });
        this.svg.pivot.attr({ cx: faceCoords.cx, cy: faceCoords.cy, r: ( faceCoords.r * 0.0283 ) });

        const plotR = this.type === HOUR_FLAG ? (faceCoords.r * 0.14) : (faceCoords.r * 0.12);        // radius for the plot circles that hold the numbers
        const padding = (width * 0.0306);                                                                                                                // padding to keep plot circles in the clock face
        const armLen = (faceCoords.r - plotR) - padding;                                                                                // length for the arms on the clock
        const pathLen = (armLen - (plotR * 2));

        this.filterEach('isRender', point => {
            const { arm, plot, path, text } = this.svg.at(point.num);
            const degree = getSliceDegree(this.points.length, point.num);

            const armCoords = getLineFromDegree(degree, armLen, faceCoords.cx, faceCoords.cy);        // xy coords for the plot circle center
            arm.attr(armCoords);
            plot.attr({ cx: armCoords.x2, cy: armCoords.y2, r: plotR });

            // calculate text position if there is a text
            // at this number
            if (!isNone(text)) {
                // show box long enough to get text box size
                this.svg.__el.style.display = "";
                const bounds = text.node.getBBox();
                this.svg.__el.style.display = "none";

                let tx = (armCoords.x2 - (bounds.width / 2));
                let ty = (armCoords.y2 + (bounds.height / 3));
                let tAttr = `translate(${tx}, ${ty})`;
                text.attr('transform', tAttr);
            }

            // calculate section position for click areas on minutes
            if (!isNone(path)) {
                const pt = createSVGPath(this.length, degree, faceCoords.cx, faceCoords.cy, pathLen, faceCoords.r);
                // build the path string
                path.attr(pt);
            }
        });
        // show box
        this.svg.__el.style.display = "";
    }

    handleDrag(num, cb) {
        const faceAttrs = getAttrs(this.svg.face, ['cx', 'cy']);
        const angle = getSliceDegree(this.points.length, num);

        const { plot, arm, text } = this.svg.at(num);
        const plotAttrs = getAttrs(plot, ['cx', 'cy']);

        if (!isNone(this.__lastGroup)) {
            this.__lastGroup.undrag();
        }

        let group;
        if (!isNone(text)) {
            group = this.svg.snap.g(arm, plot, text);
        } else {
            group = this.svg.snap.g(arm, plot);
        }

        let point;
        group.drag(
            onMove(angle, parseFloat(faceAttrs.cx), parseFloat(faceAttrs.cy), parseFloat(plotAttrs.cx), parseFloat(plotAttrs.cy), ang => {
                point = this.getPointFromDegree(ang);
            }),
            onMoveStart(text, 'selected', this.svg.snap),
            onMoveStop(this, (el, evt) => {
                if (!point) {
                    // use offsetX and offsetY to get local point to clock
                    let angle = angleOfLine(evt.offsetX, evt.offsetY, faceAttrs.cx, faceAttrs.cy);
                    point = this.getPointFromDegree(angle);
                }
                cb(point.num);
            })
        );

        this.__lastGroup = group;
    }

    getPointFromDegree(degree) {
        let len = this.__end;
        if (this.__start === 0) {
            len = len + 1;
        }

        // get round num for rounding by 1, 5, 10, 15, 30
        const round = this.__selectRounder || 1;

        // get slice num for clock
        let slice = getSliceFromDegree(len, degree);

        // round the slice based on the selected round number
        slice = Math.round(slice / round) * round;

        // make sure max and min numbers are correct
        if (this.__end < slice) {
            slice = this.__start;
        } else if (this.__start > slice) {
            slice = this.__end;
        }

        // subtract the offset for the array index
        if (this.__start !== 0) {
            slice = slice - this.__start;
        }

        return this.objectAt(slice);
    }
}

export function createMetaFor(component, key, start, end, min, max, rounder, selectRounder) {
    const clock = new Clock(component, key, start, end, min, max, rounder, selectRounder);
    set(component, metaName(key), clock);
}

export function metaFor(component, key) {
    return get(component, metaName(key));
}