Source: extension/Map.js

/**
 * 2D Tiled Map
 * - 2D Tiled Map is optimized canvas rendering. so you should set renderingMode option as "canvas" in a layer. but an object layer doesn't need to set canvas rendering.
 * @class
 * @extends collie.Component
 * @param {Number} nTileWidth 타일 1개의 너비
 * @param {Number} nTileHeight 타일 1개의 높이
 * @param {Object} htOption
 * @param {Boolean} [htOption.useEvent=true] 맵을 움직이는 이벤트를 사용한다
 * @param {Boolean} [htOption.useCache=false] Canvas를 사용할 때, 맵의 크기가 작을 경우 전체 사이즈를 미리 만들어 놓는 방식을 사용한다
 * @param {Boolean} [htOption.useLiveUpdate=false] 맵을 움직이는 중간에 타일을 업데이트 한다
 * @param {Number} [htOption.updateInterval=150] 업데이트 주기를 설정한다. 단위는 ms
 * @param {Boolean} [htOption.useLimitMoving=true] 맵을 벗어나게 움직일 수 없음
 * @param {Number} [htOption.cacheTileSize=1] 화면 범위 밖의 타일을 사방에 해당 사이즈 크기만큼 미리 생성함
 * @requires collie.addon.js
 * @example
// Make random data
var rowLength = 20;
var colLength = 20;
var objectCount = 10;
var tileSize = 30; // px
var mapData = [];
for (var row = 0; row < rowLength; row++) {
    var rowData = [];
    for (var col = 0; col < colLength; col++) {
        rowData.push({
            backgroundColor: Math.random() > 0.5 ? "red" : "yellow" // A Tile data doesn't necessary to contain dimension and position.
        });
    }
    mapData.push(rowData);
}

// Create a Map instance
var map = new collie.Map(tileSize, tileSize, {
    useEvent: true,
    useLiveUpdate : false
}).addTo(layer).addObjectTo(objectLayer);

// Add a mapdata.
map.setMapData(mapData);

// Add objects for display on the map.
for (var i = 0; i < objectCount; i++) {
    var tileX = Math.round(Math.random() * (colLength - 1));
    var tileY = Math.round(Math.random() * (rowLength - 1));
    var pos = map.getTileIndexToPos(tileX, tileY);
    map.addObject(tileX, tileY, new collie.DisplayObject({
        x : pos.x + 5,
        y : pos.y + 5,
        width : tileSize - 10,
        height : tileSize - 10,
        backgroundColor : "blue"
    }));
}

// Attach a event handler.
map.attach({
    mapclick : function (e) {
        console.log(e.tileX, e.tileY);
    }
});
 */
collie.Map = collie.Class(/** @lends collie.Map.prototype */{
    $init : function (nTileWidth, nTileHeight, htOption) {
        this._oLayer = null;
        this._oObjectLayer = null;
        this._fOnMousemove = this._onMousemove.bind(this);
        this._fOnMouseup = this._onMouseup.bind(this);
        this._fOnMousedown = this._onMousedown.bind(this);
        this._fOnClick = this._onClick.bind(this);
        this._fOnResize = this._onResize.bind(this);
        this._nTileWidth = nTileWidth;
        this._nTileHeight = nTileHeight;
        this._nStartMouseX = null;
        this._nStartMouseY = null;
        this._nStartContainerX = null;
        this._nStartContainerY = null;
        this._bIsMousedown = false;
        this._bLockScreen = false;
        this._nRowLength = 0;
        this._nColLength = 0;
        this._aMapData = [];
        this._aTiles = [];
        this._htTile = {};
        this._htObject = {};
        this._htEventInfo = {};
        this._sRenderingMode = null;
        this._oPool = new collie.Pool();
        this._oContainer = new collie.DisplayObject();
        this._htContainerInfo = this._oContainer.get();
        this._oObjectContainer = new collie.DisplayObject();
        this._htLayerOption = null;
        this.option({
            cacheTileSize : 1,
            useCache : false,
            updateInterval : 150,
            useLiveUpdate : false,
            useEvent : true,
            useLimitMoving : true
        });
        
        this._oTimerUpdate = collie.Timer.repeat(this._prepareTile.bind(this), this._htOption.updateInterval, {
            useAutoStart : false
        });
        
        
        this.optionSetter("useEvent", this._setterUseEvent.bind(this));
        this.optionSetter("updateInterval", function (v) {
            this._oTimerUpdate.setDuration(v);
        }.bind(this));
        
        if (typeof htOption !== "undefined") {
            this.option(htOption);
        }
        
        this._setterUseEvent(this._htOption.useEvent);
    },
    
    /**
     * 레이어에 바로 추가한다.
     * 인터페이스의 통일성을 위해 추가함
     * 
     * @param {collie.Layer} oLayer
     * @return {collie.Map} 메서드 체이닝을 위한 반환 값
     */
    addTo : function (oLayer) {
        this.setLayer(oLayer);
        return this;
    },
    
    /**
     * 레이어에 Object을 바로 추가한다.
     * 인터페이스의 통일성을 위해 추가함
     * 
     * @param {collie.Layer} oLayer
     * @return {collie.Map} 메서드 체이닝을 위한 반환 값
     */
    addObjectTo : function (oLayer) {
        this.setObjectLayer(oLayer);
        return this;
    },
    
    /**
     * Map을 추가할 레이어를 설정한다
     * @param {collie.Layer} oLayer
     */
    setLayer : function (oLayer) {
        // 기존에 등록된 레이어가 있다면 제거
        if (this._oLayer !== null) {
            this.unsetLayer();
        }
        
        // 모바일에서 DOM모드에 Map을 쓸 수 없음
        // if (oLayer.getRenderingMode() === "dom" && !collie.util.getDeviceInfo().desktop) {
            // throw new Error("The collie.Map doesn't support DOM mode in mobile devices");
        // }
        
        this._oLayer = oLayer;
        this._htLayerOption = this._oLayer._htOption;
        this._sRenderingMode = this._oLayer.getRenderingMode();
        this._oLayer.attach("resize", this._fOnResize);
        
        if (this._htOption.useEvent) {
            this._oLayer.attach("mousemove", this._fOnMousemove);
            this._oLayer.attach("mouseup", this._fOnMouseup);
            this._oLayer.attach("mousedown", this._fOnMousedown);
            this._oLayer.attach("click", this._fOnClick);
        }
        
        this._oContainer.addTo(oLayer);
        this._prepareStage();
    },
    
    /**
     * @private
     */
    _prepareStage : function () {
        var nContainerWidth = this._htLayerOption.width;
        var nContainerHeight = this._htLayerOption.height;
        var bUseCache = false;
        var nPoolSize = 0;
        
        if (this._htOption.useCache && this._sRenderingMode === "canvas") {
            nContainerWidth = this._nTileWidth * this._nColLength,
            nContainerHeight = this._nTileHeight * this._nRowLength,
            bUseCache = true;
            nPoolSize = this._nColLength * this._nRowLength;
        } else {
            var nColCount = Math.ceil(this._htLayerOption.width / this._nTileWidth) + 2;
            var nRowCount = Math.ceil(this._htLayerOption.height / this._nTileHeight) + 2;
            nPoolSize = (nColCount + this._htOption.cacheTileSize * 2) * (nRowCount + this._htOption.cacheTileSize * 2);            
        }
        
        this._oContainer.set({
            width : nContainerWidth,
            height : nContainerHeight,
            useCache : bUseCache
        });
            
        // pool 사이즈를 화면 크기에 맞게 변경
        this._oPool.changeSize(nPoolSize);
        
        if (bUseCache) {
            this._prepareTileWithCache();
        } else {
            this._prepareTile();
        }
    },
    
    /**
     * Map을 레이어에서 제거한다
     */
    unsetLayer : function () {
        if (this._oLayer !== null) {
            this._oLayer.detach("resize", this._fOnResize);
            
            if (this._htOption.useEvent) {
                this._oLayer.detach("mousemove", this._fOnMousemove);
                this._oLayer.detach("mouseup", this._fOnMouseup);
                this._oLayer.detach("mousedown", this._fOnMousedown);
                this._oLayer.detach("click", this._fOnClick);
            }
            
            this._oContainer.leave();
            this._oLayer = null;
            this._sRenderingMode = null;
        }
    },
    
    /**
     * Object을 추가할 레이어를 설정한다
     * @param {collie.Layer} oLayer
     */
    setObjectLayer : function (oLayer) {
        // 기존에 등록된 레이어가 있다면 제거
        if (this._oObjectLayer !== null) {
            this.unsetObjectLayer();
        }
        
        this._oObjectLayer = oLayer;
        this._oObjectContainer.addTo(this._oObjectLayer);
        this._oObjectContainer.set({
            width : this._oObjectLayer._htOption.width,
            height : this._oObjectLayer._htOption.height
        });
    },
    
    /**
     * Object을 레이어에서 제거한다
     */
    unsetObjectLayer : function () {
        if (this._oObjectLayer !== null) {
            this._oObjectContainer.leave();
            this._oObjectLayer = null;
        }
    },
    
    /**
     * @return {collie.DisplayObject} 컨테이너 DisplayObject를 반환
     */
    getContainer : function () {
        return this._oContainer;
    },
    
    /**
     * @return {collie.DisplayObject} Object 컨테이너 DisplayObject를 반환
     */
    getObjectContainer : function () {
        return this._oObjectContainer;
    },
    
    /**
     * Container의 위치를 설정한다
     * 
     * @param {Number} nX
     * @param {Number} nY
     */
    setPosition : function (nX, nY) {
        if (this._bLockScreen) {
            return false;
        }
        
        if (this._htOption.useLimitMoving) {
            nX = Math.max(-this._nColLength * this._nTileWidth + this._htLayerOption.width, nX);
            nY = Math.max(-this._nRowLength * this._nTileHeight + this._htLayerOption.height, nY);
            nX = Math.min(0, nX);
            nY = Math.min(0, nY);
        }
        
        nX = Math.round(nX);
        nY = Math.round(nY);
        this._oContainer.set("x", nX);
        this._oContainer.set("y", nY);
        this._htEventInfo.x = nX;
        this._htEventInfo.y = nY;
        
        // action이 활성화 돼 있다면 action도 같이 이동
        if (this._oObjectLayer !== null) {
            this._oObjectContainer.set("x", nX);
            this._oObjectContainer.set("y", nY);
        }
    },
    
    /**
     * Container의 위치를 반환한다
     * 
     * @return {Object} htResult
     * @return {Number} htResult.x
     * @return {Number} htResult.y
     */
    getPosition : function () {
        return {
            x : this._htContainerInfo.x,
            y : this._htContainerInfo.y
        };
    },
    
    /**
     * tileX 길이를 반환
     * 
     * @param {Number}
     */
    getTileXLength : function () {
        return this._nColLength;
    },
    
    /**
     * tileY 길이를 반환
     * 
     * @param {Number}
     */
    getTileYLength : function () {
        return this._nRowLength;
    },
    
    /**
     * 맵을 일괄적으로 생성한다
     * @param {Array} aMapData 맵 데이터 array[row][col] = htOption
     */
    setMapData : function (aMapData) {
        this._aMapData = aMapData;
        
        for (var row = 0, rowLength = this._aMapData.length; row < rowLength; row++) {
            for (var col = 0, colLength = this._aMapData[row].length; col < colLength; col++) {
                this._appendTileOption(col, row, this._aMapData[row][col]);
            }
        }
        
        this._cacheTileLength();
        this._prepareStage();
    },
    
    /**
     * 맵 데이터를 초기화 한다
     */
    unsetMapData : function () {
        this._aMapData = null;
    },
    
    /**
     * 맵을 개별적으로 생성한다
     * 
     * @param {Number} nTileX
     * @param {Number} nTileY
     * @param {Object} htOption
     */
    addTile : function (nTileX, nTileY, htOption) {
        htOption = htOption || {};
        this._aMapData[nTileY] = this._aMapData[nTileY] || [];
        
        // 이미 있는 타일이면 먼저 제거
        if (this._aMapData[nTileY][nTileX]) {
            this.removeTile(nTileX, nTileY);
        }
        
        this._appendTileOption(nTileX, nTileY, htOption);
        this._aMapData[nTileY][nTileX] = htOption;
        this._cacheTileLength();
    },
    
    /**
     * 타일 정보에 시스템 옵션을 삽입
     * 
     * @private
     * @param {Number} nTileX
     * @param {Number} nTileY
     * @param {Object} htOption
     */
    _appendTileOption : function (nTileX, nTileY, htOption) {
        htOption.width = this._nTileWidth;
        htOption.height = this._nTileHeight;
        htOption.x = nTileX * this._nTileWidth;
        htOption.y = nTileY * this._nTileHeight;
        htOption.tileX = nTileX;
        htOption.tileY = nTileY;
    },
    
    /**
     * 타일에 객체를 추가
     * 
     * @param {Number} nTileX
     * @param {Number} nTileY
     * @param {collie.DisplayObject} oDisplayObject
     */
    addObject : function (nTileX, nTileY, oDisplayObject) {
        // 이미 추가된 객체라면 해당 타일에서 제거
        if (typeof oDisplayObject._htOption.tileX !== "undefined" && oDisplayObject._htOption.tileX !== null) {
            this.removeObject(oDisplayObject);
        }
        
        oDisplayObject.set("mapObject", this);
        this._addObjectToTile(nTileX, nTileY, oDisplayObject);
    },
    
    /**
     * 보여지는 영역에 속하는지 확인
     * 
     * @param {Number} nTileX
     * @param {Number} nTileY
     * @return {Boolean}
     */
    isVisibleOffset : function (nTileX, nTileY) {
        return (this._htOption.useCache && this._sRenderingMode === "canvas") || (
            this._htEventInfo.startTileX <= nTileX &&
            this._htEventInfo.endTileX >= nTileX &&
            this._htEventInfo.startTileY <= nTileY &&
            this._htEventInfo.endTileY >= nTileY
        );
    },
    
    /**
     * @private
     */
    _addObjectToTile : function (nTileX, nTileY, oDisplayObject) {
        this._htObject[nTileY] = this._htObject[nTileY] || {};
        this._htObject[nTileY][nTileX] = this._htObject[nTileY][nTileX] || [];
        this._htObject[nTileY][nTileX].push(oDisplayObject);
        oDisplayObject.set("tileX", nTileX);
        oDisplayObject.set("tileY", nTileY);
        
        // 현재 보여지는 영역에 있으면 바로 추가
        if (!oDisplayObject.getParent() && this.isVisibleOffset(nTileX, nTileY)) {
            oDisplayObject.addTo(this._oObjectContainer);
        }
    },
    
    /**
     * 등록돼 있는 객체를 옮김
     * 
     * @param {Number} nTileX 옮길 타일 x
     * @param {Number} nTileY 옮길 타일 y
     * @param {collie.DisplayObject} 타일에 등록돼 있는 객체
     */
    moveObject : function (nTileX, nTileY, oDisplayObject) {
        if (typeof oDisplayObject._htOption.tileX !== "undefined" && oDisplayObject._htOption.tileX !== null) {
            this._removeObjectFromTile(oDisplayObject, this.isVisibleOffset(nTileX, nTileY));
            this._addObjectToTile(nTileX, nTileY, oDisplayObject);
        }
    },
    
    /**
     * 타일에서 객체를 제거
     * 
     * @param {collie.DisplayObject} oDisplayObject
     */
    removeObject : function (oDisplayObject) {
        oDisplayObject.leave();
        oDisplayObject.set("mapObject", null);
        this._removeObjectFromTile(oDisplayObject);
    },
    
    /**
     * @private
     * @param {collie.DisplayObject} oDisplayObject 타일에 등록돼 있는 객체
     * @param {Boolean} bSkipRemove 화면에서 제거하는 과정을 생략한다
     */
    _removeObjectFromTile : function (oDisplayObject, bSkipRemove) {
        var nTileX = oDisplayObject._htOption.tileX;
        var nTileY = oDisplayObject._htOption.tileY;
        oDisplayObject.set("tileX", null);
        oDisplayObject.set("tileY", null);
        
        if (this._htObject[nTileY] && this._htObject[nTileY][nTileX]) {
            for (var i = 0, l = this._htObject[nTileY][nTileX].length; i < l; i++) {
                if (this._htObject[nTileY][nTileX][i] === oDisplayObject) {
                    this._htObject[nTileY][nTileX].splice(i, 1);
                    break;
                }
            }
        }
        
        // 화면에서 제거
        if (!bSkipRemove && !this.isVisibleOffset(nTileX, nTileY) && oDisplayObject.getParent()) {
            oDisplayObject.leave(); 
        }
    },
    
    /**
     * 화면을 움직이지 않게 한다
     * 
     * @return {collie.Map}
     */
    lock : function () {
        this._bLockScreen = true;
        return this;
    },
    
    /**
     * 화면을 움직이게 한다
     * 
     * @return {collie.Map}
     */
    unlock : function () {
        this._bLockScreen = false;
        return this;
    },
    
    /**
     * 타일의 zIndex를 구한다
     * 
     * @private
     * @return {Number}
     */
    _getTileZIndex : function (tileX, tileY) {
        return 10 + tileY;
    },
    
    /**
     * 타일 길이를 구해놓는다
     * @private
     */
    _cacheTileLength : function () {
        this._nRowLength = this._aMapData.length;
        this._nColLength = this._aMapData[this._nRowLength - 1].length;
    },
    
    /**
     * 타일을 지운다
     * 
     * @param {Number} nTileX
     * @param {Number} nTileY
     */
    removeTile : function (nTileX, nTileY) {
        if (this._aMapData[nTileY] && this._aMapData[nTileY][nTileX]) {
            delete this._aMapData[nTileY][nTileX];
        }
    },
    
    /**
     * 해당 타일의 옵션을 변경한다
     * 
     * @param {Number} nTileX
     * @param {Number} nTileY
     * @param {Object} htOption 기존 옵션이 사라지는 것이 아니고 중첩된다
     */
    changeTile : function (nTileX, nTileY, htOption) {
        var htTile = this.getTile(nTileX, nTileY);
        
        if (htTile && htOption) {
            for (var i in htOption) {
                htTile[i] = htOption[i];
            }
            
            // 현재 타일이 만들어져 있으면 수정
            if (this._htTile[nTileY] && this._htTile[nTileY][nTileX]) {
                this._htTile[nTileY][nTileX].set(htOption);
            }
        }
    },
    
    /**
     * 타일을 가져온다
     * 
     * @param {Number} nTileX
     * @param {Number} nTileY
     * @return {Object|Boolean} DisplayObject가 반환되지 않고 타일 정보가 반환 됨
     */
    getTile : function (nTileX, nTileY) {
        return (this._aMapData[nTileY] && this._aMapData[nTileY][nTileX]) ? this._aMapData[nTileY][nTileX] : false;
        // return (this._htTile[nTileY] && this._htTile[nTileY][nTileX]) ? this._htTile[nTileY][nTileX] : false;
    },
    
    /**
     * 해당 타일을 둘러싼 타일 목록을 구한다
     * 
     * @param {Number} nTileX
     * @param {Number} nTileY
     * @return {Array} DisplayObject가 반환되지 않고 타일 정보가 반환 됨
     */
    getSurroundedTiles : function (nTileX, nTileY) {
        var aTiles = [];
        var top = this.getTile(nTileX, nTileY - 1);
        var bottom = this.getTile(nTileX, nTileY + 1);
        var left = this.getTile(nTileX - 1, nTileY);
        var right = this.getTile(nTileX + 1, nTileY);
        
        if (top) {
            aTiles.push(top);
        }
        
        if (right) {
            aTiles.push(right);
        }
        
        if (bottom) {
            aTiles.push(bottom);
        }
        
        if (left) {
            aTiles.push(left);
        }
        
        return aTiles;
    },
    
    /**
     * 해당 좌표에 등록된 객체를 가져온다
     * @param {Number} nTileX
     * @param {Number} nTileY
     * @return {Array|Boolean}
     */
    getObjects : function (nTileX, nTileY) {
        return this._htObject[nTileY] && this._htObject[nTileY][nTileX] ? this._htObject[nTileY][nTileX] : false;
    },
    
    /**
     * 필터를 이용해 해당 좌표에 등록된 객체를 가져온다
     * @param {Number} nTileX
     * @param {Number} nTileY
     * @param {Function} fFilter
     * @return {collie.DisplayObject|Boolean}
     */
    getObject : function (nTileX, nTileY, fFilter) {
        if (this.hasObject(nTileX, nTileY)) {
            for (var i = 0, l = this._htObject[nTileY][nTileX].length; i < l; i++) {
                if (fFilter(this._htObject[nTileY][nTileX][i]) === true) {
                    return this._htObject[nTileY][nTileX][i];
                }
            }
        }
        
        return false;
    },
    
    /**
     * 해당 좌표에 객체가 있는지 반환
     * 
     * @param {Number} nTileX
     * @param {Number} nTileY
     * @param {Function} fFilter
     * @return {Boolean}
     */
    hasObject : function (nTileX, nTileY, fFilter) {
        var isObject = this._htObject[nTileY] && this._htObject[nTileY][nTileX] && this._htObject[nTileY][nTileX].length;
        
        if (isObject && fFilter) {
            for (var i = 0, l = this._htObject[nTileY][nTileX].length; i < l; i++) {
                if (fFilter(this._htObject[nTileY][nTileX][i]) === true) {
                    return true;
                }
            }
            
            return false;
        }
        
        return isObject;
    },
    
    /**
     * 화면 안으로 들어온 객체 처리
     * @private
     * @param {Number} nTileX
     * @param {Number} nTileY
     */
    _enterObject : function (nTileX, nTileY) {
        var objects = this.getObjects(nTileX, nTileY);
        
        if (objects) {
            for (var i = 0, l = objects.length; i < l; i++) {
                objects[i].addTo(this._oObjectContainer);
            }
        }
    },
    
    /**
     * 화면 밖으로 나간 객체 처리
     * @private
     * @param {Number} nTileX
     * @param {Number} nTileY
     */
    _leaveObject : function (nTileX, nTileY) {
        var objects = this.getObjects(nTileX, nTileY);
        
        if (objects) {
            for (var i = 0, l = objects.length; i < l; i++) {
                objects[i].leave();
            }
        }
    },
    
    /**
     * 타일 인덱스로 포지션을 구함
     * 
     * @param {Number} nTileX
     * @param {Number} nTileY
     * @return {Object} htResult
     * @return {Number} htResult.x
     * @return {Number} htResult.y
     */
    getTileIndexToPos : function (nTileX, nTileY) {
        return {
            x : nTileX * this._nTileWidth,
            y : nTileY * this._nTileHeight
        };
    },
    
    /**
     * 포지션으로 타일 인덱스를 구함
     * 
     * @param {Number} nX
     * @param {Number} nY
     * @return {Object} htResult
     * @return {Number} htResult.tileX
     * @return {Number} htResult.tileY
     */
    getPosToTileIndex : function (nX, nY) {
        return {
            tileX : Math.floor(nX / this._nTileWidth),
            tileY : Math.floor(nY / this._nTileHeight)
        };
    },
    
    /**
     * @private
     */
    _setterUseEvent : function (bUseEvent) {
        // 레이어가 등록돼 있다면 바로 이벤트를 등록
        if (this._oLayer !== null) {
            if (bUseEvent) {
                this._oLayer.attach("mousemove", this._fOnMousemove);
                this._oLayer.attach("mouseup", this._fOnMouseup);
                this._oLayer.attach("mousedown", this._fOnMousedown);
                this._oLayer.attach("click", this._fOnClick);
            } else {
                this._oLayer.detach("mousemove", this._fOnMousemove);
                this._oLayer.detach("mouseup", this._fOnMouseup);
                this._oLayer.detach("mousedown", this._fOnMousedown);
                this._oLayer.detach("click", this._fOnClick);
            }
        }
    },
    
    _onMousemove : function (e) {
        /**
         * 맵이 이동할 떄
         * @name collie.Map#mapmove
         * @event
         * @param {Object} htEvent
         * @param {Number} htEvent.x 컨테이너 x 좌표
         * @param {Number} htEvent.y 컨테이너 y 좌표
         * @param {HTMLEvent} htEvent.event 이벤트 객체
         */
        if (this._bIsMousedown && this.fireEvent("mapmove", this._htEventInfo) !== false) {
            this.setPosition(this._nStartContainerX + (e.x - this._nStartMouseX), this._nStartContainerY + (e.y - this._nStartMouseY));
        }
    },
    
    _onMousedown : function (e) {
        this._htEventInfo.eventX = e.x;
        this._htEventInfo.eventY = e.y;
        
        /**
         * 맵에 마우스나 터치를 시작했을 때
         * @name collie.Map#mapdown
         * @event
         * @param {Object} htEvent
         * @param {Number} htEvent.x 컨테이너 x 좌표
         * @param {Number} htEvent.y 컨테이너 y 좌표
         * @param {Number} htEvent.eventX 이벤트 x 좌표
         * @param {Number} htEvent.eventY 이벤트 y 좌표
         * @param {HTMLEvent} htEvent.event 이벤트 객체
         */
        if (this.fireEvent("mapdown", this._htEventInfo) === false) {
            return;
        }
        
        this._bIsMousedown = true;
        this._nStartMouseX = e.x;
        this._nStartMouseY = e.y;
        this._nStartContainerX = this._htContainerInfo.x;
        this._nStartContainerY = this._htContainerInfo.y;
        
        if (this._htOption.useLiveUpdate && (!this._htOption.useCache || this._sRenderingMode !== "canvas")) {
            this._oTimerUpdate.start();
        }
    },
    
    _onMouseup : function (e) {
        if (this._bIsMousedown) {
            this._nStartMouseX = null;
            this._nStartMouseY = null;
            this._nStartContainerX = null;
            this._nStartContainerY = null;
            this._bIsMousedown = false;
            
            if (this._htOption.useLiveUpdate) {
                this._oTimerUpdate.pause();
            } else if (!this._htOption.useCache || this._sRenderingMode !== "canvas") {
                this._prepareTile();
            }
            
            this._htEventInfo.eventX = e.x;
            this._htEventInfo.eventY = e.y;
            
            /**
             * 맵에 마우스나 터치가 끝났을 때
             * @name collie.Map#mapup
             * @event
             * @param {Object} htEvent
             * @param {Number} htEvent.x 컨테이너 x 좌표
             * @param {Number} htEvent.y 컨테이너 y 좌표
             * @param {Number} htEvent.eventX 이벤트 x 좌표
             * @param {Number} htEvent.eventY 이벤트 y 좌표
             * @param {HTMLEvent} htEvent.event 이벤트 객체
             */
            this.fireEvent("mapup", this._htEventInfo);
        }
    },
    
    /**
     * @private
     */
    _onClick : function (e) {
        var nX = e.x - this._htContainerInfo.x;
        var nY = e.y - this._htContainerInfo.y;
        var tileIndex = this.getPosToTileIndex(nX, nY);
        this._htEventInfo.eventX = e.x;
        this._htEventInfo.eventY = e.y;
        this._htEventInfo.tileX = tileIndex.tileX;
        this._htEventInfo.tileY = tileIndex.tileY;
        
        /**
         * 맵을 클릭했을 때
         * @name collie.Map#mapclick
         * @event
         * @param {Object} htEvent
         * @param {Number} htEvent.x 컨테이너 x 좌표
         * @param {Number} htEvent.y 컨테이너 y 좌표
         * @param {Number} htEvent.eventX 이벤트 x 좌표
         * @param {Number} htEvent.eventY 이벤트 y 좌표
         * @param {Number} htEvent.tileX 타일 x 인덱스
         * @param {Number} htEvent.tileY 타일 y 인덱스
         * @param {HTMLEvent} htEvent.event 이벤트 객체
         */
        this.fireEvent("mapclick", this._htEventInfo);
    },
    
    /**
     * @private
     */
    _onResize : function () {
        if (this._oObjectLayer !== null) {
            this._oObjectContainer.set({
                width : this._oObjectLayer._htOption.width,
                height : this._oObjectLayer._htOption.height
            });
        }
        
        this._prepareStage();
    },
    
    /**
     * 타일 생성
     * @private
     * @param {Number} nTileX
     * @param {Number} nTileY
     */
    _createTile : function (nTileX, nTileY) {
        if ((!this._htTile[nTileY] || !this._htTile[nTileY][nTileX]) && this._aMapData[nTileY] && this._aMapData[nTileY][nTileX]) {
            var tile = this._oPool.get().addTo(this._oContainer);
            tile.set(this._aMapData[nTileY][nTileX]);
            this._htTile[nTileY] = this._htTile[nTileY] || {};
            this._htTile[nTileY][nTileX] = this._htTile[nTileY][nTileX] = tile;
            this._aTiles.push(tile);
            this._enterObject(nTileX, nTileY);
        }
    },
    
    /**
     * 화면 밖으로 나간 타일 회수
     * @private
     * @param {Number} startTileX
     * @param {Number} startTileY
     * @param {Number} endTileX
     * @param {Number} endTileY
     * @return {Boolean} 회수 됐는지 여부
     */
    _releaseTiles : function (startTileX, startTileY, endTileX, endTileY) {
        var tile;
        var bChanged = false;
        
        for (var i = 0; i < this._aTiles.length; i++) {
            tile = this._aTiles[i];
            
            if (
                startTileX > tile._htOption.tileX ||
                tile._htOption.tileX > endTileX ||
                startTileY > tile._htOption.tileY ||
                tile._htOption.tileY > endTileY
            ) {
                delete this._htTile[tile._htOption.tileY][tile._htOption.tileX];
                this._oPool.release(tile);
                this._aTiles.splice(i, 1);
                this._leaveObject(tile._htOption.tileX, tile._htOption.tileY);
                i--;
                bChanged = true;
            }
        }
        
        return bChanged;
    },
    
    /**
     * useCache를 쓰고 있을 떄 타일 준비
     * @private
     */
    _prepareTileWithCache : function () {
        for (var row = 0; row < this._nRowLength; row++) {
            for (var col = 0; col < this._nColLength; col++) {
                this._createTile(col, row);
            }
        }
        
        /**
         * 맵의 타일이 변경됐을 때
         * @name collie.Map#mapchange
         * @event
         * @param {Object} htEvent
         * @param {Number} htEvent.x 컨테이너 x 좌표
         * @param {Number} htEvent.y 컨테이너 y 좌표
         * @param {HTMLEvent} htEvent.event 이벤트 객체
         */
        this.fireEvent("mapchange", this._htEventInfo);
    },
    
    /**
     * 컨테이너 위치에 따라서 타일을 다시 구성
     * @private
     */
    _prepareTile : function () {
        if (!this._aMapData.length) {
            return false;
        }
        
        var startTileX = Math.max(0, Math.floor(-this._htContainerInfo.x / this._nTileWidth) - this._htOption.cacheTileSize);
        var startTileY = Math.max(0, Math.floor(-this._htContainerInfo.y / this._nTileHeight) - this._htOption.cacheTileSize);
        var endTileX = Math.min(this._nColLength - 1, Math.floor((-this._htContainerInfo.x + this._htLayerOption.width) / this._nTileWidth) + this._htOption.cacheTileSize);
        var endTileY = Math.min(this._nRowLength - 1, Math.floor((-this._htContainerInfo.y + this._htLayerOption.height) / this._nTileHeight) + this._htOption.cacheTileSize);
        var tile;
        this._htEventInfo.startTileX = startTileX;
        this._htEventInfo.startTileY = startTileY;
        this._htEventInfo.endTileX = endTileX;
        this._htEventInfo.endTileY = endTileY;
        
        // 나간 타일 회수
        var bChanged = this._releaseTiles(startTileX, startTileY, endTileX, endTileY);
        
        // 없는 타일 생성
        for (var row = startTileY; row <= endTileY; row++) {
            for (var col = startTileX; col <= endTileX; col++) {
                this._createTile(col, row);
            }
        }
        
        // 바뀐게 있다면 이벤트 발생
        if (bChanged) {
            /**
             * 맵의 타일이 변경됐을 때
             * @name collie.Map#mapchange
             * @event
             * @param {Object} htEvent
             * @param {Number} htEvent.x 컨테이너 x 좌표
             * @param {Number} htEvent.y 컨테이너 y 좌표
             * @param {Number} htEvent.startTileX 현재 화면 안에 있는 타일의 시작 인덱스
             * @param {Number} htEvent.startTileY 현재 화면 안에 있는 타일의 시작 인덱스
             * @param {Number} htEvent.endTileX 현재 화면 안에 있는 타일의 끝 인덱스
             * @param {Number} htEvent.endTileY 현재 화면 안에 있는 타일의 끝 인덱스
             * @param {HTMLEvent} htEvent.event 이벤트 객체
             */
            this.fireEvent("mapchange", this._htEventInfo);
        }
    },
    
    /**
     * 한 화면에 보이는 최대 타일 갯수 반환
     * 
     * @return {Number}
     */
    getTileCountOnScreen : function () {
        var nColCount = Math.ceil(this._htLayerOption.width / this._nTileWidth) + 2;
        var nRowCount = Math.ceil(this._htLayerOption.height / this._nTileHeight) + 2;
        return nColCount * nRowCount;
    }
}, collie.Component);
comments powered by Disqus