<commit_before>import { TextureLoader, Box2, Vector2, Color } from '../Three';

import {TerrainOverlayMaterial } from '../materials/TerrainOverlayMaterial';
import proj4 from 'proj4';
import { TERRAIN_BLEND } from '../core/constants';

function Overlay ( ctx, overlayProvider ) {

	this.provider = overlayProvider;
	this.active = false;
	this.hasCoverage = false;
	this.crsSupported = overlayProvider.crsSupported === undefined ? [ 'EPSG:3857', 'EPSG:4326', 'ORIGINAL' ] : overlayProvider.crsSupported;
	this.throughMode = TERRAIN_BLEND;
	this.ctx = ctx;

	const attribution = overlayProvider.getAttribution();

	if ( attribution ) {

		const c = new Color( ctx.cfg.themeValue( 'background' ) );
		const hsl = { h: 0, s: 0, l: 0 };

		c.getHSL( hsl );

		attribution.classList.add( 'overlay-branding' );
		attribution.style.color = hsl.l < 0.5 ? 'white' : 'black';

		this.attribution = attribution;

	}

	this.materialCache = {};
	this.missing = new Set();

	const coverage = overlayProvider.coverage;

	if ( coverage !== undefined ) {

		this.coverage = new Box2(
			new Vector2( coverage.minX, coverage.minY ),
			new Vector2( coverage.maxX, coverage.maxY )
		);

	}

}

Overlay.prototype.getMinZoom = function () {

	return this.provider.minZoom;

};

Overlay.prototype.checkCoverage = function ( limits, displayCRS, surveyCRS ) {

	const coverage = this.coverage;

	if ( this.crsSupported.indexOf( displayCRS ) === -1 ) return false;

	// transform survey limits to wgs84 for comparison with overlay limits

	const transform = proj4( ( displayCRS === 'ORIGINAL' ? surveyCRS : displayCRS ), 'WGS84' );
	const wgs84Limits = new Box2();

	wgs84Limits.expandByPoint( transform.forward( { x: limits.min.x, y: limits.min.y } ) );
	wgs84Limits.expandByPoint( transform.forward( { x: limits.min.x, y: limits.max.y } ) );
	wgs84Limits.expandByPoint( transform.forward( { x: limits.max.x, y: limits.min.y } ) );
	wgs84Limits.expandByPoint( transform.forward( { x: limits.max.x, y: limits.max.y } ) );

	this.provider.crs = displayCRS;
	this.hasCoverage = ( coverage === undefined ) ? true : coverage.intersectsBox( wgs84Limits );

	return this.hasCoverage;

};

Overlay.prototype.showAttribution = function () {

	const attribution = this.attribution;

	if ( attribution !== undefined ) this.ctx.container.appendChild( attribution );

};

Overlay.prototype.hideAttribution = function () {

	const attribution = this.attribution;
	const parent = attribution.parentNode;

	if ( parent !== null ) parent.removeChild( attribution );

};

Overlay.prototype.getTile = function ( x, y, z, overlayLoaded ) {

	const self = this;
	const key = x + ':' + y + ':' + z;
	const cfg = this.ctx.cfg;
	const materials = this.ctx.materials;

	const material = this.materialCache[ key ];
	const overlayMaxZoom = this.provider.maxZoom;

	var repeat = 1;
	var xOffset = 0;
	var yOffset = 0;

	if ( material !== undefined ) {

		overlayLoaded( this.active ? material : null );

		return;

	}

	const zoomDelta = z - overlayMaxZoom;

	if ( zoomDelta > 0 ) {

		const scale = Math.pow( 2, zoomDelta );

		repeat = 1 / scale;

		// get image for lower zoom
		const newX = Math.floor( x * repeat );
		const newY = Math.floor( y * repeat );

		xOffset = ( x - newX * scale ) / scale;
		yOffset = 1 - ( y - newY * scale ) / scale;
		yOffset -= repeat;

		x = newX;
		y = newY;
		z = overlayMaxZoom;

	}

	const url = this.provider.getUrl( x, y, z );

	if ( url === null || this.missing.has( url ) ) {

		overlayLoaded( materials.getMissingMaterial() );

		return;

	}

	new TextureLoader().setCrossOrigin( 'anonymous' ).load( url, _textureLoaded, undefined, _textureMissing );

	return;

	function _textureLoaded( texture ) {

		if ( ! self.active ) {

			texture.dispose();

			overlayLoaded( null );
			return;

		}
		const material = new TerrainOverlayMaterial( self.ctx );

		texture.anisotropy = cfg.value( 'anisotropy', 4 );

		texture.repeat.setScalar( repeat );
		texture.offset.set( xOffset, yOffset );

		material.map = texture;
		material.needsUpdate = true;

		self.materialCache[ key ] = material;

		overlayLoaded( material );

	}

	function _textureMissing( /* texture */ ) {

		self.missing.add( url );

		overlayLoaded( self.active ? materials.getMissingMaterial() : null );

	}

};

Overlay.prototype.setActive = function () {

	this.showAttribution();
	this.active = true;

};

Overlay.prototype.setInactive = function () {

	// flush cache

	const materialCache = this.materialCache;

	for ( var name in materialCache ) {

		let material = materialCache[ name ];

		material.map.dispose();
		material.dispose();

	}

	this.materialCache = {};

	this.hideAttribution();
	this.active = false;

};

Overlay.prototype.constructor = Overlay;

export { Overlay };<commit_msg>whitespace<commit_after>import { TextureLoader, Box2, Vector2, Color } from '../Three';

import {TerrainOverlayMaterial } from '../materials/TerrainOverlayMaterial';
import proj4 from 'proj4';
import { TERRAIN_BLEND } from '../core/constants';

function Overlay ( ctx, overlayProvider ) {

	this.provider = overlayProvider;
	this.active = false;
	this.hasCoverage = false;
	this.crsSupported = overlayProvider.crsSupported === undefined ? [ 'EPSG:3857', 'EPSG:4326', 'ORIGINAL' ] : overlayProvider.crsSupported;
	this.throughMode = TERRAIN_BLEND;
	this.ctx = ctx;

	const attribution = overlayProvider.getAttribution();

	if ( attribution ) {

		const c = new Color( ctx.cfg.themeValue( 'background' ) );
		const hsl = { h: 0, s: 0, l: 0 };

		c.getHSL( hsl );

		attribution.classList.add( 'overlay-branding' );
		attribution.style.color = hsl.l < 0.5 ? 'white' : 'black';

		this.attribution = attribution;

	}

	this.materialCache = {};
	this.missing = new Set();

	const coverage = overlayProvider.coverage;

	if ( coverage !== undefined ) {

		this.coverage = new Box2(
			new Vector2( coverage.minX, coverage.minY ),
			new Vector2( coverage.maxX, coverage.maxY )
		);

	}

}

Overlay.prototype.getMinZoom = function () {

	return this.provider.minZoom;

};

Overlay.prototype.checkCoverage = function ( limits, displayCRS, surveyCRS ) {

	const coverage = this.coverage;

	if ( this.crsSupported.indexOf( displayCRS ) === -1 ) return false;

	// transform survey limits to wgs84 for comparison with overlay limits

	const transform = proj4( ( displayCRS === 'ORIGINAL' ? surveyCRS : displayCRS ), 'WGS84' );
	const wgs84Limits = new Box2();

	wgs84Limits.expandByPoint( transform.forward( { x: limits.min.x, y: limits.min.y } ) );
	wgs84Limits.expandByPoint( transform.forward( { x: limits.min.x, y: limits.max.y } ) );
	wgs84Limits.expandByPoint( transform.forward( { x: limits.max.x, y: limits.min.y } ) );
	wgs84Limits.expandByPoint( transform.forward( { x: limits.max.x, y: limits.max.y } ) );

	this.provider.crs = displayCRS;
	this.hasCoverage = ( coverage === undefined ) ? true : coverage.intersectsBox( wgs84Limits );

	return this.hasCoverage;

};

Overlay.prototype.showAttribution = function () {

	const attribution = this.attribution;

	if ( attribution !== undefined ) this.ctx.container.appendChild( attribution );

};

Overlay.prototype.hideAttribution = function () {

	const attribution = this.attribution;
	const parent = attribution.parentNode;

	if ( parent !== null ) parent.removeChild( attribution );

};

Overlay.prototype.getTile = function ( x, y, z, overlayLoaded ) {

	const self = this;
	const key = x + ':' + y + ':' + z;
	const cfg = this.ctx.cfg;
	const materials = this.ctx.materials;

	const material = this.materialCache[ key ];
	const overlayMaxZoom = this.provider.maxZoom;

	var repeat = 1;
	var xOffset = 0;
	var yOffset = 0;

	if ( material !== undefined ) {

		overlayLoaded( this.active ? material : null );

		return;

	}

	const zoomDelta = z - overlayMaxZoom;

	if ( zoomDelta > 0 ) {

		const scale = Math.pow( 2, zoomDelta );

		repeat = 1 / scale;

		// get image for lower zoom
		const newX = Math.floor( x * repeat );
		const newY = Math.floor( y * repeat );

		xOffset = ( x - newX * scale ) / scale;
		yOffset = 1 - ( y - newY * scale ) / scale;
		yOffset -= repeat;

		x = newX;
		y = newY;
		z = overlayMaxZoom;

	}

	const url = this.provider.getUrl( x, y, z );

	if ( url === null || this.missing.has( url ) ) {

		overlayLoaded( materials.getMissingMaterial() );

		return;

	}

	new TextureLoader().setCrossOrigin( 'anonymous' ).load( url, _textureLoaded, undefined, _textureMissing );

	return;

	function _textureLoaded( texture ) {

		if ( ! self.active ) {

			texture.dispose();

			overlayLoaded( null );
			return;

		}

		const material = new TerrainOverlayMaterial( self.ctx );

		texture.anisotropy = cfg.value( 'anisotropy', 4 );

		texture.repeat.setScalar( repeat );
		texture.offset.set( xOffset, yOffset );

		material.map = texture;
		material.needsUpdate = true;

		self.materialCache[ key ] = material;

		overlayLoaded( material );

	}

	function _textureMissing( /* texture */ ) {

		self.missing.add( url );

		overlayLoaded( self.active ? materials.getMissingMaterial() : null );

	}

};

Overlay.prototype.setActive = function () {

	this.showAttribution();
	this.active = true;

};

Overlay.prototype.setInactive = function () {

	// flush cache

	const materialCache = this.materialCache;

	for ( var name in materialCache ) {

		let material = materialCache[ name ];

		material.map.dispose();
		material.dispose();

	}

	this.materialCache = {};

	this.hideAttribution();
	this.active = false;

};

Overlay.prototype.constructor = Overlay;

export { Overlay };<|endoftext|>


<commit_before>T5.TileGrid = function(params) {
    // extend the params with the defaults
    params = T5.ex({
        tileSize: T5.tileSize,
        drawGrid: false,
        center: new T5.Vector(),
        gridSize: 25,
        shiftOrigin: null,
        supportFastDraw: true
    }, params);

    // initialise tile store related information
    var storage = new Array(Math.pow(params.gridSize, 2)),
        gridSize = params.gridSize,
        tileSize = params.tileSize,
        gridHalfWidth = Math.ceil(params.gridSize >> 1),
        topLeftOffset = T5.V.offset(params.center, -gridHalfWidth),
        lastTileCreator = null,
        tileShift = new T5.Vector(),
        lastNotifyListener = null;

    // initialise varibles
    var halfTileSize = Math.round(params.tileSize / 2),
        invTileSize = params.tileSize ? 1 / params.tileSize : 0,
        active = true,
        tileDrawQueue = null,
        loadedTileCount = 0,
        lastTilesDrawn = false,
        lastCheckOffset = new T5.Vector(),
        shiftDelta = new T5.Vector(),
        repaintDistance = T5.getConfig().repaintDistance,
        reloadTimeout = 0,
        gridHeightWidth = gridSize * params.tileSize,
        tileCols, tileRows, centerPos;

    /* internal functions */

    function copyStorage(dst, src, delta) {
        // set the length of the destination to match the source
        dst.length = src.length;

        for (var xx = 0; xx < params.gridSize; xx++) {
            for (var yy = 0; yy < params.gridSize; yy++) {
                dst[getTileIndex(xx, yy)] = getTile(xx + delta.x, yy + delta.y);
            } // for
        } // for
    } // copyStorage

    function createTempTile(col, row) {
        var gridXY = getGridXY(col, row);
        return new T5.ImageTile({
            gridX: gridXY.x,
            gridY: gridXY.y
        });
    } // createTempTile

    function findTile(matcher) {
        if (! matcher) { return null; }

        for (var ii = storage.length; ii--; ) {
            var tile = storage[ii];
            if (tile && matcher(tile)) {
                return tile;
            } // if
        } // for

        return null;
    } // findTile

    function getGridXY(col, row) {
        return T5.Vector(
            col * tileSize - tileShift.x,
            row * tileSize - tileShift.y);
    } // getGridXY

    function getNormalizedPos(col, row) {
        return T5.V.add(new T5.Vector(col, row), T5.V.invert(topLeftOffset), tileShift);
    } // getNormalizedPos

    function getShiftDelta(topLeftX, topLeftY, cols, rows) {
        // initialise variables
        var shiftAmount = Math.floor(params.gridSize * 0.2),
            shiftDelta = new T5.Vector();

        // test the x
        if (topLeftX < 0 || topLeftX + cols > params.gridSize) {
            shiftDelta.x = topLeftX < 0 ? -shiftAmount : shiftAmount;
        } // if

        // test the y
        if (topLeftY < 0 || topLeftY + rows > params.gridSize) {
            shiftDelta.y = topLeftY < 0 ? -shiftAmount : shiftAmount;
        } // if

        return shiftDelta;
    } // getShiftDelta

    function getTile(col, row) {
        return (col >= 0 && col < params.gridSize) ? storage[getTileIndex(col, row)] : null;
    } // getTile

    function setTile(col, row, tile) {
        storage[getTileIndex(col, row)] = tile;
    } // setTile

    function getTileIndex(col, row) {
        return (row * params.gridSize) + col;
    } // getTileIndex

    /*
    What a cool function this is.  Basically, this goes through the tile
    grid and creates each of the tiles required at that position of the grid.
    The tileCreator is a callback function that takes a two parameters (col, row) and
    can do whatever it likes but should return a Tile object or null for the specified
    column and row.
    */
    function populate(tileCreator, notifyListener, resetStorage) {
        // take a tick count as we want to time this
        var startTicks = GT.Log.getTraceTicks(),
            tileIndex = 0,
            gridSize = params.gridSize,
            tileSize = params.tileSize,
            centerPos = new T5.Vector(gridSize / 2, gridSize / 2);

        // if the storage is to be reset, then do that now
        if (resetStorage) {
            storage = [];
        } // if

        if (tileCreator) {
            // GT.Log.info("populating grid, x shift = " + tileShift.x + ", y shift = " + tileShift.y);

            for (var row = 0; row < gridSize; row++) {
                for (var col = 0; col < gridSize; col++) {
                    if (! storage[tileIndex]) {
                        var tile = tileCreator(col, row, topLeftOffset, gridSize);

                        // set the tile grid x and grid y position
                        tile.gridX = (col * tileSize) - tileShift.x;
                        tile.gridY = (row * tileSize) - tileShift.y;

                        // add the tile to storage
                        storage[tileIndex] = tile;
                    } // if

                    // increment the tile index
                    tileIndex++;
                } // for
            } // for
        } // if

        // save the last tile creator
        lastTileCreator = tileCreator;
        lastNotifyListener = notifyListener;

        // log how long it took
        GT.Log.trace("tile grid populated", startTicks);

        // if we have an onpopulate listener defined, let them know
        self.dirty = true;
        self.wakeParent();
    } // populate

    function shift(shiftDelta, shiftOriginCallback) {
        // if the shift delta x and the shift delta y are both 0, then return
        if ((shiftDelta.x === 0) && (shiftDelta.y === 0)) { return; }

        var ii, startTicks = GT.Log.getTraceTicks();
        // GT.Log.info("need to shift tile store grid, " + shiftDelta.x + " cols and " + shiftDelta.y + " rows.");

        // create new storage
        var newStorage = Array(storage.length);

        // copy the storage from given the various offsets
        copyStorage(newStorage, storage, shiftDelta);

        // update the storage and top left offset
        storage = newStorage;

        // TODO: check whether this is right or not
        if (shiftOriginCallback) {
            topLeftOffset = shiftOriginCallback(topLeftOffset, shiftDelta);
        }
        else {
            topLeftOffset = T5.V.add(topLeftOffset, shiftDelta);
        } // if..else

        // create the tile shift offset
        tileShift.x += (-shiftDelta.x * params.tileSize);
        tileShift.y += (-shiftDelta.y * params.tileSize);
        GT.Log.trace("tile storage shifted", startTicks);

        // populate with the last tile creator (crazy talk)
        populate(lastTileCreator, lastNotifyListener);
    } // shift

    function updateDrawQueue(offset, state) {
        if (! centerPos) { return; }

        var tile, tmpQueue = [],
            tileStart = new T5.Vector(
                            Math.floor((offset.x + tileShift.x) * invTileSize),
                            Math.floor((offset.y + tileShift.y) * invTileSize));

        // reset the tile draw queue
        tilesNeeded = false;

        // right, let's draw some tiles (draw rows first)
        for (var yy = tileRows; yy--; ) {
            // iterate through the columns and draw the tiles
            for (var xx = tileCols; xx--; ) {
                // get the tile
                tile = getTile(xx + tileStart.x, yy + tileStart.y);
                var centerDiff = new T5.Vector(xx - centerPos.x, yy - centerPos.y);

                if (! tile) {
                    shiftDelta = getShiftDelta(tileStart.x, tileStart.y, tileCols, tileRows);

                    // TODO: replace the tile with a temporary draw tile here
                    tile = createTempTile(xx + tileStart.x, yy + tileStart.y);
                } // if

                // add the tile and position to the tile draw queue
                tmpQueue[tmpQueue.length] = {
                    tile: tile,
                    centerness: T5.V.absSize(centerDiff)
                };
            } // for
        } // for

        // sort the tile queue by "centerness"
        tmpQueue.sort(function(itemA, itemB) {
            return itemB.centerness - itemA.centerness;
        });

        if (! tileDrawQueue) {
            tileDrawQueue = new Array(tmpQueue.length);
        } // if

        // copy the temporary queue item to the draw queue
        for (var ii = tmpQueue.length; ii--; ) {
            tileDrawQueue[ii] = tmpQueue[ii].tile;
            self.prepTile(tileDrawQueue[ii], state);
        } // for
    } // updateDrawQueue

    /* external object definition */

    // initialise self
    var self = T5.ex(new T5.ViewLayer(params), {
        gridDimensions: new T5.Dimensions(gridHeightWidth, gridHeightWidth),
        dirty: false,

        cycle: function(tickCount, offset, state) {
            var needTiles = shiftDelta.x !== 0 || shiftDelta.y !== 0,
                changeCount = 0;

            if (needTiles) {
                shift(shiftDelta, params.shiftOrigin);

                // reset the delta
                shiftDelta = new T5.Vector();

                // things need to happen
                changeCount++;
            } // if

            if (state !== T5.viewState('PINCH')) {
                updateDrawQueue(offset, state);
            } // if

            // if the grid is dirty let the calling view know
            return changeCount + self.dirty ? 1 : 0;
        },

        deactivate: function() {
            active = false;
        },

        find: findTile,

        prepTile: function(tile, state) {
        },

        drawTile: function(context, tile, x, y, state) {
            return false;
        },

        // TODO: convert to a configurable implementation
        getTileSize: function() {
            return params.tileSize;
        },

        draw: function(context, offset, dimensions, state, view) {
            if (! active) { return; }

            // initialise variables
            var startTicks = T5.time(),
                xShift = offset.x,
                yShift = offset.y,
                tilesDrawn = true,
                redraw = (state === T5.viewState('PAN')) || (state === T5.viewState('PINCH')) || T5.isTweening();

            if (! centerPos) {
                tileCols = Math.ceil(dimensions.width * invTileSize) + 1;
                tileRows = Math.ceil(dimensions.height * invTileSize) + 1;
                centerPos = new T5.Vector(Math.floor((tileCols-1) / 2), Math.floor((tileRows-1) / 2));
            } // if

            // if we don't have a draq queue return
            if (! tileDrawQueue) { return; }

            // set the context stroke style for the border
            if (params.drawGrid) {
                context.strokeStyle = "rgba(50, 50, 50, 0.3)";
            } // if

            // begin the path for the tile borders
            context.beginPath();

            // iterate through the tiles in the draw queue
            for (var ii = tileDrawQueue.length; ii--; ) {
                var tile = tileDrawQueue[ii];

                // if the tile is loaded, then draw, otherwise load
                if (tile) {
                    var x = tile.gridX - xShift,
                        y = tile.gridY - yShift,
                        drawn = redraw ? false : (! tile.dirty);

                    // draw the tile
                    if (! drawn) {
                        tilesDrawn =  self.drawTile(context, tile, x, y, state) && tilesDrawn;

                        tile.x = x;
                        tile.y = y;
                    } // if
                }
                else {
                    tilesDrawn = false;
                } // if..else

                // if we are drawing borders, then draw that now
                if (params.drawGrid) {
                    context.rect(x, y, params.tileSize, params.tileSize);
                } // if
            } // for

            // draw the borders if we have them...
            context.stroke();
            GT.Log.trace("drawn tiles", startTicks);

            // if the tiles have been drawn and previously haven't then fire the tiles drawn event
            if (tilesDrawn && (! lastTilesDrawn)) {
                view.trigger("tileDrawComplete");
            } // if

            // flag the grid as not dirty
            lastTilesDrawn = tilesDrawn;
            self.dirty = false;
        },

        getTileAtXY: function(x, y) {
            var queueLength = tileDrawQueue ? tileDrawQueue.length : 0,
                tileSize = params.tileSize;

            for (var ii = queueLength; ii--; ) {
                var tile = tileDrawQueue[ii];

                if (tile && (x >= tile.x) && (y >= tile.y)) {
                    if ((x <= tile.x + tileSize) && (y <= tile.y + tileSize)) {
                        return tile;
                    } // if
                } // if
            } // for

            return null;
        },

        getTileVirtualXY: function(col, row, getCenter) {
            // get the normalized position from the tile store
            var pos = getNormalizedPos(col, row),
                fnresult = new T5.Vector(pos.x * params.tileSize, pos.y * params.tileSize);

            if (getCenter) {
                fnresult.x += halfTileSize;
                fnresult.y += halfTileSize;
            } // if

            return fnresult;
        },

        populate: function(tileCreator) {
            populate(tileCreator, null, true);
        }
    });

    GT.listen("imagecache.cleared", function(args) {
        // reset all the tiles loaded state
        for (var ii = storage.length; ii--; ) {
            if (storage[ii]) {
                storage[ii].loaded = false;
            } // if
        } // for
    });

    GT.listen("tiler.repaint", function(args) {
        for (var ii = storage.length; ii--; ) {
            if (storage[ii]) {
                storage[ii].x = null;
                storage[ii].y = null;
            } // if
        } // for
    });

    return self;
}; // T5.TileGrid

/*

(function() {
    TileStore = function(params) {

        // initialise self
        var self = {
            setOrigin: function(col, row) {
                if (! tileOrigin) {
                    topLeftOffset = T5.V.offset(new T5.Vector(col, row), -tileHalfWidth);
                }
                else {
                    shiftOrigin(col, row);
                } // if..else
            }
        };

        return self;
    };
})();

*/
<commit_msg>Added parameter to selectively allow shifting to be disabled<commit_after>T5.TileGrid = function(params) {
    // extend the params with the defaults
    params = T5.ex({
        tileSize: T5.tileSize,
        drawGrid: false,
        center: new T5.Vector(),
        gridSize: 25,
        shiftOrigin: null,
        supportFastDraw: true,
        allowShift: true
    }, params);

    // initialise tile store related information
    var storage = new Array(Math.pow(params.gridSize, 2)),
        gridSize = params.gridSize,
        tileSize = params.tileSize,
        gridHalfWidth = Math.ceil(params.gridSize >> 1),
        topLeftOffset = T5.V.offset(params.center, -gridHalfWidth),
        lastTileCreator = null,
        tileShift = new T5.Vector(),
        lastNotifyListener = null;

    // initialise varibles
    var halfTileSize = Math.round(params.tileSize / 2),
        invTileSize = params.tileSize ? 1 / params.tileSize : 0,
        active = true,
        tileDrawQueue = null,
        loadedTileCount = 0,
        lastTilesDrawn = false,
        lastCheckOffset = new T5.Vector(),
        shiftDelta = new T5.Vector(),
        repaintDistance = T5.getConfig().repaintDistance,
        reloadTimeout = 0,
        gridHeightWidth = gridSize * params.tileSize,
        tileCols, tileRows, centerPos;

    /* internal functions */

    function copyStorage(dst, src, delta) {
        // set the length of the destination to match the source
        dst.length = src.length;

        for (var xx = 0; xx < params.gridSize; xx++) {
            for (var yy = 0; yy < params.gridSize; yy++) {
                dst[getTileIndex(xx, yy)] = getTile(xx + delta.x, yy + delta.y);
            } // for
        } // for
    } // copyStorage

    function createTempTile(col, row) {
        var gridXY = getGridXY(col, row);
        return new T5.ImageTile({
            gridX: gridXY.x,
            gridY: gridXY.y
        });
    } // createTempTile

    function findTile(matcher) {
        if (! matcher) { return null; }

        for (var ii = storage.length; ii--; ) {
            var tile = storage[ii];
            if (tile && matcher(tile)) {
                return tile;
            } // if
        } // for

        return null;
    } // findTile

    function getGridXY(col, row) {
        return T5.Vector(
            col * tileSize - tileShift.x,
            row * tileSize - tileShift.y);
    } // getGridXY

    function getNormalizedPos(col, row) {
        return T5.V.add(new T5.Vector(col, row), T5.V.invert(topLeftOffset), tileShift);
    } // getNormalizedPos

    function getShiftDelta(topLeftX, topLeftY, cols, rows) {
        // initialise variables
        var shiftAmount = Math.floor(params.gridSize * 0.2),
            shiftDelta = new T5.Vector();

        // test the x
        if (topLeftX < 0 || topLeftX + cols > params.gridSize) {
            shiftDelta.x = topLeftX < 0 ? -shiftAmount : shiftAmount;
        } // if

        // test the y
        if (topLeftY < 0 || topLeftY + rows > params.gridSize) {
            shiftDelta.y = topLeftY < 0 ? -shiftAmount : shiftAmount;
        } // if

        return shiftDelta;
    } // getShiftDelta

    function getTile(col, row) {
        return (col >= 0 && col < params.gridSize) ? storage[getTileIndex(col, row)] : null;
    } // getTile

    function setTile(col, row, tile) {
        storage[getTileIndex(col, row)] = tile;
    } // setTile

    function getTileIndex(col, row) {
        return (row * params.gridSize) + col;
    } // getTileIndex

    /*
    What a cool function this is.  Basically, this goes through the tile
    grid and creates each of the tiles required at that position of the grid.
    The tileCreator is a callback function that takes a two parameters (col, row) and
    can do whatever it likes but should return a Tile object or null for the specified
    column and row.
    */
    function populate(tileCreator, notifyListener, resetStorage) {
        // take a tick count as we want to time this
        var startTicks = GT.Log.getTraceTicks(),
            tileIndex = 0,
            gridSize = params.gridSize,
            tileSize = params.tileSize,
            centerPos = new T5.Vector(gridSize / 2, gridSize / 2);

        // if the storage is to be reset, then do that now
        if (resetStorage) {
            storage = [];
        } // if

        if (tileCreator) {
            // GT.Log.info("populating grid, x shift = " + tileShift.x + ", y shift = " + tileShift.y);

            for (var row = 0; row < gridSize; row++) {
                for (var col = 0; col < gridSize; col++) {
                    if (! storage[tileIndex]) {
                        var tile = tileCreator(col, row, topLeftOffset, gridSize);

                        // set the tile grid x and grid y position
                        tile.gridX = (col * tileSize) - tileShift.x;
                        tile.gridY = (row * tileSize) - tileShift.y;

                        // add the tile to storage
                        storage[tileIndex] = tile;
                    } // if

                    // increment the tile index
                    tileIndex++;
                } // for
            } // for
        } // if

        // save the last tile creator
        lastTileCreator = tileCreator;
        lastNotifyListener = notifyListener;

        // log how long it took
        GT.Log.trace("tile grid populated", startTicks);

        // if we have an onpopulate listener defined, let them know
        self.dirty = true;
        self.wakeParent();
    } // populate

    function shift(shiftDelta, shiftOriginCallback) {
        // if the shift delta x and the shift delta y are both 0, then return
        if ((! params.allowShift) || ((shiftDelta.x === 0) && (shiftDelta.y === 0))) { return; }

        var ii, startTicks = GT.Log.getTraceTicks();
        // GT.Log.info("need to shift tile store grid, " + shiftDelta.x + " cols and " + shiftDelta.y + " rows.");

        // create new storage
        var newStorage = Array(storage.length);

        // copy the storage from given the various offsets
        copyStorage(newStorage, storage, shiftDelta);

        // update the storage and top left offset
        storage = newStorage;

        // TODO: check whether this is right or not
        if (shiftOriginCallback) {
            topLeftOffset = shiftOriginCallback(topLeftOffset, shiftDelta);
        }
        else {
            topLeftOffset = T5.V.add(topLeftOffset, shiftDelta);
        } // if..else

        // create the tile shift offset
        tileShift.x += (-shiftDelta.x * params.tileSize);
        tileShift.y += (-shiftDelta.y * params.tileSize);
        GT.Log.trace("tile storage shifted", startTicks);

        // populate with the last tile creator (crazy talk)
        populate(lastTileCreator, lastNotifyListener);
    } // shift

    function updateDrawQueue(offset, state) {
        if (! centerPos) { return; }

        var tile, tmpQueue = [],
            tileStart = new T5.Vector(
                            Math.floor((offset.x + tileShift.x) * invTileSize),
                            Math.floor((offset.y + tileShift.y) * invTileSize));

        // reset the tile draw queue
        tilesNeeded = false;

        // right, let's draw some tiles (draw rows first)
        for (var yy = tileRows; yy--; ) {
            // iterate through the columns and draw the tiles
            for (var xx = tileCols; xx--; ) {
                // get the tile
                tile = getTile(xx + tileStart.x, yy + tileStart.y);
                var centerDiff = new T5.Vector(xx - centerPos.x, yy - centerPos.y);

                if (! tile) {
                    shiftDelta = getShiftDelta(tileStart.x, tileStart.y, tileCols, tileRows);

                    // TODO: replace the tile with a temporary draw tile here
                    tile = createTempTile(xx + tileStart.x, yy + tileStart.y);
                } // if

                // add the tile and position to the tile draw queue
                tmpQueue[tmpQueue.length] = {
                    tile: tile,
                    centerness: T5.V.absSize(centerDiff)
                };
            } // for
        } // for

        // sort the tile queue by "centerness"
        tmpQueue.sort(function(itemA, itemB) {
            return itemB.centerness - itemA.centerness;
        });

        if (! tileDrawQueue) {
            tileDrawQueue = new Array(tmpQueue.length);
        } // if

        // copy the temporary queue item to the draw queue
        for (var ii = tmpQueue.length; ii--; ) {
            tileDrawQueue[ii] = tmpQueue[ii].tile;
            self.prepTile(tileDrawQueue[ii], state);
        } // for
    } // updateDrawQueue

    /* external object definition */

    // initialise self
    var self = T5.ex(new T5.ViewLayer(params), {
        gridDimensions: new T5.Dimensions(gridHeightWidth, gridHeightWidth),
        dirty: false,

        cycle: function(tickCount, offset, state) {
            var needTiles = shiftDelta.x !== 0 || shiftDelta.y !== 0,
                changeCount = 0;

            if (needTiles) {
                shift(shiftDelta, params.shiftOrigin);

                // reset the delta
                shiftDelta = new T5.Vector();

                // things need to happen
                changeCount++;
            } // if

            if (state !== T5.viewState('PINCH')) {
                updateDrawQueue(offset, state);
            } // if

            // if the grid is dirty let the calling view know
            return changeCount + self.dirty ? 1 : 0;
        },

        deactivate: function() {
            active = false;
        },

        find: findTile,

        prepTile: function(tile, state) {
        },

        drawTile: function(context, tile, x, y, state) {
            return false;
        },

        // TODO: convert to a configurable implementation
        getTileSize: function() {
            return params.tileSize;
        },

        draw: function(context, offset, dimensions, state, view) {
            if (! active) { return; }

            // initialise variables
            var startTicks = T5.time(),
                xShift = offset.x,
                yShift = offset.y,
                tilesDrawn = true,
                redraw = (state === T5.viewState('PAN')) || (state === T5.viewState('PINCH')) || T5.isTweening();

            if (! centerPos) {
                tileCols = Math.ceil(dimensions.width * invTileSize) + 1;
                tileRows = Math.ceil(dimensions.height * invTileSize) + 1;
                centerPos = new T5.Vector(Math.floor((tileCols-1) / 2), Math.floor((tileRows-1) / 2));
            } // if

            // if we don't have a draq queue return
            if (! tileDrawQueue) { return; }

            // set the context stroke style for the border
            if (params.drawGrid) {
                context.strokeStyle = "rgba(50, 50, 50, 0.3)";
            } // if

            // begin the path for the tile borders
            context.beginPath();

            // iterate through the tiles in the draw queue
            for (var ii = tileDrawQueue.length; ii--; ) {
                var tile = tileDrawQueue[ii];

                // if the tile is loaded, then draw, otherwise load
                if (tile) {
                    var x = tile.gridX - xShift,
                        y = tile.gridY - yShift,
                        drawn = redraw ? false : (! tile.dirty);

                    // draw the tile
                    if (! drawn) {
                        tilesDrawn =  self.drawTile(context, tile, x, y, state) && tilesDrawn;

                        tile.x = x;
                        tile.y = y;
                    } // if
                }
                else {
                    tilesDrawn = false;
                } // if..else

                // if we are drawing borders, then draw that now
                if (params.drawGrid) {
                    context.rect(x, y, params.tileSize, params.tileSize);
                } // if
            } // for

            // draw the borders if we have them...
            context.stroke();
            GT.Log.trace("drawn tiles", startTicks);

            // if the tiles have been drawn and previously haven't then fire the tiles drawn event
            if (tilesDrawn && (! lastTilesDrawn)) {
                view.trigger("tileDrawComplete");
            } // if

            // flag the grid as not dirty
            lastTilesDrawn = tilesDrawn;
            self.dirty = false;
        },

        getTileAtXY: function(x, y) {
            var queueLength = tileDrawQueue ? tileDrawQueue.length : 0,
                tileSize = params.tileSize;

            for (var ii = queueLength; ii--; ) {
                var tile = tileDrawQueue[ii];

                if (tile && (x >= tile.x) && (y >= tile.y)) {
                    if ((x <= tile.x + tileSize) && (y <= tile.y + tileSize)) {
                        return tile;
                    } // if
                } // if
            } // for

            return null;
        },

        getTileVirtualXY: function(col, row, getCenter) {
            // get the normalized position from the tile store
            var pos = getNormalizedPos(col, row),
                fnresult = new T5.Vector(pos.x * params.tileSize, pos.y * params.tileSize);

            if (getCenter) {
                fnresult.x += halfTileSize;
                fnresult.y += halfTileSize;
            } // if

            return fnresult;
        },

        populate: function(tileCreator) {
            populate(tileCreator, null, true);
        }
    });

    GT.listen("imagecache.cleared", function(args) {
        // reset all the tiles loaded state
        for (var ii = storage.length; ii--; ) {
            if (storage[ii]) {
                storage[ii].loaded = false;
            } // if
        } // for
    });

    GT.listen("tiler.repaint", function(args) {
        for (var ii = storage.length; ii--; ) {
            if (storage[ii]) {
                storage[ii].x = null;
                storage[ii].y = null;
            } // if
        } // for
    });

    return self;
}; // T5.TileGrid

/*

(function() {
    TileStore = function(params) {

        // initialise self
        var self = {
            setOrigin: function(col, row) {
                if (! tileOrigin) {
                    topLeftOffset = T5.V.offset(new T5.Vector(col, row), -tileHalfWidth);
                }
                else {
                    shiftOrigin(col, row);
                } // if..else
            }
        };

        return self;
    };
})();

*/
<|endoftext|>


<commit_before>import {
	BufferGeometry,
	Points,
	Float32BufferAttribute,
} from '../Three';

import { ExtendedPointsMaterial } from '../materials/ExtendedPointsMaterial';

import { STATION_ENTRANCE } from '../core/constants';
import { Viewer } from '../viewer/Viewer';
import { Cfg } from '../core/lib';
import { PointIndicator } from './PointIndicator';

function onUploadDropBuffer() {

	// call back from BufferAttribute to drop JS buffers after data has been transfered to GPU
	this.array = null;

}

function Stations () {

	Points.call( this, new BufferGeometry, new ExtendedPointsMaterial() );

	this.type = 'CV.Stations';
	this.map = new Map();
	this.stationCount = 0;

	this.baseColor     = Cfg.themeColor( 'stations.default.marker' );
	this.junctionColor = Cfg.themeColor( 'stations.junctions.marker' );

	this.pointSizes = [];
	this.vertices   = [];
	this.colors     = [];

	this.stations = [];

	this.selected = null;
	this.selectedSize = 0;

	const self = this;

	Viewer.addEventListener( 'change', _viewChanged );

	this.addEventListener( 'removed', _removed );

	const point = new PointIndicator();

	point.visible = false;

	this.addStatic( point );
	this.highlightPoint = point;

	function _viewChanged( event ) {

		if ( event.name === 'splays' ) {

			const splaySize = Viewer.splays ? 6.0 : 0.0;

			const stations = self.stations;
			const pSize = self.geometry.getAttribute( 'pSize' );
			const l = stations.length;

			var i;

			for ( i = 0; i < l; i++ ) {

				if ( stations[ i ].p.connections === 0 ) {

					pSize.setX( i, splaySize );

				}

			}

			pSize.needsUpdate = true;
			Viewer.renderView();

		}

	}

	function _removed ( ) {

		Viewer.removeEventListener( 'change', _viewChanged );

	}

}

Stations.prototype = Object.create ( Points.prototype );

Stations.prototype.addStation = function ( node ) {

	const point = node.p;

	const seen = this.map.get( point );

	if ( seen !== undefined ) {

		// console.log( 'duplicate', node.getPath(), seen.getPath() );
		return;

	}

	const connections = point.connections;

	this.vertices.push( point );

	this.colors.push( connections > 2 ? this.junctionColor : this.baseColor );

	var pointSize = 0.0;

	if ( node.type === STATION_ENTRANCE ) {

		pointSize = 12.0;

	} else {

		pointSize = 8.0;

	}

	this.pointSizes.push( pointSize );

	this.map.set( point, node );
	this.stations.push( node );

	node.stationVertexIndex = this.stationCount++;
	node.linkedSegments = [];
	node.legs = [];
	node.distance = Infinity;

};

Stations.prototype.getStation = function ( vertex ) {

	return this.map.get( vertex );

};

Stations.prototype.getStationByIndex = function ( index ) {

	return this.stations[ index ];

};

Stations.prototype.clearSelected = function () {

	if ( this.selected !== null ) {

		const pSize = this.geometry.getAttribute( 'pSize' );

		pSize.setX( this.selected, this.selectedSize );
		pSize.needsUpdate = true;

		this.selected = null;

	}

};

Stations.prototype.highlightStation = function ( node ) {

	const highlightPoint = this.highlightPoint;

	highlightPoint.position.copy( node.p );
	highlightPoint.updateMatrix();

	highlightPoint.visible = true;

	return node;

};

Stations.prototype.clearHighlight = function () {

	this.highlightPoint.visible = false;

};

Stations.prototype.selectStation = function ( node ) {

	this.selectStationByIndex( node.stationVertexIndex );

	return node;

};

Stations.prototype.selectStationByIndex = function ( index ) {

	const pSize = this.geometry.getAttribute( 'pSize' );

	if ( this.selected !== null ) {

		pSize.setX( this.selected, this.selectedSize );

	}

	this.selectedSize = pSize.getX( index );

	pSize.setX( index, this.selectedSize * 2 );

	//	pSize.updateRange.offset = index;
	//	pSize.updateRange.count  = 1;

	pSize.needsUpdate = true;

	this.selected = index;

};

Stations.prototype.finalise = function () {

	const bufferGeometry = this.geometry;

	const positions = new Float32BufferAttribute(this.vertices.length * 3, 3 );
	const colors = new Float32BufferAttribute( this.colors.length * 3, 3 );

	bufferGeometry.addAttribute( 'pSize', new Float32BufferAttribute( this.pointSizes, 1 ) );
	bufferGeometry.addAttribute( 'position', positions.copyVector3sArray( this.vertices ) );
	bufferGeometry.addAttribute( 'color', colors.copyColorsArray( this.colors ) );

	bufferGeometry.getAttribute( 'color' ).onUpload( onUploadDropBuffer );

	this.pointSizes = null;
	this.colors     = null;

};

Stations.prototype.resetDistances = function () {

	this.stations.forEach( function _resetDistance( node ) { node.distance = Infinity; } );

};

export { Stations };<commit_msg>use new entrance station theme color<commit_after>import {
	BufferGeometry,
	Points,
	Float32BufferAttribute,
} from '../Three';

import { ExtendedPointsMaterial } from '../materials/ExtendedPointsMaterial';

import { STATION_ENTRANCE } from '../core/constants';
import { Viewer } from '../viewer/Viewer';
import { Cfg } from '../core/lib';
import { PointIndicator } from './PointIndicator';

function onUploadDropBuffer() {

	// call back from BufferAttribute to drop JS buffers after data has been transfered to GPU
	this.array = null;

}

function Stations () {

	Points.call( this, new BufferGeometry, new ExtendedPointsMaterial() );

	this.type = 'CV.Stations';
	this.map = new Map();
	this.stationCount = 0;

	this.baseColor     = Cfg.themeColor( 'stations.default.marker' );
	this.junctionColor = Cfg.themeColor( 'stations.junctions.marker' );
	this.entranceColor = Cfg.themeColor( 'stations.entrances.marker' );

	this.pointSizes = [];
	this.vertices   = [];
	this.colors     = [];

	this.stations = [];

	this.selected = null;
	this.selectedSize = 0;

	const self = this;

	Viewer.addEventListener( 'change', _viewChanged );

	this.addEventListener( 'removed', _removed );

	const point = new PointIndicator();

	point.visible = false;

	this.addStatic( point );
	this.highlightPoint = point;

	function _viewChanged( event ) {

		if ( event.name === 'splays' ) {

			const splaySize = Viewer.splays ? 6.0 : 0.0;

			const stations = self.stations;
			const pSize = self.geometry.getAttribute( 'pSize' );
			const l = stations.length;

			var i;

			for ( i = 0; i < l; i++ ) {

				if ( stations[ i ].p.connections === 0 ) {

					pSize.setX( i, splaySize );

				}

			}

			pSize.needsUpdate = true;
			Viewer.renderView();

		}

	}

	function _removed ( ) {

		Viewer.removeEventListener( 'change', _viewChanged );

	}

}

Stations.prototype = Object.create ( Points.prototype );

Stations.prototype.addStation = function ( node ) {

	const point = node.p;

	const seen = this.map.get( point );

	if ( seen !== undefined ) {

		// console.log( 'duplicate', node.getPath(), seen.getPath() );
		return;

	}

	const connections = point.connections;

	this.vertices.push( point );

	var pointSize = 0.0;

	if ( node.type === STATION_ENTRANCE ) {

		this.colors.push( this.entranceColor );

		pointSize = 12.0;

	} else {

		this.colors.push( connections > 2 ? this.junctionColor : this.baseColor );

		pointSize = 8.0;

	}

	this.pointSizes.push( pointSize );

	this.map.set( point, node );
	this.stations.push( node );

	node.stationVertexIndex = this.stationCount++;
	node.linkedSegments = [];
	node.legs = [];
	node.distance = Infinity;

};

Stations.prototype.getStation = function ( vertex ) {

	return this.map.get( vertex );

};

Stations.prototype.getStationByIndex = function ( index ) {

	return this.stations[ index ];

};

Stations.prototype.clearSelected = function () {

	if ( this.selected !== null ) {

		const pSize = this.geometry.getAttribute( 'pSize' );

		pSize.setX( this.selected, this.selectedSize );
		pSize.needsUpdate = true;

		this.selected = null;

	}

};

Stations.prototype.highlightStation = function ( node ) {

	const highlightPoint = this.highlightPoint;

	highlightPoint.position.copy( node.p );
	highlightPoint.updateMatrix();

	highlightPoint.visible = true;

	return node;

};

Stations.prototype.clearHighlight = function () {

	this.highlightPoint.visible = false;

};

Stations.prototype.selectStation = function ( node ) {

	this.selectStationByIndex( node.stationVertexIndex );

	return node;

};

Stations.prototype.selectStationByIndex = function ( index ) {

	const pSize = this.geometry.getAttribute( 'pSize' );

	if ( this.selected !== null ) {

		pSize.setX( this.selected, this.selectedSize );

	}

	this.selectedSize = pSize.getX( index );

	pSize.setX( index, this.selectedSize * 2 );

	//	pSize.updateRange.offset = index;
	//	pSize.updateRange.count  = 1;

	pSize.needsUpdate = true;

	this.selected = index;

};

Stations.prototype.finalise = function () {

	const bufferGeometry = this.geometry;

	const positions = new Float32BufferAttribute(this.vertices.length * 3, 3 );
	const colors = new Float32BufferAttribute( this.colors.length * 3, 3 );

	bufferGeometry.addAttribute( 'pSize', new Float32BufferAttribute( this.pointSizes, 1 ) );
	bufferGeometry.addAttribute( 'position', positions.copyVector3sArray( this.vertices ) );
	bufferGeometry.addAttribute( 'color', colors.copyColorsArray( this.colors ) );

	bufferGeometry.getAttribute( 'color' ).onUpload( onUploadDropBuffer );

	this.pointSizes = null;
	this.colors     = null;

};

Stations.prototype.resetDistances = function () {

	this.stations.forEach( function _resetDistance( node ) { node.distance = Infinity; } );

};

export { Stations };<|endoftext|>


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

import angular from 'angular';
import {MessageType} from 'ngeo/message/Message';
import gmfExternalDataSourcesManager from 'gmf/datasource/ExternalDataSourcesManager';
import ngeoMessageNotification from 'ngeo/message/Notification';

/**
 * This function handles drag and drop on the element. It is used on the map
 *
 *

 *

 *
 * Displays a 'drop here' div on dragover/dragenter and removes it on dragleave/drop
 * On drop the file is added to the external datasources if it is valid
 * Otherwise an alert message is shown
 * This function is enabled for desktop only in AbstractDesktopController.js with
 *
 *     module.value('gmfFileDropEnabled', true);
 *
 *
 * @ngInject
 * @param {import('gmf/datasource/ExternalDataSourcesManager').ExternalDatSourcesManager} gmfExternalDataSourcesManager The manager of external datasources.
 * @param {angular.gettext.gettextCatalog} gettextCatalog Gettext catalog.
 * @returns {angular.IDirective} The Directive Definition Object.
 * @ngdoc directive
 * @ngname gmfFileDropZone
 */
const fileDrop = function (gmfExternalDataSourcesManager, gettextCatalog) {
  return {
    restrict: 'A',
    template: '

{{"Drop file here." | translate}}

',
    /**
     * @param {angular.IScope} $scope Scope.
     * @param {JQuery} element Element.
     * @param {angular.IAttributes} attrs Attributes.
     */
    link: function ($scope, element, attrs) {
      // @ts-ignore: scope ......
      if ($scope.ctrl.fileDropEnabled !== true) {
        return;
      }

      element.bind('dragover', processDrag_);
      element.bind('dragenter', processDrag_);
      element.bind('dragleave', processDrag_);

      element.bind('drop', processDrop_.bind(this, element, gmfExternalDataSourcesManager, gettextCatalog));
    },
  };
};

/**
 * Function to display and hide the 'drop-zone' element, which covers the element
 */
function processDrag_() {
  if (event !== null) {
    event.preventDefault();
    event.stopPropagation();
  }
  if (event.type === 'dragenter' || event.type === 'dragleave') {
    const dropZone = document.getElementById('drop-zone');

    if (event.type === 'dragenter') {
      dropZone.classList.remove('drop-zone-off');
      dropZone.classList.add('drop-zone');
    }
    if (event.type === 'dragleave') {
      dropZone.classList.remove('drop-zone');
      dropZone.classList.add('drop-zone-off');
    }
  }
}

/**
 * This function handles the drop event
 * For a valid file, the file is added to the external source
 * For an invalid file an alert message is dsplayed
 * If an element with 'gmf-app-map-messages' (disclaimers) exists the message is added to it
 * otherwise it is placed directly on the element
 * the message desappears after a delay of 4 seconds
 *
 * @param {JQuery} element Element.
 * @param {import('gmf/datasource/ExternalDataSourcesManager').ExternalDatSourcesManager} gmfExternalDataSourcesManager The manager of external datasources.
 * @param {angular.gettext.gettextCatalog} gettextCatalog Gettext catalog
 * @param {JQuery.DropEvent} event drop event.
 * @private
 * @hidden
 */
function processDrop_(element, gmfExternalDataSourcesManager, gettextCatalog, event) {
  if (event !== null) {
    event.preventDefault();
  }
  const dropZone = document.getElementById('drop-zone');
  dropZone.classList.remove('drop-zone');
  dropZone.classList.add('drop-zone-off');
  const file = event.originalEvent.dataTransfer.files[0];
  gmfExternalDataSourcesManager.createAndAddDataSourceFromFile(file, (success) => {
    if (!success) {
      const div = document.createElement('DIV');
      div.id = 'file-alert';

      const alertElements = document.getElementsByClassName('gmf-app-map-messages');
      if (alertElements && alertElements.length > 0) {
        div.classList.add('gmf-file-alert-contained');
        alertElements[0].insertBefore(div, alertElements[0].childNodes[0]);
      } else {
        div.classList.add('gmf-file-alert-alone');
        element[0].appendChild(div);
      }
      const delay = 4000;
      ngeoMessageNotification.notify({
        msg: gettextCatalog.getString('This file can not be imported!'),
        type: MessageType.ERROR,
        target: document.querySelector('#file-alert'),
        delay: delay,
      });

      setTimeout(function () {
        const el = document.getElementById('file-alert');
        el.remove();
      }, delay);
    }
  });
}

/**
 * @type {angular.IModule}
 * @hidden
 */
const myModule = angular.module('gmfFileDropZone', [gmfExternalDataSourcesManager.name]);

myModule.directive('gmfFileDropZone', fileDrop);

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

import angular from 'angular';
import {MessageType} from 'ngeo/message/Message';
import gmfExternalDataSourcesManager from 'gmf/datasource/ExternalDataSourcesManager';
import ngeoMessageNotification from 'ngeo/message/Notification';

/**
 * This function handles drag and drop on the element. It is used on the map
 *
 *

 *

 *
 * Displays a 'drop here' div on dragover/dragenter and removes it on dragleave/drop
 * On drop the file is added to the external datasources if it is valid
 * Otherwise an alert message is shown
 * This function is enabled for desktop only in AbstractDesktopController.js with
 *
 *     module.value('gmfFileDropEnabled', true);
 *
 *
 * @ngInject
 * @param {import('gmf/datasource/ExternalDataSourcesManager').ExternalDatSourcesManager} gmfExternalDataSourcesManager The manager of external datasources.
 * @param {angular.gettext.gettextCatalog} gettextCatalog Gettext catalog.
 * @returns {angular.IDirective} The Directive Definition Object.
 * @ngdoc directive
 * @ngname gmfFileDropZone
 */
const fileDrop = function (gmfExternalDataSourcesManager, gettextCatalog) {
  return {
    restrict: 'A',
    template: '

{{"Drop file here." | translate}}

',
    /**
     * @param {angular.IScope} $scope Scope.
     * @param {JQuery} element Element.
     * @param {angular.IAttributes} attrs Attributes.
     */
    link: function ($scope, element, attrs) {
      // @ts-ignore: scope ......
      if ($scope.ctrl.fileDropEnabled !== true) {
        return;
      }

      element.bind('dragover', processDrag_);
      element.bind('dragenter', processDrag_);
      element.bind('dragleave', processDrag_);

      element.bind('drop', processDrop_.bind(this, element, gmfExternalDataSourcesManager, gettextCatalog));
    },
  };
};

/**
 * Function to display and hide the 'drop-zone' element, which covers the element
 */
function processDrag_() {
  if (event !== null) {
    event.preventDefault();
    event.stopPropagation();
  }
  if (event.type === 'dragenter' || event.type === 'dragleave') {
    const dropZone = document.getElementById('drop-zone');

    if (event.type === 'dragenter') {
      dropZone.classList.remove('drop-zone-off');
      dropZone.classList.add('drop-zone');
    }
    if (event.type === 'dragleave') {
      dropZone.classList.remove('drop-zone');
      dropZone.classList.add('drop-zone-off');
    }
  }
}

/**
 * This function handles the drop event
 * For a valid file, the file is added to the external source
 * For an invalid file an alert message is dsplayed
 * If an element with 'gmf-app-map-messages' (disclaimers) exists the message is added to it
 * otherwise it is placed directly on the element
 * the message desappears after a delay of 4 seconds
 *
 * @param {JQuery} element Element.
 * @param {import('gmf/datasource/ExternalDataSourcesManager').ExternalDatSourcesManager} gmfExternalDataSourcesManager The manager of external datasources.
 * @param {angular.gettext.gettextCatalog} gettextCatalog Gettext catalog
 * @param {JQuery.DropEvent} event drop event.
 * @private
 * @hidden
 */
function processDrop_(element, gmfExternalDataSourcesManager, gettextCatalog, event) {
  if (event !== null) {
    event.preventDefault();
  }
  const dropZone = document.getElementById('drop-zone');
  dropZone.classList.remove('drop-zone');
  dropZone.classList.add('drop-zone-off');
  if (event.originalEvent.dataTransfer.files.length != 1) {
    return;
  }
  const file = event.originalEvent.dataTransfer.files[0];
  gmfExternalDataSourcesManager.createAndAddDataSourceFromFile(file, (success) => {
    if (!success) {
      const div = document.createElement('DIV');
      div.id = 'file-alert';

      const alertElements = document.getElementsByClassName('gmf-app-map-messages');
      if (alertElements && alertElements.length > 0) {
        div.classList.add('gmf-file-alert-contained');
        alertElements[0].insertBefore(div, alertElements[0].childNodes[0]);
      } else {
        div.classList.add('gmf-file-alert-alone');
        element[0].appendChild(div);
      }
      const delay = 4000;
      ngeoMessageNotification.notify({
        msg: gettextCatalog.getString('This file can not be imported!'),
        type: MessageType.ERROR,
        target: document.querySelector('#file-alert'),
        delay: delay,
      });

      setTimeout(function () {
        const el = document.getElementById('file-alert');
        el.remove();
      }, delay);
    }
  });
}

/**
 * @type {angular.IModule}
 * @hidden
 */
const myModule = angular.module('gmfFileDropZone', [gmfExternalDataSourcesManager.name]);

myModule.directive('gmfFileDropZone', fileDrop);

export default myModule;
<|endoftext|>


<commit_before>import * as DS from './delaney';
import * as p  from './properties';
import * as d  from './derived';
import * as cv from './covers';

import { rationals } from '../arithmetic/types';
const Q = rationals;

const _assert = (condition, message) => {
  if (!condition)
    throw new Error(message || 'assertion error');
};

const _map1dOrbits = (fn, ds) => {
  const result = [];

  for (const i of ds.indices()) {
    for (const j of ds.indices()) {
      if (j > i) {
        for (const D of DS.orbitReps2(ds, i, j))
          result.push(fn(i, j, D));
      }
    }
  }

  return result;
};

const _loopless = (ds, i, j, D) =>
  DS.orbit2(ds, i, j, D).every(E => ds.s(i, E) != E && ds.s(j, E) != E);

const _unbranched = ds => _map1dOrbits(ds.v.bind(ds), ds).every(v => v == 1);

const _fullyBranched = ds => _map1dOrbits(ds.v.bind(ds), ds).every(v => !!v);

const _sum = numbers => numbers.reduce((a, x) => Q.plus(a, x), 0);

export const curvature = (ds, vDefault = 1) => {
  _assert(DS.dim(ds) == 2, 'must be two-dimensional');
  _assert(p.isConnected(ds), 'must be connected');

  const orbitContribution = (i, j, D) =>
    Q.div((_loopless(ds, i, j, D) ? 2 : 1), (ds.v(i, j, D) || vDefault));

  return Q.minus(_sum(_map1dOrbits(orbitContribution, ds)), DS.size(ds));
};

export const isProtoEuclidean = ds => Q.ge(curvature(ds), 0);
export const isProtoSpherical = ds => Q.gt(curvature(ds), 0);
export const isEuclidean = ds => _fullyBranched(ds) && Q.eq(curvature(ds), 0);
export const isHyperbolic = ds => _fullyBranched(ds) && Q.lt(curvature(ds), 0);

export const isSpherical = ds => {
  if (_fullyBranched(ds) && isProtoSpherical(ds)) {
    const dso = d.orientedCover(ds);
    const cones = _map1dOrbits(dso.v.bind(dso), dso).filter(v => v > 1);

    return (
      cones.length != 1
        && (cones.length != 2 || (cones[0] == cones[1]))
    );
  }
  else
    return false;
};

export const orbifoldSymbol = ds => {
  //TODO correctly handle multiple boundary components

  const orbitType = (i, j, D) => [ds.v(i, j, D), _loopless(ds, i, j, D)];

  const types = _map1dOrbits(orbitType, ds);
  const cones = types.filter(([v, c]) => v > 1 && c).map(([v]) => v);
  const corners = types.filter(([v, c]) => v > 1 && !c).map(([v]) => v);

  const cost = Q.minus(2, _sum([
    Q.div(curvature(ds), 2),
    _sum(cones.map(v => Q.div(v - 1, v))),
    _sum(corners.map(v => Q.div(v - 1, 2*v))),
    (p.isLoopless(ds) ? 0 : 1)
  ]));

  _assert(Q.typeOf(cost) == 'Integer',
          'residual cost should be an integer, got ${cost}');

  const repeat = (c, n) => new Array(n).fill(c);

  const sym = [].concat(
    cones.sort().reverse(),
    (p.isLoopless(ds) ? [] : ['*']),
    corners.sort().reverse(),
    (p.isWeaklyOriented(ds) ? repeat('o', cost/2) : repeat('x', cost))
  ).join('');

  if (sym == 'x' || sym == '*' || sym == '')
    return '1'+sym;
  else
    return sym;
};

export const toroidalCover = ds => {
  _assert(isEuclidean(ds), 'must be euclidean');

  const dso = d.orientedCover(ds);
  const degree = Math.max(..._map1dOrbits(dso.v.bind(dso), dso));

  return cv.covers(dso, degree).filter(_unbranched).first();
};

const _eulerCharacteristic = ds => {
  const nrLoops = i => ds.elements().filter(D => ds.s(i, D) == D).length;
  const nrOrbits = (i, j) => DS.orbitReps2(ds, i, j).length;

  const nf = ds.size;
  const ne = (3 * nf + nrLoops(0) + nrLoops(1) + nrLoops(2)) / 2;
  const nv = nrOrbits(0, 1) + nrOrbits(0, 2) + nrOrbits(1, 2);

  return nf - ne + nv;
};

const _cutsOffDisk = (ds, cut, allow2Cone) => {
  const checkCones = cones => (
    cones.length == 0 || (allow2Cone && cones.length == 1 && cones[0] == 2)
  );

  const pairs = [];
  for (const D of cut) {
    const E = ds.s(1, D);
    pairs.push([D, D]);
    pairs.push([E, E]);
  }
  const tmp = DS.withPairings(ds, 1, pairs);
  const patch = d.subsymbol(tmp, [0, 1, 2], cut[0]);

  if (patch.size == cut.length)
    return false;

  if (patch.size == ds.size) {
    const vs = [ds.v(0, 1, cut[0]), ds.v(1, 2, cut[0])];
    if (cut.length > 2)
      vs.push(ds.v(1, 2, cut[1]));

    if (checkCones(vs.filter(v => v > 1)))
      return false;
  }

  if (patch.size == ds.size - cut.length) {
    if (
      cut.every(D => ds.v(1, 2, D) == 1)
        && cut.every(D => ds.v(0, 1, D) == 1)
    )
      return false;
  }

  if (!p.isWeaklyOriented(patch))
    return false;

  if (_eulerCharacteristic(patch) != 1)
    return false;

  const orbitType = (i, j, D) => [patch.v(i, j, D), _loopless(patch, i, j, D)];
  const types = _map1dOrbits(orbitType, patch);

  return checkCones(types.filter(([v, c]) => v > 1 && c).map(([v]) => v));
};

export const isPseudoConvex = ds => {
  _assert(DS.dim(ds) == 2, 'must be two-dimensional');
  _assert(p.isConnected(ds), 'must be connected');

  const log = () => {};
  //const log = console.log;

  ds = d.orientedCover(ds);
  log(`isPseudoConvex(${ds})`);
  const ori = p.partialOrientation(ds);

  for (const A1 of ds.elements().filter(D => ori[D] > 0)) {
    log(`  A1 = ${A1}`);
    const onTrail1 = Array(ds.size + 1).fill(0);
    onTrail1[A1] = 1;
    let A2 = ds.s(0, A1);

    while (!onTrail1[A2]) {
      log(`    A2 = ${A2}`);
      const onTrail2 = onTrail1.slice();
      onTrail2[A2] = 1;
      let B2 = ds.s(2, A2);

      while (B2 == A1 || !onTrail2[B2]) {
        log(`      B2 = ${B2}`);
        if (B2 == A1) {
          if (_cutsOffDisk(ds, [A1, A2], true))
            return false;
          else
            break;
        }

        const onTrail3 = onTrail2.slice();
        onTrail3[B2] = 1;
        let B1 = ds.s(0, B2);

        while (!onTrail3[B1]) {
          log(`        B1 = ${B1}`);

          const onTrail4 = onTrail3.slice();
          onTrail4[B1] = 1;
          let T = ds.s(2, B1);

          while (T != A1 && ds.s(1, T) != B1 && !onTrail4[T]) {
            onTrail4[T] = onTrail4[ds.s(1, T)] = 1;
            T = ds.s(2, ds.s(1, T));
          }
          log(`          T = ${T}`);

          if (T == A1 && _cutsOffDisk(ds, [A1, A2, B2, B1], false))
            return false;

          onTrail3[B1] = onTrail3[ds.s(1, B1)] = 1;
          B1 = ds.s(0, ds.s(1, B1));
        }

        onTrail2[B2] = onTrail2[ds.s(1, B2)] = 1;
        B2 = ds.s(2, ds.s(1, B2));
      }

      onTrail1[A2] = onTrail1[ds.s(1, A2)] = 1;
      A2 = ds.s(0, ds.s(1, A2));
    }
  }

  return true;
};

if (require.main == module) {
  const test = ds => {
    const is = fn => fn(ds) ? 'is' : 'is not';

    console.log(`ds = ${ds}`);
    console.log(`  curvature is ${curvature(ds)}`);
    console.log(`  symbol ${is(isProtoEuclidean)} proto-euclidean`);
    console.log(`  symbol ${is(isProtoSpherical)} proto-spherical`);
    console.log(`  symbol ${is(isEuclidean)} euclidean`);
    console.log(`  symbol ${is(isHyperbolic)} hyperbolic`);
    console.log(`  symbol ${is(isSpherical)} spherical`);
    console.log(`  symbol ${is(isPseudoConvex)} pseudo-convex`);
    console.log(`  orbifold symbol = ${orbifoldSymbol(ds)}`);

    if (isEuclidean(ds)) {
      const dst = toroidalCover(ds);
      console.log(`  toroidal cover = ${dst}`);

      const curv = curvature(dst);
      const orbs = orbifoldSymbol(dst);

      if (Q.eq(curv, 0) && orbs == 'o')
        console.log('    (curvature and orbifold symbol okay)');
      else
        console.error(`    !!!! curvature ${curv}, orbifold symbol ${orbs}`);
    }
    console.log();
  };

  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 0,0>'));
  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 8,0>'));
  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 8,3>'));
  test(DS.parse('<1.1:1:1,1,1:5,3>'));
  test(DS.parse('<1.1:1:1,1,1:6,3>'));
  test(DS.parse('<1.1:1:1,1,1:7,3>'));
  test(DS.parse('<1.1:2:2,1 2,1 2:2,4 4>'));
  test(DS.parse('<1.1:2:2,1 2,1 2:2,4 5>'));
  test(DS.parse('<1.1:8:2 4 6 8,8 3 5 7,6 5 8 7:4,4>'));
  test(DS.parse('<1.1:8:2 4 6 8,8 3 5 7,5 6 8 7:4,4>'));
  test(DS.parse('<1.1:5:2 4 5,1 2 3 5,3 4 5:8 3,8 3>')); // not pseudo-convex
  test(DS.parse('<1.1:4:2 4,1 3 4,3 4:4,4>'));
  test(DS.parse('<1.1:16:2 4 6 8 10 12 14 16,8 3 5 7 16 11 13 15,6 5 11 12 15 16 14 13:4 4,4 4>'));
}
<commit_msg>delaney2d: restrict special cases for spherical tilings to just those<commit_after>import * as DS from './delaney';
import * as p  from './properties';
import * as d  from './derived';
import * as cv from './covers';

import { rationals } from '../arithmetic/types';
const Q = rationals;

const _assert = (condition, message) => {
  if (!condition)
    throw new Error(message || 'assertion error');
};

const _map1dOrbits = (fn, ds) => {
  const result = [];

  for (const i of ds.indices()) {
    for (const j of ds.indices()) {
      if (j > i) {
        for (const D of DS.orbitReps2(ds, i, j))
          result.push(fn(i, j, D));
      }
    }
  }

  return result;
};

const _loopless = (ds, i, j, D) =>
  DS.orbit2(ds, i, j, D).every(E => ds.s(i, E) != E && ds.s(j, E) != E);

const _unbranched = ds => _map1dOrbits(ds.v.bind(ds), ds).every(v => v == 1);

const _fullyBranched = ds => _map1dOrbits(ds.v.bind(ds), ds).every(v => !!v);

const _sum = numbers => numbers.reduce((a, x) => Q.plus(a, x), 0);

export const curvature = (ds, vDefault = 1) => {
  _assert(DS.dim(ds) == 2, 'must be two-dimensional');
  _assert(p.isConnected(ds), 'must be connected');

  const orbitContribution = (i, j, D) =>
    Q.div((_loopless(ds, i, j, D) ? 2 : 1), (ds.v(i, j, D) || vDefault));

  return Q.minus(_sum(_map1dOrbits(orbitContribution, ds)), DS.size(ds));
};

export const isProtoEuclidean = ds => Q.ge(curvature(ds), 0);
export const isProtoSpherical = ds => Q.gt(curvature(ds), 0);
export const isEuclidean = ds => _fullyBranched(ds) && Q.eq(curvature(ds), 0);
export const isHyperbolic = ds => _fullyBranched(ds) && Q.lt(curvature(ds), 0);

export const isSpherical = ds => {
  if (_fullyBranched(ds) && isProtoSpherical(ds)) {
    const dso = d.orientedCover(ds);
    const cones = _map1dOrbits(dso.v.bind(dso), dso).filter(v => v > 1);

    return (
      cones.length != 1
        && (cones.length != 2 || (cones[0] == cones[1]))
    );
  }
  else
    return false;
};

export const orbifoldSymbol = ds => {
  //TODO correctly handle multiple boundary components

  const orbitType = (i, j, D) => [ds.v(i, j, D), _loopless(ds, i, j, D)];

  const types = _map1dOrbits(orbitType, ds);
  const cones = types.filter(([v, c]) => v > 1 && c).map(([v]) => v);
  const corners = types.filter(([v, c]) => v > 1 && !c).map(([v]) => v);

  const cost = Q.minus(2, _sum([
    Q.div(curvature(ds), 2),
    _sum(cones.map(v => Q.div(v - 1, v))),
    _sum(corners.map(v => Q.div(v - 1, 2*v))),
    (p.isLoopless(ds) ? 0 : 1)
  ]));

  _assert(Q.typeOf(cost) == 'Integer',
          'residual cost should be an integer, got ${cost}');

  const repeat = (c, n) => new Array(n).fill(c);

  const sym = [].concat(
    cones.sort().reverse(),
    (p.isLoopless(ds) ? [] : ['*']),
    corners.sort().reverse(),
    (p.isWeaklyOriented(ds) ? repeat('o', cost/2) : repeat('x', cost))
  ).join('');

  if (sym == 'x' || sym == '*' || sym == '')
    return '1'+sym;
  else
    return sym;
};

export const toroidalCover = ds => {
  _assert(isEuclidean(ds), 'must be euclidean');

  const dso = d.orientedCover(ds);
  const degree = Math.max(..._map1dOrbits(dso.v.bind(dso), dso));

  return cv.covers(dso, degree).filter(_unbranched).first();
};

const _eulerCharacteristic = ds => {
  const nrLoops = i => ds.elements().filter(D => ds.s(i, D) == D).length;
  const nrOrbits = (i, j) => DS.orbitReps2(ds, i, j).length;

  const nf = ds.size;
  const ne = (3 * nf + nrLoops(0) + nrLoops(1) + nrLoops(2)) / 2;
  const nv = nrOrbits(0, 1) + nrOrbits(0, 2) + nrOrbits(1, 2);

  return nf - ne + nv;
};

const _cutsOffDisk = (ds, cut, allow2Cone) => {
  const checkCones = cones => (
    cones.length == 0 || (allow2Cone && cones.length == 1 && cones[0] == 2)
  );

  const pairs = [];
  for (const D of cut) {
    const E = ds.s(1, D);
    pairs.push([D, D]);
    pairs.push([E, E]);
  }
  const tmp = DS.withPairings(ds, 1, pairs);
  const patch = d.subsymbol(tmp, [0, 1, 2], cut[0]);

  if (patch.size == cut.length)
    return false;

  if (_eulerCharacteristic(ds) > 0) {
    if (patch.size == ds.size) {
      const vs = [ds.v(0, 1, cut[0]), ds.v(1, 2, cut[0])];
      if (cut.length > 2)
        vs.push(ds.v(1, 2, cut[1]));

      if (checkCones(vs.filter(v => v > 1)))
        return false;
    }

    if (patch.size == ds.size - cut.length) {
      if (
        cut.every(D => ds.v(1, 2, D) == 1)
          && cut.every(D => ds.v(0, 1, D) == 1)
      )
        return false;
    }
  }

  if (!p.isWeaklyOriented(patch))
    return false;

  if (_eulerCharacteristic(patch) != 1)
    return false;

  const orbitType = (i, j, D) => [patch.v(i, j, D), _loopless(patch, i, j, D)];
  const types = _map1dOrbits(orbitType, patch);

  return checkCones(types.filter(([v, c]) => v > 1 && c).map(([v]) => v));
};

export const isPseudoConvex = ds => {
  _assert(DS.dim(ds) == 2, 'must be two-dimensional');
  _assert(p.isConnected(ds), 'must be connected');

  const log = () => {};
  //const log = console.log;

  ds = d.orientedCover(ds);
  log(`isPseudoConvex(${ds})`);
  const ori = p.partialOrientation(ds);

  for (const A1 of ds.elements().filter(D => ori[D] > 0)) {
    log(`  A1 = ${A1}`);
    const onTrail1 = Array(ds.size + 1).fill(0);
    onTrail1[A1] = 1;
    let A2 = ds.s(0, A1);

    while (!onTrail1[A2]) {
      log(`    A2 = ${A2}`);
      const onTrail2 = onTrail1.slice();
      onTrail2[A2] = 1;
      let B2 = ds.s(2, A2);

      while (B2 == A1 || !onTrail2[B2]) {
        log(`      B2 = ${B2}`);
        if (B2 == A1) {
          if (_cutsOffDisk(ds, [A1, A2], true))
            return false;
          else
            break;
        }

        const onTrail3 = onTrail2.slice();
        onTrail3[B2] = 1;
        let B1 = ds.s(0, B2);

        while (!onTrail3[B1]) {
          log(`        B1 = ${B1}`);

          const onTrail4 = onTrail3.slice();
          onTrail4[B1] = 1;
          let T = ds.s(2, B1);

          while (T != A1 && ds.s(1, T) != B1 && !onTrail4[T]) {
            onTrail4[T] = onTrail4[ds.s(1, T)] = 1;
            T = ds.s(2, ds.s(1, T));
          }
          log(`          T = ${T}`);

          if (T == A1 && _cutsOffDisk(ds, [A1, A2, B2, B1], false))
            return false;

          onTrail3[B1] = onTrail3[ds.s(1, B1)] = 1;
          B1 = ds.s(0, ds.s(1, B1));
        }

        onTrail2[B2] = onTrail2[ds.s(1, B2)] = 1;
        B2 = ds.s(2, ds.s(1, B2));
      }

      onTrail1[A2] = onTrail1[ds.s(1, A2)] = 1;
      A2 = ds.s(0, ds.s(1, A2));
    }
  }

  return true;
};

if (require.main == module) {
  const test = ds => {
    const is = fn => fn(ds) ? 'is' : 'is not';

    console.log(`ds = ${ds}`);
    console.log(`  curvature is ${curvature(ds)}`);
    console.log(`  symbol ${is(isProtoEuclidean)} proto-euclidean`);
    console.log(`  symbol ${is(isProtoSpherical)} proto-spherical`);
    console.log(`  symbol ${is(isEuclidean)} euclidean`);
    console.log(`  symbol ${is(isHyperbolic)} hyperbolic`);
    console.log(`  symbol ${is(isSpherical)} spherical`);
    console.log(`  symbol ${is(isPseudoConvex)} pseudo-convex`);
    console.log(`  orbifold symbol = ${orbifoldSymbol(ds)}`);

    if (isEuclidean(ds)) {
      const dst = toroidalCover(ds);
      console.log(`  toroidal cover = ${dst}`);

      const curv = curvature(dst);
      const orbs = orbifoldSymbol(dst);

      if (Q.eq(curv, 0) && orbs == 'o')
        console.log('    (curvature and orbifold symbol okay)');
      else
        console.error(`    !!!! curvature ${curv}, orbifold symbol ${orbs}`);
    }
    console.log();
  };

  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 0,0>'));
  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 8,0>'));
  test(DS.parse('<1.1:3:1 2 3,1 3,2 3:4 8,3>'));
  test(DS.parse('<1.1:1:1,1,1:5,3>'));
  test(DS.parse('<1.1:1:1,1,1:6,3>'));
  test(DS.parse('<1.1:1:1,1,1:7,3>'));
  test(DS.parse('<1.1:2:2,1 2,1 2:2,4 4>'));
  test(DS.parse('<1.1:2:2,1 2,1 2:2,4 5>'));
  test(DS.parse('<1.1:8:2 4 6 8,8 3 5 7,6 5 8 7:4,4>'));
  test(DS.parse('<1.1:8:2 4 6 8,8 3 5 7,5 6 8 7:4,4>'));
  test(DS.parse('<1.1:5:2 4 5,1 2 3 5,3 4 5:8 3,8 3>')); // not pseudo-convex
  test(DS.parse('<1.1:4:2 4,1 3 4,3 4:4,4>'));
}
<|endoftext|>


<commit_before>import * as I  from 'immutable';

import { build } from './delaney';
import { invariant } from './properties';

export function fromCyclicAdjencencies(adjs) {
  const v2ch = I.Map().asMutable();
  const e2ch = I.Map().asMutable();

  let nextch = 1;
  let ne = 0;

  for (const [v, a] of adjs) {
    v2ch.set(v, nextch);

    for (const w of a) {
      if (w == v)
        throw new Error(`found a loop at vertex ${v}`);
      if (w > v)
        ++ne;
      e2ch.set(I.List([v, w]), nextch);
      nextch += 2;
    }
  }

  const size = 4 * ne;
  const op = [ [], [], [] ];

  e2ch.forEach((D, [v, w]) => {
    const E = e2ch.get(I.List([w, v]));
    if (E == null)
      console.warn(`missing ${v} in adjacencies for ${w}`);
    op[0].push([D, E+1]);
  });

  for (const [v, a] of adjs) {
    const d = 2 * a.length;
    const D = v2ch.get(v);
    for (let i = 1; i < d; i += 2)
      op[1].push([D+i, D + (i+1)%d]);
  }

  for (let D = 1; D < size; D += 2)
    op[2].push([D, D+1]);

  return build(
    2, size,
    (_, i) => op[i],
    (_, i) => I.Range(1, size+1).map(D => [D, 1])
  );
};

if (require.main == module) {
  const fs = require('fs');
  const path = require('path');

  const { minimal } = require('./derived');

  Array.prototype.toString = function() {
    return '[ ' + this.map(x => x.toString()).join(', ') + ' ]';
  };

  const symByInvariant = I.Map().asMutable();
  const minByInvariant = I.Map().asMutable();

  process.argv.slice(2).forEach(file => {
    const name = path.basename(file, '.txt')

    console.log();
    console.log();
    console.log(name);

    const text = fs.readFileSync(file, { encoding: 'utf8' });
    const adjs = text
      .split(/\n+/)
      .filter(s => s.trim().length > 0)
      .map(s => s.trim().split(/\s+/).map(f => parseInt(f)))
      .map(a => [a[0], a.slice(1)]);

    console.log();
    console.log(adjs);

    const ds = fromCyclicAdjencencies(adjs);

    minByInvariant.update(invariant(minimal(ds)), I.List(), a => a.push(name));
    symByInvariant.update(invariant(ds), I.List(), a => a.push(name));

    console.log();
    console.log(`${ds}`);
    console.log();
    console.log(`${minimal(ds)}`);
  });

  console.log();
  console.log('Equivalence classes for maximal symmetry:');
  minByInvariant.valueSeq().forEach(a => console.log(`{ ${a.join(', ')} }`));

  console.log();
  console.log('Equivalence classes for given symmetry:');
  symByInvariant.valueSeq().forEach(a => console.log(`{ ${a.join(', ')} }`));
}
<commit_msg>output face lists, as well<commit_after>import * as I  from 'immutable';

import { build } from './delaney';
import { invariant, partialOrientation } from './properties';

export function fromCyclicAdjencencies(adjs) {
  const v2ch = I.Map().asMutable();
  const e2ch = I.Map().asMutable();
  const ch2v = I.Map().asMutable();

  let nextch = 1;
  let ne = 0;

  for (const [v, a] of adjs) {
    v2ch.set(v, nextch);

    for (const w of a) {
      if (w == v)
        throw new Error(`found a loop at vertex ${v}`);
      if (w > v)
        ++ne;
      e2ch.set(I.List([v, w]), nextch);
      ch2v.set(nextch, v);
      ch2v.set(nextch+1, v);
      nextch += 2;
    }
  }

  const size = 4 * ne;
  const op = [ [], [], [] ];

  e2ch.forEach((D, [v, w]) => {
    const E = e2ch.get(I.List([w, v]));
    if (E == null)
      console.warn(`missing ${v} in adjacencies for ${w}`);
    op[0].push([D, E+1]);
  });

  for (const [v, a] of adjs) {
    const d = 2 * a.length;
    const D = v2ch.get(v);
    for (let i = 1; i < d; i += 2)
      op[1].push([D+i, D + (i+1)%d]);
  }

  for (let D = 1; D < size; D += 2)
    op[2].push([D, D+1]);

  const ds = build(
    2, size,
    (_, i) => op[i],
    (_, i) => I.Range(1, size+1).map(D => [D, 1])
  );

  return {
    symbol: ds,
    chamberToVertex: ch2v.asImmutable()
  };
};

const traceFaces = (ds, ch2v) => {
  const ori = partialOrientation(ds);
  const seen = {};
  const faces = [];

  for (const D of ds.elements()) {
    if (ori.get(D) < 0 || seen[D])
      continue;

    const newFace = [];
    let E = D;
    do {
      newFace.push(ch2v.get(E));
      seen[E] = true;
      E = ds.s(0, ds.s(1, E));
    } while (E != D);

    faces.push(newFace);
  }

  return faces;
};

if (require.main == module) {
  const fs = require('fs');
  const path = require('path');

  const { minimal } = require('./derived');

  Array.prototype.toString = function() {
    return '[ ' + this.map(x => x.toString()).join(', ') + ' ]';
  };

  const symByInvariant = I.Map().asMutable();
  const minByInvariant = I.Map().asMutable();

  process.argv.slice(2).forEach(file => {
    const name = path.basename(file, '.txt')

    console.log();
    console.log();
    console.log(name);

    const text = fs.readFileSync(file, { encoding: 'utf8' });
    const adjs = text
      .split(/\n+/)
      .filter(s => s.trim().length > 0)
      .map(s => s.trim().split(/\s+/).map(f => parseInt(f)))
      .map(a => [a[0], a.slice(1)]);

    console.log();
    console.log(adjs);

    const { symbol: ds, chamberToVertex } = fromCyclicAdjencencies(adjs);

    minByInvariant.update(invariant(minimal(ds)), I.List(), a => a.push(name));
    symByInvariant.update(invariant(ds), I.List(), a => a.push(name));

    console.log();
    console.log(traceFaces(ds, chamberToVertex));
    console.log();
    console.log(`${ds}`);
    console.log();
    console.log(`${minimal(ds)}`);
  });

  console.log();
  console.log('Equivalence classes for maximal symmetry:');
  minByInvariant.valueSeq().forEach(a => console.log(`{ ${a.join(', ')} }`));

  console.log();
  console.log('Equivalence classes for given symmetry:');
  symByInvariant.valueSeq().forEach(a => console.log(`{ ${a.join(', ')} }`));
}
<|endoftext|>


<commit_before>/*

EAGLE .BRD Renderer

Benjamin D. Richards, Gamelab

This script generates a model of a PCB (printed circuit board)
based on XML data in a BRD file. Cadsoft EAGLE BRD files use XML from version
6.0 onwards.

The model includes `Connectors` with which the PCB may be added
as a component to a larger hardware system.

This visualisation should be regarded as indicative only.
Although every care has been taken to represent the actual data,
it is not guaranteed to be completely accurate.

*/

/**
@module EagleBrdRenderer
**/

var EagleBrdRenderer = function( xml, params ) {

	/**
	Creator of board renders. Reads XML and composites the appropriate
	textures and geometry.

	@class EagleBrdRenderer
	@constructor
	@param xml {Document} XML document to parse
	@param [params] {object} Composite parameter object
		@param [params.pixelMicrons=35] {number} Resolution of texture maps.
			By default, this is 35 microns, equal to the thickness
			of a default copper layer.
	**/

	/**
	Optional parameters used to configure render.

	@property params {object}
	**/
	this.params = params;

	/**
	XML document that contains the board data

	@property xml {Document}
	**/
	this.xml = xml;

	/**
	PCB layers to be combined into the final board

	@property layers {array}
	@default []
	**/
	this.layers = [];

	this._setDefaultParams();

	this._parseDesignRules();

	this._parseBoardBounds();

	// TODO Init layers
	// We probably want a proper layer manager here, rather than
	// a naïve array. On the other hand, the BRD does specify the complete
	// layer list.  On the first hand again, most of them are unused.

	// TODO Parse BRD into layers
};

EagleBrdRenderer.prototype._getChordPoints = function( wire ) {

	/**
	Return a list of `[ x, y ]` coordinate pairs
	representing the cardinal points of a circle described by the
	curvature of the wire parameter. This list may be length 0.

	@method _getChordPoints
	@return array
	@private
	**/

	var ang, bearing, centroidX, centroidY, dx, dy, len, radius,
		sweepMin, sweepMax,
		x1, x2, y1, y2,
		curve = parseFloat( wire.getAttribute( "curve" ) ) * Math.PI / 180,
		points = [];

	// Zero-curvature points are straight. Obviously.
	if ( curve === 0 ) {
		return points;
	}

	// Get coords.
	x1 = parseFloat( wire.getAttribute( "x1" ) );
	y1 = parseFloat( wire.getAttribute( "y1" ) );
	x2 = parseFloat( wire.getAttribute( "x2" ) );
	y2 = parseFloat( wire.getAttribute( "y2" ) );
	dx = x2 - x1;
	dy = y2 - y1;
	len = Math.sqrt( Math.pow( dx, 2 ) + Math.pow( dy, 2 ) );
	bearing = Math.atan2( dy, dx );

	// Determine circle characteristics.
	// Per chord identities, chord length = 2 * radius * sin( angle / 2 )
	radius = len / ( 2 * Math.sin( curve / 2 ) );

	// Determine sidedness
	if ( curve > 0 ) {
		ang = bearing + Math.PI / 2 - curve / 2;
	} else {
		ang = bearing - Math.PI / 2 + curve / 2;
	}
	centroidX = x1 + radius * Math.cos( ang );
	centroidY = y1 + radius * Math.sin( ang );

	// Determine angular sweep from point 1 to point 2.
	// The sweep may be greater than PI.
	sweepMin = Math.atan2( centroidY - y1, centroidX - x1 );
	sweepMax = Math.atan2( centroidY - y2, centroidX - x2 );
	if ( sweepMin > sweepMax ) {
		sweepMax += Math.PI * 2;
	}

	// Determine points that fall within sweep.
	// Must check extra points, as `sweepMax` may be as high as PI * 3.
	// Note that perfect matches are discarded,
	// as this indicates the wire point is on that cardinal point.
	if ( sweepMin < -Math.PI / 2 && sweepMax > -Math.PI / 2 ) {
		points.push( [ centroidX, centroidY + radius ] );
	}
	if ( sweepMin < 0 && sweepMax > 0 ) {
		points.push( [ centroidX - radius, centroidY ] );
	}
	if ( sweepMin < Math.PI / 2 && sweepMax > Math.PI / 2 ) {
		points.push( [ centroidX, centroidY - radius ] );
	}
	if ( sweepMin < Math.PI && sweepMax > Math.PI ) {
		points.push( [ centroidX + radius, centroidY ] );
	}
	if ( sweepMax > Math.PI * 1.5 ) {
		points.push( [ centroidX, centroidY + radius ] );
	}
	if ( sweepMax > Math.PI * 2 ) {
		points.push( [ centroidX - radius, centroidY ] );
	}
	if ( sweepMax > Math.PI * 2.5 ) {
		points.push( [ centroidX, centroidY - radius ] );
	}
	if ( sweepMax > Math.PI * 3 ) {
		points.push( [ centroidX + radius, centroidY ] );
	}

	return points;
};

EagleBrdRenderer.prototype._parseDesignRules = function() {

	/**
	Set `designRules` to contain important circuit rules from XML.

	@method _parseDesignRules
	@private
	**/

	var i,
		rules = this.xml.getElementsByTagName( "designrules" )[ 0 ]
			.getElementsByTagName( "param" );

	/**
	Design rules for this PCB layout. These are used in determining
	various important layout characteristics.

	@property designRules {object}
	@default {}
	**/
	this.designRules = {};

	for ( i = 0; i < rules.length; i++ ) {
		this.designRules[ rules[ i ].getAttribute( "name" ) ] =
			rules[ i ].getAttribute( "value" );
	}
};

EagleBrdRenderer.prototype._parseBoardBounds = function() {

	/**
	Determine and record the boundaries of the board surface.
	This is derived from `eagle.drawing.board.plain` wires.

	@method _parseBoardBounds
	@private
	**/

	var curve, chordPoints, i, j, k, x, y,
		testMinMax = function( x, y ) {
			minX = Math.min( x, minX );
			minY = Math.min( y, minY );
			maxX = Math.max( x, maxX );
			maxY = Math.max( y, maxY );
		},
		maxX = 0,
		maxY = 0,
		minX = 0,
		minY = 0,
		wires = this.xml.getElementsByTagName( "plain" )[ 0 ]
			.getElementsByTagName( "wire" );

	/*
	Some wires may be curved. Under rare circumstances,
	such as a curved line between two horizontally-aligned points,
	this might disrupt normal minmax point estimates of bounds.

	Solution: chord checking. This is simple and robust.
	Wire curve is a circular chord. If the chord sweep takes in
	any cardinal direction (top, bottom, left, right), that cardinal point
	is a minmax candidate. As we know the start and end points of the chord,
	we can compute this easily.
	*/

	// Assemble boundaries
	for ( i = 0; i < wires.length; i++ ) {

		// Simple minmax point test
		for ( j = 1; j <= 2; j++ ) {
			x = wires[ i ].getAttribute( "x" + j );
			y = wires[ i ].getAttribute( "y" + j );
			testMinMax( x, y );
		}

		// Cord checks
		curve = wires[ i ].getAttribute( "curve" );
		if ( curve ) {
			chordPoints = this._getChordPoints( wires[ i ] );
			for ( k = 0; k < chordPoints.length; k++ ) {
				testMinMax( chordPoints[ k ][ 0 ], chordPoints[ k ][ 1 ] );
			}
		}
	}

	/**
	Information on physical bounds of board, in mm.

	@property bounds {object}
		@property bounds.maxX {number}
		@property bounds.maxY {number}
		@property bounds.minX {number}
		@property bounds.minY {number}
		@property bounds.width {number}
		@property bounds.height {number}
	**/
	this.bounds = {
		minX: minX,
		minY: minY,
		maxX: maxX,
		maxY: maxY,
		width: maxX - minX,
		height: maxY - minY
	};

	/**
	Horizontal resolution of this board's texture maps.

	@property width {number}
	**/
	this.width = Math.ceil( this.bounds.width * this.params.pixelMicrons );

	/**
	Vertical resolution of this board's texture maps.

	@property height {number}
	**/
	this.height = Math.ceil( this.bounds.height * this.params.pixelMicrons );

	// TODO slots

	// TODO draw outline mask
};

EagleBrdRenderer.prototype._setDefaultParams = function() {

	/**
	Ensure that required params are set.

	@method _setDefaultParams
	@private
	**/

	if ( !this.params ) {
		this.params = {};
	}

	this.params.pixelMicrons = this.params.pixelMicrons || 35;
};
<commit_msg>Implement `ChordData` object for arc analysis<commit_after>/*

EAGLE .BRD Renderer

Benjamin D. Richards, Gamelab

This script generates a model of a PCB (printed circuit board)
based on XML data in a BRD file. Cadsoft EAGLE BRD files use XML from version
6.0 onwards.

The model includes `Connectors` with which the PCB may be added
as a component to a larger hardware system.

This visualisation should be regarded as indicative only.
Although every care has been taken to represent the actual data,
it is not guaranteed to be completely accurate.

*/

/**
@module EagleBrdRenderer
**/

var EagleBrdRenderer = function( xml, params ) {

	/**
	Creator of board renders. Reads XML and composites the appropriate
	textures and geometry.

	@class EagleBrdRenderer
	@constructor
	@param xml {Document} XML document to parse
	@param [params] {object} Composite parameter object
		@param [params.pixelMicrons=35] {number} Resolution of texture maps.
			By default, this is 35 microns, equal to the thickness
			of a default copper layer.
	**/

	/**
	Optional parameters used to configure render.

	@property params {object}
	**/
	this.params = params;

	/**
	XML document that contains the board data

	@property xml {Document}
	**/
	this.xml = xml;

	/**
	PCB layers to be combined into the final board

	@property layers {array}
	@default []
	**/
	this.layers = [];

	this._setDefaultParams();

	this._parseDesignRules();

	this._parseBoardBounds();

	// TODO Init layers
	// We probably want a proper layer manager here, rather than
	// a naïve array. On the other hand, the BRD does specify the complete
	// layer list.  On the first hand again, most of them are unused.

	// TODO Parse BRD into layers
};

EagleBrdRenderer.prototype._getChordPoints = function( wire ) {

	/**
	Return a list of `[ x, y ]` coordinate pairs
	representing the cardinal points of a circle described by the
	curvature of the wire parameter. This list may be length 0.

	@method _getChordPoints
	@return array
	@private
	**/

	var ang, bearing, centroidX, centroidY, dx, dy, len, radius,
		sweepMin, sweepMax,
		x1, x2, y1, y2,
		curve = parseFloat( wire.getAttribute( "curve" ) ) * Math.PI / 180,
		points = [];

	// Zero-curvature points are straight. Obviously.
	if ( curve === 0 ) {
		return points;
	}

	// Get coords.
	x1 = parseFloat( wire.getAttribute( "x1" ) );
	y1 = parseFloat( wire.getAttribute( "y1" ) );
	x2 = parseFloat( wire.getAttribute( "x2" ) );
	y2 = parseFloat( wire.getAttribute( "y2" ) );
	dx = x2 - x1;
	dy = y2 - y1;
	len = Math.sqrt( Math.pow( dx, 2 ) + Math.pow( dy, 2 ) );
	bearing = Math.atan2( dy, dx );

	// Determine circle characteristics.
	// Per chord identities, chord length = 2 * radius * sin( angle / 2 )
	radius = len / ( 2 * Math.sin( curve / 2 ) );

	// Determine sidedness
	if ( curve > 0 ) {
		ang = bearing + Math.PI / 2 - curve / 2;
	} else {
		ang = bearing - Math.PI / 2 + curve / 2;
	}
	centroidX = x1 + radius * Math.cos( ang );
	centroidY = y1 + radius * Math.sin( ang );

	// Determine angular sweep from point 1 to point 2.
	// The sweep may be greater than PI.
	sweepMin = Math.atan2( centroidY - y1, centroidX - x1 );
	sweepMax = Math.atan2( centroidY - y2, centroidX - x2 );
	if ( sweepMin > sweepMax ) {
		sweepMax += Math.PI * 2;
	}

	// Determine points that fall within sweep.
	// Must check extra points, as `sweepMax` may be as high as PI * 3.
	// Note that perfect matches are discarded,
	// as this indicates the wire point is on that cardinal point.
	if ( sweepMin < -Math.PI / 2 && sweepMax > -Math.PI / 2 ) {
		points.push( [ centroidX, centroidY + radius ] );
	}
	if ( sweepMin < 0 && sweepMax > 0 ) {
		points.push( [ centroidX - radius, centroidY ] );
	}
	if ( sweepMin < Math.PI / 2 && sweepMax > Math.PI / 2 ) {
		points.push( [ centroidX, centroidY - radius ] );
	}
	if ( sweepMin < Math.PI && sweepMax > Math.PI ) {
		points.push( [ centroidX + radius, centroidY ] );
	}
	if ( sweepMax > Math.PI * 1.5 ) {
		points.push( [ centroidX, centroidY + radius ] );
	}
	if ( sweepMax > Math.PI * 2 ) {
		points.push( [ centroidX - radius, centroidY ] );
	}
	if ( sweepMax > Math.PI * 2.5 ) {
		points.push( [ centroidX, centroidY - radius ] );
	}
	if ( sweepMax > Math.PI * 3 ) {
		points.push( [ centroidX + radius, centroidY ] );
	}

	return points;
};

EagleBrdRenderer.prototype._parseDesignRules = function() {

	/**
	Set `designRules` to contain important circuit rules from XML.

	@method _parseDesignRules
	@private
	**/

	var i,
		rules = this.xml.getElementsByTagName( "designrules" )[ 0 ]
			.getElementsByTagName( "param" );

	/**
	Design rules for this PCB layout. These are used in determining
	various important layout characteristics.

	@property designRules {object}
	@default {}
	**/
	this.designRules = {};

	for ( i = 0; i < rules.length; i++ ) {
		this.designRules[ rules[ i ].getAttribute( "name" ) ] =
			rules[ i ].getAttribute( "value" );
	}
};

EagleBrdRenderer.prototype._parseBoardBounds = function() {

	/**
	Determine and record the boundaries of the board surface.
	This is derived from `eagle.drawing.board.plain` wires.

	@method _parseBoardBounds
	@private
	**/

	var curve, chordPoints, i, j, k, x, y,
		testMinMax = function( x, y ) {
			minX = Math.min( x, minX );
			minY = Math.min( y, minY );
			maxX = Math.max( x, maxX );
			maxY = Math.max( y, maxY );
		},
		maxX = 0,
		maxY = 0,
		minX = 0,
		minY = 0,
		wires = this.xml.getElementsByTagName( "plain" )[ 0 ]
			.getElementsByTagName( "wire" );

	/*
	Some wires may be curved. Under rare circumstances,
	such as a curved line between two horizontally-aligned points,
	this might disrupt normal minmax point estimates of bounds.

	Solution: chord checking. This is simple and robust.
	Wire curve is a circular chord. If the chord sweep takes in
	any cardinal direction (top, bottom, left, right), that cardinal point
	is a minmax candidate. As we know the start and end points of the chord,
	we can compute this easily.
	*/

	// Assemble boundaries
	for ( i = 0; i < wires.length; i++ ) {

		// Simple minmax point test
		for ( j = 1; j <= 2; j++ ) {
			x = wires[ i ].getAttribute( "x" + j );
			y = wires[ i ].getAttribute( "y" + j );
			testMinMax( x, y );
		}

		// Cord checks
		curve = wires[ i ].getAttribute( "curve" );
		if ( curve ) {
			chordPoints = this._getChordPoints( wires[ i ] );
			for ( k = 0; k < chordPoints.length; k++ ) {
				testMinMax( chordPoints[ k ][ 0 ], chordPoints[ k ][ 1 ] );
			}
		}
	}

	/**
	Information on physical bounds of board, in mm.

	@property bounds {object}
		@property bounds.maxX {number}
		@property bounds.maxY {number}
		@property bounds.minX {number}
		@property bounds.minY {number}
		@property bounds.width {number}
		@property bounds.height {number}
	**/
	this.bounds = {
		minX: minX,
		minY: minY,
		maxX: maxX,
		maxY: maxY,
		width: maxX - minX,
		height: maxY - minY
	};

	/**
	Horizontal resolution of this board's texture maps.

	@property width {number}
	**/
	this.width = Math.ceil( this.bounds.width * this.params.pixelMicrons );

	/**
	Vertical resolution of this board's texture maps.

	@property height {number}
	**/
	this.height = Math.ceil( this.bounds.height * this.params.pixelMicrons );

	// TODO slots

	// TODO draw outline mask
};

EagleBrdRenderer.prototype._setDefaultParams = function() {

	/**
	Ensure that required params are set.

	@method _setDefaultParams
	@private
	**/

	if ( !this.params ) {
		this.params = {};
	}

	this.params.pixelMicrons = this.params.pixelMicrons || 35;
};

EagleBrdRenderer.ChordData = function( wire ) {

	/**
	Analysis of the chord and circular segment described
	by a curving wire element.

	@namespace EagleBrdRenderer
	@class ChordData
	@constructor
	@param wire {Element} EAGLE BRD `wire` XML element
	**/

	var ang;

	/**
	Curvature of wire segment

	@property curve {number}
	**/
	this.curve = parseFloat( wire.getAttribute( "curve" ) ) * Math.PI / 180;

	/**
	Horizontal coodinate of wire start

	@property x1 {number}
	**/
	this.x1 = parseFloat( wire.getAttribute( "x1" ) );

	/**
	Vertical coodinate of wire start

	@property y1 {number}
	**/
	this.y1 = parseFloat( wire.getAttribute( "y1" ) );

	/**
	Horizontal coodinate of wire end

	@property x2 {number}
	**/
	this.x2 = parseFloat( wire.getAttribute( "x2" ) );

	/**
	Vertical coodinate of wire end

	@property y2 {number}
	**/
	this.y2 = parseFloat( wire.getAttribute( "y2" ) );

	/**
	Horizontal wire displacement

	@property dx {number}
	**/
	this.dx = this.x2 - this.x1;

	/**
	Vertical wire displacement

	@property dy {number}
	**/
	this.dy = this.y2 - this.y1;

	/**
	Distance between start and end points of wire.

	@property chord {number}
	**/
	this.chord = Math.sqrt( Math.pow( this.dx, 2 ) + Math.pow( this.dy, 2 ) );

	/**
	Angle from start to end points of wire.

	@property bearing {number}
	**/
	this.bearing = Math.atan2( this.dy, this.dx );

	/**
	Radius of chord arc. Per chord identity,
	chord = 2 * radius * sin( angle / 2 ), rearranged to acquire radius.

	@property radius {number}
	**/
	this.radius = this.chord / ( 2 * Math.sin( this.curve / 2 ) );

	// Determine sidedness
	ang = curve > 0 ?
		bearing + Math.PI / 2 - curve / 2 :
		bearing - Math.PI / 2 + curve / 2;

	/**
	Horizontal position of arc origin

	@property x {number}
	**/
	this.x = this.x1 + radius * Math.cos( ang );

	/**
	Vertical position of arc origin

	@property y {number}
	**/
	this.y = this.y1 + radius * Math.sin( ang );
};
<|endoftext|>