// christopher pietsch
// cpietsch@gmail.com
// 2015-2018


function Canvas() {
    var margin = {
        top: 20,
        right: 50,
        bottom: 30,
        left: 50
    };

    var minHeight = 400;
    var width = window.innerWidth - margin.left - margin.right;
    var widthOuter = window.innerWidth;
    var height = window.innerHeight < minHeight ? minHeight : window.innerHeight;

    var scale;
    var scale1 = 1;
    var scale2 = 1;
    var scale3 = 1;
    var allData = [];

    var translate = [0, 0];
    var scale = 1;
    var timeDomain = [];
    var loadImagesCue = [];

    var x = d3.scale.ordinal()
        .rangeBands([margin.left, width + margin.left], 0.2);

    var Quadtree = d3.geom.quadtree()
        .x(function (d) {
            return d.x;
        })
        .y(function (d) {
            return d.y;
        });

    var quadtree;

    var maxZoomLevel = utils.isMobile() ? 400 : 800;

    var zoom = d3.behavior.zoom()
        .scaleExtent([1, maxZoomLevel])
        .size([width, height])
        .on("zoom", zoomed)
        .on("zoomend", zoomend)
        .on("zoomstart", zoomstart);

    var canvas;
    var config;
    var container;
    var entries;
    var years;
    var data;
    var rangeBand = 0;
    var rangeBandImage = 0;
    var imageSize = 256;
    var imageSize2 = 1024;
    var imageSize3 = 4000;
    var collumns = 4;
    var renderer, stage;

    var svgscale, voronoi;

    var selectedImageDistance = 0;
    var selectedImage = null;

    var drag = false;
    var sleep = false

    var stagePadding = 40;
    var imgPadding;

    var bottomPadding = 70;
    var extent = [0, 0];
    var bottomZooming = true;

    var touchstart = 0;
    var vizContainer;
    var spriteClick = false

    var state = {
        lastZoomed: 0,
        zoomingToImage: false,
        mode: "time",
        init: false
    };

    var zoomedToImage = false;
    var zoomedToImageScale = 30;
    var zoomBarrier = 2;

    var startTranslate = [0, 0];
    var startScale = 0;
    var cursorCutoff = 1;
    var zooming = false;
    var detailContainer = d3.select(".sidebar")
    var timelineData;
    var stage, stage1, stage2, stage3, stage4, stage5;
    var timelineHover = false;
    var tsne = []
    var tsneIndex = {}

    var scaleFactor = 0.9

    function canvas() {}

    canvas.rangeBand = function () {
        return rangeBand
    }
    canvas.width = function () {
        return width
    }
    canvas.height = function () {
        return height
    }
    canvas.rangeBandImage = function () {
        return rangeBandImage
    }
    canvas.zoom = zoom
    canvas.selectedImage = function () {
        return selectedImage
    }
    canvas.x = x

    canvas.resize = function () {
        if (!state.init) return;
        width = window.innerWidth - margin.left - margin.right;
        height = window.innerHeight < minHeight ? minHeight : window.innerHeight;
        widthOuter = window.innerWidth;
        renderer.resize(width + margin.left + margin.right, height);
        canvas.makeScales();
        canvas.project();
    }

    canvas.makeScales = function () {
        x.rangeBands([margin.left, width + margin.left], 0.2)

        rangeBand = x.rangeBand();
        rangeBandImage = x.rangeBand() / collumns;

        imgPadding = rangeBand / collumns / 2;

        scale1 = imageSize / (x.rangeBand() / collumns);
        scale2 = imageSize2 / (x.rangeBand() / collumns);
        scale3 = imageSize3 / (x.rangeBand() / collumns);

        stage3.scale.x = 1 / scale1;
        stage3.scale.y = 1 / scale1;
        stage3.y = height;

        stage4.scale.x = 1 / scale2;
        stage4.scale.y = 1 / scale2;
        stage4.y = height;

        stage5.scale.x = 1 / scale3;
        stage5.scale.y = 1 / scale3;
        stage5.y = height;

        timeline.rescale(scale1)

        cursorCutoff =  1/scale1 * imageSize * 0.48
        zoomedToImageScale = 0.3 / (x.rangeBand() / collumns / width)
        // console.log("zoomedToImageScale", zoomedToImageScale)
    }

    canvas.init = function (_data, _timeline, _config) {
        data = _data;
        config = _config;

        container = d3.select(".page").append("div").classed("viz", true);
        detailVue._data.structure = config.detail.structure

        collumns = config.projection.columns;
        imageSize = config.loader.textures.medium.size;
        imageSize2 = config.loader.textures.detail.size;

        if (config.loader.textures.big) {
            imageSize3 = config.loader.textures.big.size;
        }

        PIXI.settings.SCALE_MODE = 1

        var renderOptions = {
            resolution: 1,
            antialiasing: false,
            width: width + margin.left + margin.right,
            height: height
        };
        renderer = new PIXI.WebGLRenderer(renderOptions);
        renderer.backgroundColor = parseInt(config.style.canvasBackground.substring(1), 16)
        window.renderer = renderer;

        var renderElem = d3.select(container.node().appendChild(renderer.view));

        stage = new PIXI.Container();
        stage2 = new PIXI.Container();
        stage3 = new PIXI.Container();
        stage4 = new PIXI.Container();
        stage5 = new PIXI.Container();

        stage.addChild(stage2);
        stage2.addChild(stage3);
        stage2.addChild(stage4);
        stage2.addChild(stage5);

        _timeline.forEach(function (d) {
            d.type = "timeline";
        });

        var canvasDomain = d3.nest()
            .key(function (d) {
                return d.year;
            })
            .entries(_data.concat(_timeline))
            .sort(function (a, b) {
                return a.key - b.key;
            })
            .map(function (d) {
                return d.key;
            })

        timeDomain = canvasDomain.map(function (d) {
            return {
                key: d,
                values: _timeline.filter(function (e) {
                    return d == e.year;
                })
            }
        })

        timeline.init(timeDomain)
        x.domain(canvasDomain);
        canvas.makeScales();

        // add preview pics
        data.forEach(function (d, i) {
            var sprite = new PIXI.Sprite(PIXI.Texture.WHITE);

            sprite.anchor.x = 0.5;
            sprite.anchor.y = 0.5;

            sprite.scale.x = scaleFactor;
            sprite.scale.y = scaleFactor;

            sprite._data = d;
            d.sprite = sprite;

            stage3.addChild(sprite);

        })


        vizContainer = d3.select(".viz")
            .call(zoom)
            .on("mousemove", mousemove)
            .on("dblclick.zoom", null)
            .on("touchstart", function (d) {
                mousemove(d);
                touchstart = new Date() * 1;
            })
            .on("touchend", function (d) {
                var touchtime = (new Date() * 1) - touchstart;
                if (touchtime > 250) return;
                if (selectedImageDistance > 15) return;
                if (selectedImage && !selectedImage.id) return;
                if (selectedImage && !selectedImage.active) return;
                if (drag) return;

                zoomToImage(selectedImage, 1400 / Math.sqrt(Math.sqrt(scale)))
            })
            .on("click", function () {
                console.log("click");
                // if (spriteClick) {
                //     spriteClick = false;
                //     return;
                // }

                // if (selectedImage && !selectedImage.id) return;
                if (drag) return;
                // if (selectedImageDistance > cursorCutoff) return;
                // if (selectedImage && !selectedImage.active) return;
                if (timelineHover) return;
                // console.log(selectedImage)

                // if (Math.abs(zoomedToImageScale - scale) < 0.1) {
                //     canvas.resetZoom();
                // } else {
                //     zoomToImage(selectedImage, 1400 / Math.sqrt(Math.sqrt(scale)));
                // }
                console.log("zoom to ")
                zoomToImage(selectedImage, 1400 / Math.sqrt(Math.sqrt(scale)));

            })

        canvas.project();
        animate();

        // selectedImage = data.find(d => d.id == 88413)
        // showDetail(selectedImage)
        state.init = true;
    };

    canvas.addTsneData = function (d) {
        console.time("tsne")
        var clean = d.map(function (d) {
            return {
                id: d.id,
                x: parseFloat(d.x),
                y: parseFloat(d.y)
            }
        })
        var xExtent = d3.extent(clean, function (d) {
            return d.x
        })
        var yExtent = d3.extent(clean, function (d) {
            return d.y
        })

        var x = d3.scale.linear().range([0, 1]).domain(xExtent)
        var y = d3.scale.linear().range([0, 1]).domain(yExtent)

        d.forEach(function (d) {
            tsneIndex[d.id] = [
                x(d.x),
                y(d.y)
            ]
        })

        data.forEach(d => {
            d.active = tsneIndex[d.id] !== undefined
        })

        console.timeEnd("tsne")
    }


    function mousemove(d) {
        if (timelineHover) return;

        var mouse = d3.mouse(vizContainer.node());
        var p = toScreenPoint(mouse);

        var distance = 200

        var best = nearest(p[0] - imgPadding, p[1] - imgPadding, {
            d: distance,
            p: null
        }, quadtree);

        selectedImageDistance = best.d;
        // console.log(cursorCutoff,scale, scale1,  selectedImageDistance)

        
            if (best.p) {
                var d = best.p;
                var center = [((d.x + imgPadding) * scale) + translate[0], (height + d.y + imgPadding) * scale + translate[1]];
                zoom.center(center);
                selectedImage = d;
            }

            container.style("cursor", function () {
                return ((selectedImageDistance < cursorCutoff) && selectedImage.active) ? "pointer" : "default";
            });
        
    }


    function stackLayout(data, invert) {

        var years = d3.nest()
            .key(function (d) {
                return d.year;
            })
            .entries(data)

        years.forEach(function (year) {
            var startX = x(year.key);
            var total = year.values.length;
            year.values.sort(function (a, b) {
                return b._layerId - a._layerId;
            })

            year.values.forEach(function (d, i) {
                var row = (Math.floor(i / collumns) + 2);
                d.ii = i;

                d.x = startX + ((i % collumns) * (rangeBand / collumns));
                d.y = (invert ? 1 : -1) * (row * (rangeBand / collumns));

                d.x1 = d.x * scale1 + imageSize / 2;
                d.y1 = d.y * scale1 + imageSize / 2;

                d.sprite.scale.x = d.sprite.scale.y = scaleFactor

                if (d.sprite.position.x == 0) {
                    d.sprite.position.x = d.x1;
                    d.sprite.position.y = d.y1;
                }

                if (d.sprite2) {
                    d.sprite2.position.x = d.x * scale2 + imageSize2 / 2;
                    d.sprite2.position.y = d.y * scale2 + imageSize2 / 2;
                }

                d.order = (invert ? 1 : 1) * (total - i);
            })
        })
    }

    canvas.distance = function (a, b) {
        return Math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]));
    }

    function toScreenPoint(p) {
        var p2 = [0, 0];

        p2[0] = p[0] / scale - translate[0] / scale;
        p2[1] = (p[1] / scale - height) - translate[1] / scale;

        return p2;
    }

    function imageAnimation() {
        var sleep = true

        data.forEach(function (d, i) {
            var diff;
            diff = (d.x1 - d.sprite.position.x);
            if (Math.abs(diff) > 0.1) {
                d.sprite.position.x += diff * 0.1;
                sleep = false;
            }

            diff = (d.y1 - d.sprite.position.y);
            if (Math.abs(diff) > 0.1) {
                d.sprite.position.y += diff * 0.1
                sleep = false;
            }

            diff = (d.alpha - d.sprite.alpha);
            if (Math.abs(diff) > 0.01) {
                d.sprite.alpha += diff * 0.2
                sleep = false;
            }

            d.sprite.visible = d.visible && d.sprite.alpha > 0.1;

            // d.sprite.visible = d.visible;


            if (d.sprite2) {
                diff = (d.alpha2 - d.sprite2.alpha);
                if (Math.abs(diff) > 0.01) {
                    d.sprite2.alpha += diff * 0.2
                    sleep = false;
                }

                d.sprite2.visible = d.sprite2.alpha > 0.1;
                //else d.sprite2.visible = d.visible;
            }
        });
        return sleep
    }


    canvas.wakeup = function () {
        sleep = false
    }

    canvas.setMode = function (mode) {
        state.mode = mode
        canvas.project()
    }

    function animate(time) {
        requestAnimationFrame(animate);
        loadImages();
        if (sleep) return
        sleep = imageAnimation();
        renderer.render(stage);
    }

    function zoomToYear(d) {

        var xYear = x(d.year);
        var scale = 1 / (rangeBand * 4 / width);
        var padding = rangeBand * 1.5
        var translateNow = [-scale * (xYear - padding), -scale * (height + d.y)];

        vizContainer
            .call(zoom.translate(translate).event)
            .transition().duration(2000)
            .call(zoom.scale(scale).translate(translateNow).event)
    }

    function zoomToImage(d, duration) {
        state.zoomingToImage = true;
        zoom.center(null);
        loadMiddleImage(d);
        d3.select(".tagcloud").classed("hide", true);
        var padding = x.rangeBand() / collumns / 2;
        var sidbar = width / 8;
        var scale = 0.3 / (x.rangeBand() / collumns / width);
        var translateNow = [(-scale * d.x) - sidbar + width/2 - imageSize/2, (-scale * (height + d.y)) + height/4];

        console.log(translateNow, width, scale, d.x)

        zoomedToImageScale = scale;

        setTimeout(function () {
            hideTheRest(d);
        }, duration / 2);

        vizContainer
            .call(zoom.translate(translate).event)
            .transition().duration(duration)
            .call(zoom.scale(scale).translate(translateNow).event)
            .each("end", function () {
                zoomedToImage = true;
                selectedImage = d;
                hideTheRest(d);
                showDetail(d);
                loadBigImage(d, "click");
                state.zoomingToImage = false;
            })
    }

    function showDetail(d) {
        // console.log("show detail", d)

        detailContainer
            .select(".outer")
            .node()
            .scrollTop = 0;

        detailContainer
            .classed("hide", false)
            .classed("sneak", utils.isMobile())

        // needs to be done better
        var detailData = {}
        for (field in selectedImage) {
            if (field[0] === '_') detailData[field] = selectedImage[field]
        }
        detailData['_id'] = selectedImage.id
        detailData['_keywords'] = selectedImage.keywords
        detailData['_year'] = selectedImage.year
        detailData['_imagenum'] = selectedImage.imagenum || 1
        detailVue._data.item = detailData
        detailVue._data.id = d.id
        detailVue._data.page = d.page

    }

    canvas.changePage = function (id, page) {
        console.log("changePage", id, page, selectedImage);
        selectedImage.page = page
        detailVue._data.page = page
        clearBigImages();
        loadBigImage(selectedImage)
    }


    function hideTheRest(d) {
        return;
        data.forEach(function (d2) {
            if (d2.id !== d.id) {
                d2.alpha = 0;
                d2.alpha2 = 0;

            }
        })
    }

    function showAllImages() {
        data.forEach(function (d) {
            d.alpha = d.active ? 1 : 0.2;;
            d.alpha2 = d.visible ? 1 : 0;
        })
    }

    function zoomed() {
        translate = d3.event.translate;
        scale = d3.event.scale;
        if (!startTranslate) startTranslate = translate
        drag = startTranslate && translate !== startTranslate;
        // check borders
        var x1 = -1 * translate[0] / scale;
        var x2 = (x1 + (widthOuter / scale));

        if (d3.event.sourceEvent != null) {
            if (x1 < 0) {
                translate[0] = 0;
            } else if (x2 > widthOuter) {
                translate[0] = ((widthOuter * scale) - widthOuter) * -1;
            }

            zoom.translate([translate[0], translate[1]]);

            x1 = -1 * translate[0] / scale;
            x2 = (x1 + (width / scale))
        }

        if (zoomedToImageScale != 0 && scale > zoomedToImageScale*0.9 && !zoomedToImage && selectedImage && selectedImage.type == "image") {

            zoomedToImage = true;
            zoom.center(null);
            zoomedToImageScale = scale;
            hideTheRest(selectedImage);
            showDetail(selectedImage)
        }

        if (zoomedToImage && zoomedToImageScale *0.8 > scale) {
            // console.log("clear")
            zoomedToImage = false;
            state.lastZoomed = 0;
            showAllImages();
            clearBigImages();
            detailContainer.classed("hide", true)
        }

        timeline.update(x1, x2, scale, translate, scale1);

        // toggle zoom overlays
        if (scale > zoomBarrier) {
            d3.select(".tagcloud").classed("hide", true);
            d3.select(".searchbar").classed("hide", true);
            d3.select(".infobar").classed("sneak", true);
        } else {
            d3.select(".tagcloud").classed("hide", false);
            d3.select(".searchbar").classed("hide", false);
        }


        stage2.scale.x = d3.event.scale;
        stage2.scale.y = d3.event.scale;
        stage2.x = d3.event.translate[0];
        stage2.y = d3.event.translate[1];

        sleep = false
    }

    function zoomstart(d) {
        zooming = true;
        startTranslate = false;
        drag = false
        startScale = scale;
        sleep = false
    }

    function zoomend(d) {
        drag = startTranslate && translate !== startTranslate;
        zooming = false;
        filterVisible();
        sleep = false

        if (zoomedToImage && !selectedImage.big && state.lastZoomed != selectedImage.id && !state.zoomingToImage) {
            loadBigImage(selectedImage, "zoom");
        }
    }

    canvas.highlight = function () {
        data.forEach(function (d, i) {
            d.alpha = d.highlight ? 1 : 0.2;
        });
        canvas.wakeup();
    }

    // canvas.project = function () {
    //     sleep = false
    //     canvas.split();
    //     canvas.resetZoom();
    // }

    canvas.project = function () {
        sleep = false
        if (state.mode == "tsne") {
            scaleFactor = 0.5
            canvas.projectTSNE();
        } else {
            scaleFactor = 0.9
            canvas.split();
        }
        canvas.resetZoom();
    }

    canvas.projectTSNE = function () {

        var marginBottom = -height / 2.5;

        var inactive = data.filter(function (d) {
            return !d.active;
        });
        var inactiveSize = inactive.length;

        var active = data.filter(function (d) {
            return d.active;
        });

        // inactive.sort(function (a, b) {
        //     return a.rTSNE - b.rTSNE
        // });

        var dimension = Math.min(width, height) * 0.8

        inactive.forEach(function (d, i) {
            var r = dimension / 1.9 + Math.random() * 40;
            var a = -Math.PI / 2 + (i / inactiveSize) * 2 * Math.PI;

            d.x = r * Math.cos(a) + width / 2 + margin.left;
            d.y = r * Math.sin(a) + marginBottom;
        });

        active.forEach(function (d) {

            var factor = height / 2;
            var tsneEntry = tsneIndex[d.id]
            if(tsneEntry) {
                d.x = (tsneEntry[0] * dimension) + width / 2 - dimension / 2 + margin.left;
                d.y = (tsneEntry[1] * dimension) - dimension / 2 + marginBottom;
            }
            // var tsneEntry = tsne.find(function (t) {
            //     return t.id == d.id
            // })
           

        })

        data.forEach(function (d) {
            d.x1 = d.x * scale1 + imageSize / 2;
            d.y1 = d.y * scale1 + imageSize / 2;

            d.sprite.scale.x = d.sprite.scale.y = scaleFactor

            if (d.sprite.position.x == 0) {
                d.sprite.position.x = d.x1;
                d.sprite.position.y = d.y1;
            }

            if (d.sprite2) {
                d.sprite2.position.x = d.x * scale2 + imageSize2 / 2;
                d.sprite2.position.y = d.y * scale2 + imageSize2 / 2;
            }
        });


        quadtree = Quadtree(data);
        //chart.resetZoom();


    }

    canvas.resetZoom = function () {
        var duration = 1400;

        extent = d3.extent(data, function (d) {
            return d.y;
        });

        var y = -extent[1]*2 -height -extent[0]/4;
        // y = (extent[1] / -1.5) - bottomPadding

        vizContainer
            .call(zoom.translate(translate).event)
            .transition().duration(duration)
            .call(zoom.translate([-width/2, y]).scale(2).event)
    }

    canvas.split = function () {
        var active = data.filter(function (d) {
            return d.active;
        })
        stackLayout(active, false);
        var inactive = data.filter(function (d) {
            return !d.active;
        })
        stackLayout(inactive, true);
        quadtree = Quadtree(data);
    }

    function filterVisible() {
        var zoomScale = scale;
        if (zoomedToImage) return;

        data.forEach(function (d, i) {
            var p = d.sprite.position;
            var x = (p.x / scale1) + translate[0] / zoomScale;
            var y = ((p.y / scale1) + (translate[1]) / zoomScale);
            var padding = (width/3) / scale;

            if (x > (-padding) && x < ((width / zoomScale) + padding) && y + height < (height / zoomScale + padding) && y > (height * -1) - padding) {
                d.visible = true;              
            } else {
                d.visible = false;
            }
        });

        var visible = data.filter(function (d) {
            return d.visible;
        });

        console.log(visible.length)

        if (visible.length < 200) {
            data.forEach(function (d) {
                if (d.visible && d.loaded && d.active) {d.alpha2 = 1 }
                else if (d.visible && !d.loaded && d.active) loadImagesCue.push(d);
                else {d.alpha2 = 0}
            })
        } 
        // else {
        //     data.forEach(function (d) {
        //         d.alpha2 = 0;
        //         // d.alpha = 1
        //         d.visible = true;
        //     })
        // }
    }

    function loadMiddleImage(d) {
        if (d.loaded) {
            d.alpha2 = 1;
            return;
        }

        // console.log("load", d)
        var url = config.loader.textures.detail.url + d.id + '.jpg'
        var texture = new PIXI.Texture.fromImage(url)
        var sprite = new PIXI.Sprite(texture);

        var update = function () {
            sleep = false
            d.visible = false
        }

        sprite.on('added', update)
        texture.once('update', update)

        sprite.scale.x = scaleFactor;
        sprite.scale.y = scaleFactor;

        sprite.anchor.x = 0.5;
        sprite.anchor.y = 0.5;
        sprite.position.x = d.x * scale2 + imageSize2 / 2;
        sprite.position.y = d.y * scale2 + imageSize2 / 2;
        sprite._data = d;
        stage4.addChild(sprite);
        d.sprite2 = sprite;
        d.alpha2 = d.highlight;
        d.loaded = true;
        sleep = false
    }

    function loadBigImage(d) {
        if (!config.loader.textures.big) {
            loadMiddleImage(d)
            return
        }

        state.lastZoomed = d.id;
        var page = d.page ? '_' + d.page : ''
        var url = config.loader.textures.big.url + d.id + page + ".jpg";

        console.log(url)

        var texture = new PIXI.Texture.from(url)
        var sprite = new PIXI.Sprite(texture);
        var res = config.loader.textures.big.size

        var updateSize = function () {
            var size = Math.max(texture.width, texture.height)
            sprite.scale.x = sprite.scale.y = (imageSize3 / size) * scaleFactor;
            sleep = false
        }

        sprite.on('added', updateSize)
        texture.once('update', updateSize)

        sprite.anchor.x = 0.5;
        sprite.anchor.y = 0.5;
        sprite.position.x = d.x * scale3 + imageSize3 / 2;
        sprite.position.y = d.y * scale3 + imageSize3 / 2;
        sprite._data = d;
        d.big = true;
        stage5.addChild(sprite);
        sleep = false
    }

    function clearBigImages() {
        while (stage5.children[0]) {
            stage5.children[0]._data.big = false;
            stage5.removeChild(stage5.children[0]);
            sleep = false
        }
    }

    function loadImages() {
        if (zooming) return;
        if (zoomedToImage) return;

        if (loadImagesCue.length) {
            var d = loadImagesCue.pop();
            // console.log(d.id)
            if (!d.loaded) {
                loadMiddleImage(d);
            }
        }
        if (loadImagesCue.length) {
            var d = loadImagesCue.pop();
            // console.log(d.id)
            if (!d.loaded) {
                loadMiddleImage(d);
            }
        }
        if (loadImagesCue.length) {
            var d = loadImagesCue.pop();
            // console.log(d.id)
            if (!d.loaded) {
                loadMiddleImage(d);
            }
        }
        // if (loadImagesCue.length) {
        //     var d = loadImagesCue.pop();
        //     console.log(d.id)
        //     if (!d.loaded) {
        //         loadMiddleImage(d);
        //     }
        // }
    }

    function nearest(x, y, best, node) {
        // mike bostock https://bl.ocks.org/mbostock/4343214
        var x1 = node.x1,
            y1 = node.y1,
            x2 = node.x2,
            y2 = node.y2;
        node.visited = true;
        //console.log(node, x , x1 , best.d);
        //return;
        // exclude node if point is farther away than best distance in either axis
        if (x < x1 - best.d || x > x2 + best.d || y < y1 - best.d || y > y2 + best.d) {
            return best;
        }
        // test point if there is one, potentially updating best
        var p = node.point;
        if (p) {
            p.scanned = true;
            var dx = p.x - x,
                dy = p.y - y,
                d = Math.sqrt(dx * dx + dy * dy);
            if (d < best.d) {
                best.d = d;
                best.p = p;
            }
        }
        // check if kid is on the right or left, and top or bottom
        // and then recurse on most likely kids first, so we quickly find a 
        // nearby point and then exclude many larger rectangles later
        var kids = node.nodes;
        var rl = (2 * x > x1 + x2),
            bt = (2 * y > y1 + y2);
        if (kids[bt * 2 + rl]) best = nearest(x, y, best, kids[bt * 2 + rl]);
        if (kids[bt * 2 + (1 - rl)]) best = nearest(x, y, best, kids[bt * 2 + (1 - rl)]);
        if (kids[(1 - bt) * 2 + rl]) best = nearest(x, y, best, kids[(1 - bt) * 2 + rl]);
        if (kids[(1 - bt) * 2 + (1 - rl)]) best = nearest(x, y, best, kids[(1 - bt) * 2 + (1 - rl)]);

        return best;
    }


    return canvas;

}