com.aerohive.nms.web.config.lbs.services.HmFolderServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.aerohive.nms.web.config.lbs.services.HmFolderServiceImpl.java

Source

package com.aerohive.nms.web.config.lbs.services;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.imageio.ImageIO;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;

import com.aerohive.afs.client.service.ClientFileService;
import com.aerohive.core.base.exception.AhCarrierException;
import com.aerohive.core.base.exception.BadRequestException;
import com.aerohive.core.base.exception.BaseErrorCodes;
import com.aerohive.core.base.exception.InternalServerException;
import com.aerohive.core.config.EnvType;
import com.aerohive.core.data.jdbc.TransactionManagerNames;
import com.aerohive.core.logging.AHLogger;
import com.aerohive.core.logging.LoggerFactory;
import com.aerohive.core.service.VoDoUtil;
import com.aerohive.core.service.annotation.TxMgr;
import com.aerohive.core.util.coder.AhDecoder;
import com.aerohive.core.util.coder.AhEncoder;
import com.aerohive.nms.core.data.inventory.AhDeviceAdminState;
import com.aerohive.nms.core.data.inventory.AhDeviceProperties.AP_MODEL;
import com.aerohive.nms.core.data.inventory.AhProductType;
import com.aerohive.nms.core.util.HmFileServerUrlFactory;
import com.aerohive.nms.core.util.TarArchive;
import com.aerohive.nms.data.base.HiveResourceShardedDataCache;
import com.aerohive.nms.data.cache.config.service.CoverageCacheService;
import com.aerohive.nms.data.config.lbs.model.HmFolder;
import com.aerohive.nms.data.config.lbs.model.HmFolder.EnvironmentType;
import com.aerohive.nms.data.config.lbs.model.HmFolder.FolderType;
import com.aerohive.nms.data.config.lbs.model.HmFolder.IconType;
import com.aerohive.nms.data.config.lbs.model.HmFolder.LengthUnit;
import com.aerohive.nms.data.config.lbs.model.HmFolder.ResizePosition;
import com.aerohive.nms.data.config.lbs.model.HmImageMetadata;
import com.aerohive.nms.data.config.lbs.model.HmPlanningConfig;
import com.aerohive.nms.data.config.lbs.model.HmVertex;
import com.aerohive.nms.data.config.lbs.model.HmVertex.VertexType;
import com.aerohive.nms.data.config.lbs.model.HmWall;
import com.aerohive.nms.data.config.lbs.model.HmWall.WallType;
import com.aerohive.nms.data.config.lbs.repositories.HmFolderRepository;
import com.aerohive.nms.data.config.lbs.repositories.HmImageMetadataRepository;
import com.aerohive.nms.data.config.lbs.repositories.HmPlanningConfigRepository;
import com.aerohive.nms.data.inventory.device.model.HmDevice;
import com.aerohive.nms.data.inventory.device.repositories.HmDeviceRepository;
import com.aerohive.nms.data.inventory.extension.model.HmApPlatformSpec;
import com.aerohive.nms.data.inventory.extension.model.HmDeviceLocationEx;
import com.aerohive.nms.data.inventory.extension.model.HmDevicePlanningEx;
import com.aerohive.nms.data.inventory.extension.repositories.HmApPlatformSpecRepository;
import com.aerohive.nms.data.inventory.extension.repositories.HmDeviceLocationExRepository;
import com.aerohive.nms.data.inventory.extension.repositories.HmDevicePlanningExRepository;
import com.aerohive.nms.data.monitoring.model.HmActiveClientsPerDevice;
import com.aerohive.nms.data.monitoring.model.HmNeighborStat;
import com.aerohive.nms.data.monitoring.model.HmNeighborStats;
import com.aerohive.nms.data.monitoring.model.HmWifiInterfaceStat;
import com.aerohive.nms.data.monitoring.model.HmWifiInterfaceStats;
import com.aerohive.nms.data.monitoring.service.HmNearRealTimeStatsService;
import com.aerohive.nms.data.system.model.HmVhmAccount;
import com.aerohive.nms.web.config.lbs.model.vo.HmFileUploadResponsedVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmFolderBuildingViewVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmFolderBuildingViewVo.HmFolderFloorViewVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmFolderDataConvertVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmFolderDetailsVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmFolderElementsVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmFolderGeoVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmFolderHierarchyVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmFolderInitVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmFolderLinksVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmFolderNetworkSummaryVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmFolderOffsetVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmFolderResizeVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmFolderVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmGeoPerimeterInvalidVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmGeoPerimeterVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmGeoVertexVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmIconMoveVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmPerimeterInvalidPointsVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmPerimeterVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmPerimetersInFolderVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmVertexVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmWallVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmWallsInFolderVo;
import com.aerohive.nms.web.config.lbs.model.vo.HmWirelessLinkVo;
import com.aerohive.nms.web.inventory.extension.model.vo.HmDeviceLocationExVo;
import com.aerohive.nms.web.inventory.extension.model.vo.HmDevicePlanningExVo;
import com.aerohive.nms.web.services.common.HmSimpleCrudServiceImpl;

/**
 * IMPORTANT: This is a generated source file, make sure you only add custom
 * implementation within BEGIN and END demarcated blocks. This ensures that your
 * custom code do not get overwritten during source code regeneration. There
 * MUST NOT be overlapping blocks
 * 
 * An example is as follows:
 * 
 * // CUSTOM_CODE 1 BEGIN ... ... ==> Here goes your implementation ... ... //
 * CUSTOM_CODE 1 END
 * 
 * NOTE: JAVA IMPORT LINES DO NOT NEED TO BE CONSIDERED.
 */
// CUSTOM_CODE_BLOCK 3 BEGIN
@Service("folderService")
@TxMgr(TransactionManagerNames.CONFIGDB_TX_MGR)
public class HmFolderServiceImpl extends HmSimpleCrudServiceImpl<HmFolderVo> implements HmFolderService {
    // CUSTOM_CODE_BLOCK 3 END

    @Autowired
    private HmFolderRepository rep;
    @Autowired
    private HmDeviceLocationExRepository deviceLocationExRep;
    @Autowired
    private HmDevicePlanningExRepository devicePlanningExRep;
    @Autowired
    private HmPlanningConfigRepository planningConfigRep;
    @Autowired
    private HmDeviceRepository devRep;
    @Autowired
    private HmApPlatformSpecRepository apRep;
    @Autowired
    private HmImageMetadataRepository imageRep;
    @Autowired
    private HmNearRealTimeStatsService statsService;
    @Autowired
    protected ClientFileService clientFileService;
    @Autowired
    protected HmFileServerUrlFactory urlFactory;
    @Autowired
    private VoDoUtil voDoUtil;
    @Autowired
    private CoverageCacheService cvCache;
    @Autowired
    private HmImageMetadataService imageService;

    // CUSTOM_CODE_BLOCK 1 BEGIN

    protected AHLogger logger = LoggerFactory.getLogger(getClass());

    @Value("${app.environment}")
    private EnvType environment;
    @Value("${gm.license.key}")
    private String licenseKey;
    @Value("${gm.api.key}")
    private String apiKey;

    private static double FEET_TO_METERS = 0.3048;
    private static double MAX_AP_ELEVATION = 15.0; // meters

    private Color[] startChannelColors = new Color[] { new Color(255, 0, 0), new Color(0, 255, 0),
            new Color(0, 0, 255), new Color(255, 255, 0), new Color(0, 255, 255), new Color(255, 0, 255),
            new Color(255, 128, 0), new Color(67, 29, 95), new Color(0, 130, 60), new Color(128, 255, 0),
            new Color(128, 0, 255), new Color(0, 128, 255) };

    @Override
    @Transactional
    public void delete(Serializable id) throws Exception {
        logger.info(String.format("Remove %s and all its subfolders.", id));
        HmFolder f = rep.findOne((Long) id);
        if (f == null) {
            throw new BadRequestException(ErrorCodes.removeFolderNotExist);
        }
        //is root folder?
        HmFolder root = findRootFolder(f.getOwnerId());
        if (root != null && f.getId().equals(root.getId())) {
            throw new BadRequestException(ErrorCodes.rootFolderCannotBeRemoved);
        }
        removeFolder(rep.findOne((Long) id));
    }

    @Override
    @Transactional
    public HmFolderVo create(HmFolderVo vo) throws Exception {
        logger.info("Creating folder: " + vo.getName());
        HmFolder f = (HmFolder) voDoUtil.toDo(vo);
        f = (HmFolder) rep.saveAndFlush(f);
        vo = voDoUtil.toVo(vo.getClass(), f);
        return vo;
    }

    @Override
    public HmFolderVo findFolder(Long folderId) throws Exception {
        logger.debug(String.format("find folder with id %s", folderId));
        if (null == folderId) {
            return null;
        }
        return voDoUtil.toVo(HmFolderVo.class, rep.findOne(folderId));
    }

    @Override
    @Transactional
    public void init(HmFolderInitVo vo) throws Exception {
        Page<HmFolder> folders = rep.findByOwnerId(vo.getOwnerId(), DEFAULT_PAGEREQUEST);
        if (folders.getTotalElements() > 0) {
            logger.error(String.format("Map has been initialized already for ownerId %s", vo.getOwnerId()));
            throw new BadRequestException(ErrorCodes.folderAlreadyInitialized);
        }
        // create root folders
        HmFolderVo organizationVo = newHmFolderVo(vo.getOwnerId(), null, vo.getOrganization());
        HmFolder organization = (HmFolder) voDoUtil.toDo(organizationVo);
        organization.setCenterZoom((short) 2);
        organization = rep.save(organization);
        HmFolderVo cityVo = newHmFolderVo(vo.getOwnerId(), organization, vo.getCity());
        HmFolder city = (HmFolder) voDoUtil.toDo(cityVo);
        city.setFolderType(FolderType.GENERIC);
        city.setIconType(IconType.AREA);
        city.setEnvironmentType(EnvironmentType.OUTDOOR_FREE_SPACE);
        city.setCenterZoom((short) 12);
        city.setAddress(vo.getStreet() + ", " + vo.getCity());
        city = rep.save(city);
        HmFolderVo streetVo = newHmFolderVo(vo.getOwnerId(), city, vo.getStreet());
        HmFolder street = (HmFolder) voDoUtil.toDo(streetVo);
        street.setFolderType(FolderType.BUILDING);
        street.setIconType(IconType.BUILDING);
        street.setEnvironmentType(EnvironmentType.OUTDOOR_FREE_SPACE);
        street.setAddress(vo.getStreet() + ", " + vo.getCity());
        rep.save(street);
        // save country code
        HmPlanningConfig planningConfig = planningConfigRep.findByOwnerId(vo.getOwnerId());
        if (null == planningConfig) {
            planningConfig = new HmPlanningConfig();
            planningConfig.setOwnerId(vo.getOwnerId());
        }
        planningConfig.setDefaultCountryCode(vo.getCountryCode());
        planningConfigRep.save(planningConfig);
    }

    private HmFolderVo newHmFolderVo(Long ownerId, HmFolder parent, String name) {
        HmFolderVo folder = new HmFolderVo();
        folder.setFolderType(HmFolder.FolderType.GENERIC.toString());
        folder.setOwnerId(ownerId);
        folder.setName(name);
        folder.setFloorLoss(HmFolder.DEFAULT_FLOOR_LOSS);
        folder.setEnvironmentType(EnvironmentType.AUTO.toString());
        folder.setIconType(IconType.REGION.toString());
        folder.setLengthUnit(LengthUnit.METERS.toString());
        folder.setDeviceElevation(HmFolder.DEFAULT_DEVICE_ELEVATION);
        folder.setFloorLoss(HmFolder.DEFAULT_FLOOR_LOSS);
        folder.setMapOrder(1);
        if (null != parent) {
            folder.setParentId(parent.getId());
        }
        return folder;
    }

    @Override
    public HmFolderHierarchyVo getFolderHierarchyTree(Long ownerId, boolean isAlpha) throws Exception {
        HmFolder o = findRootFolder(ownerId);
        if (null != o) {
            HmFolderHierarchyVo vo = getHmFolderHierarchyVo(o);
            loadFolderHierarchy(o, vo, isAlpha);
            return vo;
        } else {
            return null;
        }
    }

    private void loadFolderHierarchy(HmFolder o, HmFolderHierarchyVo vo, boolean isAlpha) throws Exception {
        List<HmFolder> children = rep.findChildFolders(o.getId());
        if (null != children && !children.isEmpty()) {
            if (isAlpha) {
                if (children.get(0).getFolderType() != FolderType.FLOOR) {
                    Collections.sort(children, new Comparator<HmFolder>() {
                        @Override
                        public int compare(HmFolder o1, HmFolder o2) {
                            return o1.getName().compareToIgnoreCase(o2.getName());
                        }
                    });
                } else {
                    Collections.sort(children, new Comparator<HmFolder>() {
                        @Override
                        public int compare(HmFolder o1, HmFolder o2) {
                            return o1.getMapOrder() - o2.getMapOrder();
                        }
                    });
                }
            } else {
                if (children.get(0).getFolderType() != FolderType.FLOOR) {
                    Collections.sort(children, new Comparator<HmFolder>() {
                        @Override
                        public int compare(HmFolder o1, HmFolder o2) {
                            return o1.getMapOrder() - o2.getMapOrder();
                        }
                    });
                } else {
                    Collections.sort(children, new Comparator<HmFolder>() {
                        @Override
                        public int compare(HmFolder o1, HmFolder o2) {
                            return o2.getMapOrder() - o1.getMapOrder();
                        }
                    });
                }
            }
            for (HmFolder child : children) {
                HmFolderHierarchyVo childVo = getHmFolderHierarchyVo(child);
                vo.addFolder(childVo);
                loadFolderHierarchy(child, childVo, isAlpha);
            }
        }
    }

    private HmFolderHierarchyVo getHmFolderHierarchyVo(HmFolder folder) {
        HmFolderHierarchyVo vo = new HmFolderHierarchyVo();
        vo.setId(folder.getId());
        vo.setName(folder.getName());
        vo.setFolderType(folder.getFolderType().toString());
        return vo;
    }

    @Override
    @Transactional
    public HmFolderVo createFolder(HmFolderVo vo, Long previousId) throws Exception {
        HmFolder f = (HmFolder) voDoUtil.toDo(vo);
        HmFolder parent = f.getParent();
        if (null == parent) {
            logger.error(String.format("Cannot find folder with id %s", vo.getParentId()));
            throw new BadRequestException(ErrorCodes.folderNotExisted,
                    new Object[][] { { "id", vo.getParentId() } });
        }
        if (FolderType.FLOOR.equals(parent.getFolderType())) {
            throw new BadRequestException(ErrorCodes.cannotCreateFolderUnderFloor);
        }
        if (FolderType.BUILDING.equals(parent.getFolderType()) && (!FolderType.FLOOR.equals(f.getFolderType()))) {
            throw new BadRequestException(ErrorCodes.cannotCreateBuildingUnderBuilding);
        }
        if (FolderType.GENERIC.equals(parent.getFolderType()) && FolderType.FLOOR.equals(f.getFolderType())) {
            throw new BadRequestException(ErrorCodes.cannotCreateFloorUnderFolder);
        }
        if (isNameDuplicated(vo.getName(), parent)) {
            logger.error(String.format(
                    "You cannot create a duplicate name child with other children, parent name %s, child name %s",
                    parent.getName(), vo.getName()));
            throw new BadRequestException(ErrorCodes.duplicateNameCannotBeCreated,
                    new Object[][] { { "name", vo.getName() } });
        }
        //set Image height and width size
        setFolderImageSize(f);
        setDeviceElevation(f);
        f.setParent(parent);
        Set<HmFolder> penddingFolders = new HashSet<>();
        List<HmFolder> children = findChildNodes(vo.getParentId());
        if (null == children || children.isEmpty()) {
            f.setMapOrder(1);
        } else {
            HmFolder preFolder;
            if (null != previousId) {
                preFolder = rep.findOne(previousId);
                if (null == preFolder) {
                    // cannot find previous node, try to add as no previous
                    // folder
                    logger.warn(String.format("Cannot find folder with id %s, just create folder under parent %s",
                            previousId, parent.getName()));
                    addToParent(f, parent, children);
                } else {
                    // add folder under previous folder
                    addToPrevious(preFolder, f, children, penddingFolders);
                }
            } else {
                addToParent(f, parent, children);
            }
        }

        for (HmFolder penddingFolder : penddingFolders) {
            rep.save(penddingFolder);
        }

        logger.info(String.format("Adding child node %s to parent %s", vo.getName(), parent.getName()));
        f = (HmFolder) rep.saveAndFlush(f);
        // Just to show that refreshing parent fails from proxy object, when
        // in same transaction of creating the child
        /*-
        parent = f.getParent();
        logger.info("Refreshing parent: " + parent.getName());
        parent = rep.findOne(parent.getId());
        if (parent.getFolders() == null) {
        logger.info("Why are folders null ?");
        }else {
        logger.info(String.format("Folder %s has %d child nodes.", parent.getName(), parent.getFolders().size()));
        }
        List<HmFolder> folders = findChildNodes(parentId);
        logger.info("Did it find all the folders ? " + folders.size());
        */
        vo = voDoUtil.toVo(vo.getClass(), f);
        return vo;
    }

    private void addToParent(HmFolder folder, HmFolder parent, List<HmFolder> children) {
        int maxMapOrder = findMaxMapOrder(children);
        logger.info(String.format("max map order under folder %s is %d", parent.getName(), maxMapOrder));
        folder.setMapOrder(maxMapOrder + 1);
    }

    private void addToPrevious(HmFolder preFolder, HmFolder folder, List<HmFolder> children,
            Set<HmFolder> penddingFolders) {
        int preMapOrder = preFolder.getMapOrder();
        List<HmFolder> nexts = findNextFolders(preFolder, children);
        logger.info(String.format("have # %d of folders next to folder %s, need to update their order.",
                nexts.size(), preFolder.getName()));
        folder.setMapOrder(preMapOrder);
        for (HmFolder next : nexts) {
            next.setMapOrder(next.getMapOrder() + 1);
            penddingFolders.add(next);
        }
    }

    private int findMaxMapOrder(List<HmFolder> folders) {
        int mapOrder = 0;
        for (HmFolder folder : folders) {
            if (mapOrder < folder.getMapOrder()) {
                mapOrder = folder.getMapOrder();
            }
        }
        return mapOrder;
    }

    private List<HmFolder> findNextFolders(HmFolder folder, List<HmFolder> folders) {
        Integer order = folder.getMapOrder();
        List<HmFolder> nexts = new ArrayList<>();
        for (HmFolder f : folders) {
            if (order.intValue() <= f.getMapOrder().intValue()) {
                nexts.add(f);
            }
        }
        return nexts;
    }

    @Override
    @Transactional
    public HmFolderVo updateFolder(HmFolderVo vo) throws Exception {
        logger.info(String.format("Update folder which id is %d, name is %s", vo.getId(), vo.getName()));
        HmFolder target = rep.findOne(vo.getId());
        if (null == target) {
            logger.error(String.format("Cannot find folder with id %s", vo.getId()));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", vo.getId() } });
        }
        Integer originalWidth = target.getImageHeight();
        Integer originalHeight = target.getImageHeight();
        boolean nameChanged = !target.getName().equals(vo.getName());

        //HmFolder f = (HmFolder) voDoUtil.toDo(vo);
        HmFolder parent = target.getParent();
        if (nameChanged && isNameDuplicated(vo.getName(), parent)) {
            logger.error(String.format(
                    "You cannot changed the name duplicate with other children, parent name %s, child name %s",
                    parent == null ? "null" : parent.getName(), vo.getName()));
            throw new BadRequestException(ErrorCodes.duplicateNameCannotBeUpdated,
                    new Object[][] { { "name", vo.getName() } });
        }
        target.setName(vo.getName());
        target.setIconType(IconType.valueOf(vo.getIconType()));
        if (target.getFolderType() != FolderType.BUILDING) {
            target.setEnvironmentType(EnvironmentType.valueOf(vo.getEnvironmentType()));
            target.setBackground(vo.getBackground());
            target.setMetricHeight(vo.getMetricHeight());
            target.setMetricWidth(vo.getMetricWidth());
            target.setDeviceElevation(vo.getDeviceElevation());
        }
        if (nameChanged) {
            setFolderUniqueName(target);
            if (FolderType.BUILDING == target.getFolderType()) {
                // Update child unique names
                List<HmFolder> childNodes = findChildNodes(target.getId());
                if (null != childNodes && !childNodes.isEmpty()) {
                    for (HmFolder childNode : childNodes) {
                        setFolderUniqueName(childNode);
                    }
                    rep.save(childNodes);
                }
            }
        }
        setFolderImageSize(target);
        setDeviceElevation(target);

        if (null == originalWidth) {
            originalWidth = 0;
        }
        if (null == originalHeight) {
            originalHeight = 0;
        }
        boolean sizeChanged = !target.getImageWidth().equals(originalWidth)
                || !target.getImageHeight().equals(originalHeight);
        logger.info("Is the folder: " + target.getName() + " size changed? " + sizeChanged);
        if (FolderType.BUILDING != target.getFolderType() && sizeChanged && originalWidth != 0
                && originalHeight != 0) {
            float widthRatio = target.getImageWidth().floatValue() / originalWidth.floatValue();
            float heightRatio = target.getImageHeight().floatValue() / originalHeight.floatValue();
            if (widthRatio != 0 && heightRatio != 0) {
                updateNodeCoordinates(target, widthRatio, heightRatio);
            }
        }

        if (target.getParent() == null) {
            logger.info("Update Root Folder :  " + vo.getName());
        }
        HmFolder f = (HmFolder) rep.saveAndFlush(target);
        vo = voDoUtil.toVo(vo.getClass(), f);
        return vo;
    }

    private void updateNodeCoordinates(HmFolder target, float widthRatio, float heightRatio) {
        List<HmWall> walls = target.getWalls();
        if (!walls.isEmpty()) {
            for (HmWall wall : walls) {
                wall.setX1(wall.getX1() * widthRatio);
                wall.setY1(wall.getY1() * heightRatio);
                wall.setX2(wall.getX2() * widthRatio);
                wall.setY2(wall.getY2() * heightRatio);
            }
        }

        List<HmVertex> perimeters = target.getPerimeter();
        if (!perimeters.isEmpty()) {
            for (HmVertex vertex : perimeters) {
                vertex.setX(vertex.getX() * widthRatio);
                vertex.setY(vertex.getY() * heightRatio);
            }
        }

        List<HmDeviceLocationEx> liveDevices = deviceLocationExRep.findAllHmDevices(target.getId());
        if (!liveDevices.isEmpty()) {
            for (HmDeviceLocationEx liveDevice : liveDevices) {
                liveDevice.setX(liveDevice.getX() * widthRatio);
                liveDevice.setY(liveDevice.getY() * heightRatio);
            }
            deviceLocationExRep.save(liveDevices);
        }

        List<HmDevicePlanningEx> plannedDevices = devicePlanningExRep.findAllPlannedDevices(target.getId());
        if (!plannedDevices.isEmpty()) {
            for (HmDevicePlanningEx plannedDevice : plannedDevices) {
                plannedDevice.setX(plannedDevice.getX() * widthRatio);
                plannedDevice.setY(plannedDevice.getY() * heightRatio);
            }
            devicePlanningExRep.save(plannedDevices);
        }
    }

    @Override
    @Transactional
    public boolean reorderFolder(Long floorId, boolean up) {
        if (floorId == 0)
            return false;

        HmFolder currentFolder = rep.findOne(floorId);
        HmFolder parent = rep.findOne(currentFolder.getParent().getId());
        List<HmFolder> childrenList = rep.findChildFolders(parent.getId());
        sortList(childrenList);
        for (int i = 0; i < childrenList.size(); i++) {
            HmFolder f = childrenList.get(i);
            if (f.getId().equals(floorId)) {
                HmFolder f2 = null;
                if (f.getFolderType() == FolderType.FLOOR) {
                    logger.info("Move this floor: " + f.getName());
                    f2 = up ? childrenList.get(i + 1) : childrenList.get(i - 1);
                } else {
                    logger.info("Move this building or folder: " + f.getName());
                    f2 = up ? childrenList.get(i - 1) : childrenList.get(i + 1);
                }
                if (f2 == null)
                    return false;
                int f2Order = f2.getMapOrder();
                f2.setMapOrder(f.getMapOrder());
                f.setMapOrder(f2Order);
                rep.saveAndFlush(f2);
                rep.saveAndFlush(f);
            }

        }
        return true;
    }

    @Override
    @Transactional
    public HmFolderVo cloneFolder(Long fromId, Long toId, String toName) throws Exception {
        HmFolder clonedFolder;
        HmFolder fromFolder = rep.findOne(fromId);
        if (fromFolder == null) {
            logger.error(String.format("Cannot find folder with id %s", fromId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", fromId } });
        }
        HmFolder parent = fromFolder.getParent();

        if (FolderType.FLOOR == fromFolder.getFolderType()) {
            int maxOrder = 0;
            List<HmFolder> children = findChildNodes(parent.getId());
            if (null != children && !children.isEmpty()) {
                if (isNameDuplicated(toName, parent)) {
                    logger.error(String.format(
                            "You cannot clone a floor in the builing with duplicate name , building name %s, floor name %s",
                            parent.getName(), toName));
                    throw new BadRequestException(ErrorCodes.duplicateNameCannotBeCreated,
                            new Object[][] { { "name", toName } });
                }
                maxOrder = findMaxMapOrder(children);
            }
            logger.info(String.format("max map order value under node %s is: %d", parent.getName(), maxOrder));
            clonedFolder = cloneAndSaveFolder(fromFolder, toName, maxOrder + 1, null);
        } else if (FolderType.BUILDING == fromFolder.getFolderType()) {
            HmFolder toFolder;
            if (null == toId) {
                logger.info(String.format("assume to clone building under the same parent: %s", parent.getName()));
                toFolder = parent;
            } else {
                toFolder = rep.findOne(toId);
                if (toFolder == null) {
                    logger.error(String.format("Cannot find folder with id %s", toId));
                    throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", toId } });
                }
            }
            if (FolderType.GENERIC != toFolder.getFolderType()) {
                logger.error(String.format(
                        "You cannot clone a building into a node which is not a folder, folder name %s, type %s",
                        toFolder.getName(), toFolder.getFolderType().toString()));
                throw new BadRequestException(ErrorCodes.buildingCannotBeAssigned,
                        new Object[][] { { "name", toFolder.getName() } });
            }
            if (isNameDuplicated(toName, toFolder)) {
                logger.error(String.format(
                        "You cannot clone a building with duplicate name in folder , folder name %s, building name %s",
                        toFolder.getName(), toName));
                throw new BadRequestException(ErrorCodes.duplicateNameCannotBeCreated,
                        new Object[][] { { "name", toName } });
            }
            int maxOrder = 0;
            List<HmFolder> children = findChildNodes(toFolder.getId());
            if (null != children && !children.isEmpty()) {
                maxOrder = findMaxMapOrder(children);
            }
            logger.info(String.format("max map order value under node %s is: %d", toFolder.getName(), maxOrder));
            clonedFolder = cloneAndSaveFolder(fromFolder, toName, maxOrder + 1, toFolder);
            children = findChildNodes(fromFolder.getId());
            for (HmFolder child : children) {
                cloneAndSaveFolder(child, child.getName(), child.getMapOrder(), clonedFolder);
            }
        } else {
            logger.error(String.format("You cannot clone folder type node %s", fromFolder.getName()));
            throw new BadRequestException(ErrorCodes.folderCannotBeCloned,
                    new Object[][] { { "folderName", fromFolder.getName() } });
        }
        HmFolderVo vo = voDoUtil.toVo(HmFolderVo.class, clonedFolder);
        return vo;
    }

    private HmFolder cloneAndSaveFolder(HmFolder folder, String name, int order, HmFolder toFolder) {
        HmFolder cloned = deepCloneFolder(folder);

        cloned.setId(null);
        cloned.setUpdatedAt(null);
        cloned.setName(name);
        cloned.setMapOrder(order + 1);
        if (toFolder != null) {
            cloned.setParent(toFolder);
        }
        setFolderUniqueName(cloned);
        cloned = rep.saveAndFlush(cloned);
        return cloned;
    }

    private void setFolderUniqueName(HmFolder folder) {
        HmFolder parent = folder.getParent();
        FolderType type = folder.getFolderType();
        switch (type) {
        case GENERIC:
        case BUILDING:
            folder.setUniqueName(folder.getName());
            break;
        case FLOOR:
            if (null != parent) {
                folder.setUniqueName(parent.getName() + "|" + folder.getName());
            }
            break;
        }
    }

    private HmFolder deepCloneFolder(HmFolder folder) {
        HmFolder cloned = folder.clone();

        List<HmWall> walls = folder.getWalls();
        List<HmVertex> perimeters = folder.getPerimeter();
        List<HmWall> clonedWalls = new ArrayList<>();
        List<HmVertex> clonedPerimeters = new ArrayList<>();
        for (HmWall wall : walls) {
            HmWall clonedWall = wall.clone();
            clonedWalls.add(clonedWall);
        }
        for (HmVertex vertex : perimeters) {
            HmVertex clonedVertex = vertex.clone();
            clonedPerimeters.add(clonedVertex);
        }
        cloned.setWalls(clonedWalls);
        cloned.setPerimeter(clonedPerimeters);
        return cloned;
    }

    @Override
    @Transactional
    public HmFolderVo updateFolderGeoInfo(Long folderId, HmFolderGeoVo vo) throws Exception {
        logger.debug(String.format("to update folder geo info with id %s", folderId));
        HmFolder folder = rep.findOne(folderId);
        if (null == folder) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        if (StringUtils.isEmpty(vo.getViewType())) {
            // update lat/long of folder
            folder.setLatitude(vo.getLatitude());
            folder.setLongitude(vo.getLongitude());
        } else {
            // update center lat/long of folder
            folder.setCenterLatitude(vo.getCenterLatitude());
            folder.setCenterLongitude(vo.getCenterLongitude());
            folder.setCenterZoom(vo.getCenterZoom());
            folder.setViewType(vo.getViewType());
        }
        folder = (HmFolder) rep.saveAndFlush(folder);
        HmFolderVo fvo = voDoUtil.toVo(HmFolderVo.class, folder);
        return fvo;
    }

    @Override
    @Transactional
    public boolean moveFolder(Long fromId, Long toId) throws BadRequestException {
        HmFolder fromFolder = rep.findOne(fromId);
        HmFolder toFolder = rep.findOne(toId);
        if (fromFolder == null || toFolder == null) {
            logger.error(String.format("Cannot find folder with id %s", fromFolder == null ? fromId : toId));
            throw new BadRequestException(ErrorCodes.folderNotExisted,
                    new Object[][] { { "id", fromFolder == null ? fromId : toId } });
        } //don't need verify duplicate name.
        if (FolderType.FLOOR.equals(fromFolder.getFolderType()))
            throw new BadRequestException(ErrorCodes.floorCannotBeMoved);
        if (!FolderType.GENERIC.equals(toFolder.getFolderType())) {
            throw new BadRequestException(ErrorCodes.targetParentMustBeFolder);
        }
        if (isEqualAnyChild(fromFolder, toFolder)) {
            throw new BadRequestException(ErrorCodes.cannotMoveToOwnSubFolder);
        }
        Integer order = rep.findMaxMapOrder(toFolder.getId());
        order = (order == null ? 0 : order);
        fromFolder.setParent(toFolder);
        fromFolder.setMapOrder(order + 1);
        rep.saveAndFlush(fromFolder);
        return true;
    }

    private boolean isEqualAnyChild(HmFolder father, HmFolder sample) {
        List<HmFolder> children = findChildNodes(father.getId());
        if (children == null)
            return false;
        for (HmFolder child : children) {
            if (FolderType.GENERIC.equals(child.getFolderType())) {
                if (sample.getId().equals(child.getId())) {
                    return true;
                } else if (isEqualAnyChild(child, sample)) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    @Transactional
    public void resizeFolder(Long folderId, HmFolderResizeVo vo) throws Exception {
        logger.debug(String.format("resize folder %s", folderId));
        HmFolder folder = rep.findOne(folderId);
        if (null == folder) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        Integer width = folder.getImageWidth();
        Integer height = folder.getImageHeight();
        if (null == width || null == height || 0 == width || 0 == height) {
            logger.error(
                    String.format("Cannot resize folder %s, as its image width %s or image height %s is null value",
                            folder.getName(), width, height));
            throw new BadRequestException(ErrorCodes.folderCannotBeResized,
                    new Object[][] { { "name", folder.getName() } });
        }

        LengthUnit unit = LengthUnit.valueOf(vo.getLengthUnit());
        folder.setLengthUnit(unit);
        if (null == folder.getMetricWidth()) {
            folder.setDeviceElevation(getDefaultDeviceElevation(unit));
        }
        if (ResizePosition.VERTICAL.toString().equals(vo.getResizePosition())) {
            Double actualHeight = vo.getActualLength() / vo.getSizeLength() * folder.getImageHeight();
            Double actualWidth = actualHeight * folder.getImageWidth() / folder.getImageHeight();
            folder.setMetricWidth(actualWidth);
            folder.setMetricHeight(actualHeight);
        } else {
            Double actualWidth = vo.getActualLength() / vo.getSizeLength() * folder.getImageWidth();
            Double actualHeight = actualWidth * folder.getImageHeight() / folder.getImageWidth();
            folder.setMetricWidth(actualWidth);
            folder.setMetricHeight(actualHeight);
        }
        rep.saveAndFlush(folder);
        logger.info(String.format("resize folder %s to width %f, height %f", folder.getName(),
                folder.getMetricWidth(), folder.getMetricHeight()));
    }

    protected static Double getDefaultDeviceElevation(LengthUnit unit) {
        if (LengthUnit.FEET.equals(unit)) {
            return HmFolder.DEFAULT_DEVICE_ELEVATION / HmFolder.FEET_TO_METERS;
        } else {
            return HmFolder.DEFAULT_DEVICE_ELEVATION;
        }
    }

    @Override
    @Transactional
    public void offsetFolder(Long folderId, HmFolderOffsetVo vo) throws Exception {
        logger.debug(String.format("offset folder %s", folderId));
        HmFolder folder = rep.findOne(folderId);
        if (null == folder) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        if (FolderType.FLOOR != folder.getFolderType()) {
            logger.error(String.format("Cannot set offset for folder %s as it is not a floor.", folder.getName()));
            throw new BadRequestException(ErrorCodes.folderCannotBeAligned,
                    new Object[][] { { "name", folder.getName() } });
        }
        folder.setOffsetX(vo.getOffsetX());
        folder.setOffsetY(vo.getOffsetY());
        rep.saveAndFlush(folder);
    }

    @Override
    @Transactional
    public HmFolderVo moveFolderIcon(Long id, HmIconMoveVo vo) throws Exception {
        logger.info("Moving folder icon: " + id);
        HmFolder f = rep.findOne(id);
        if (null == f) {
            logger.error(String.format("Cannot find folder with id %s", id));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", id } });
        }
        f.setX(vo.getToX());
        f.setY(vo.getToY());
        f = rep.saveAndFlush(f);
        HmFolderVo fvo = voDoUtil.toVo(HmFolderVo.class, f);
        fvo.setX(f.getX());
        fvo.setY(f.getY());
        return fvo;
    }

    @Override
    public HmFolderDetailsVo findFolderDetails(Long folderId) throws Exception {
        HmFolder f = rep.findOne(folderId);
        if (null == f) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        HmFolderDetailsVo fd = new HmFolderDetailsVo();
        fd.setBackground(f.getBackground());
        fd.setImageWidth(f.getImageWidth());
        fd.setImageHeight(f.getImageHeight());
        fd.setMetricWidth(f.getMetricWidth());
        fd.setMetricHeight(f.getMetricHeight());
        fd.setFloorLoss(f.getFloorLoss());
        fd.setDeviceElevation(f.getDeviceElevation());
        fd.setEnvironmentType(f.getEnvironmentType().toString());
        fd.setLengthUnit(f.getLengthUnit().toString());
        fd.setImageBaseUrl(getImageBaseUrl(f.getOwnerId()));
        fd.setAddress(f.getAddress());
        fd.setLatitude(f.getLatitude());
        fd.setLongitude(f.getLongitude());
        fd.setCenterLatitude(f.getCenterLatitude());
        fd.setCenterLongitude(f.getCenterLongitude());
        fd.setViewType(f.getViewType());
        fd.setCenterZoom(f.getCenterZoom());

        if (null != f.getParent()) {
            fd.setParentCenterZoom(f.getParent().getCenterZoom());
        }

        String gmeKey = getGmeKey(f.getOwnerId());
        fd.setGmeKey(gmeKey);

        Double metricWidth = f.getMetricWidth();
        if (null == metricWidth || 0 == metricWidth) {
            fd.setGridSize(0.0);
        } else {
            Integer imageWidth = f.getImageWidth() == null ? Integer.valueOf(0) : f.getImageWidth();
            Double actualGridSize = getActualGridSize(metricWidth);
            fd.setActualGridSize(actualGridSize);
            fd.setGridSize(actualGridSize * imageWidth / metricWidth);
        }
        long childCount = findChildNodeCount(folderId);
        fd.setChildCount(Long.valueOf(childCount));
        return fd;
    }

    private String getGmeKey(Long ownerId) {
        if (EnvType.Prod == environment) {
            String vhmId;
            // Usage is licensed for these servers
            HmVhmAccount vhmAccount = HiveResourceShardedDataCache.getInstance().findVhmAccountById(ownerId);
            if (null == vhmAccount) {
                vhmId = "no-vhm-id";
            } else {
                vhmId = vhmAccount.getVhmId();
            }
            if (!StringUtils.isEmpty(licenseKey)) {
                return "&client=" + licenseKey + "&channel=" + vhmId;
            } else {
                return "";
            }
        } else {
            // Usage is free for these servers
            if (!StringUtils.isEmpty(apiKey)) {
                return "&key=" + apiKey;
            } else {
                return "";
            }
        }
    }

    private Double getActualGridSize(Double actualSize) {
        int gridCount = 8;
        Double actualGridSize = actualSize / gridCount;
        Double scale = 1.0;
        while (actualGridSize >= 10) {
            actualGridSize = actualGridSize / 10;
            scale *= 10;
        }
        if (actualGridSize > 5.5) {
            actualGridSize = 10 * scale;
        } else if (actualGridSize > 3.2) {
            actualGridSize = 5 * scale;
        } else if (actualGridSize > 1.2) {
            actualGridSize = 2.5F * scale;
        } else {
            actualGridSize = scale;
        }
        if ((int) (actualSize / actualGridSize) > gridCount
                || Math.abs(actualGridSize.intValue() - actualGridSize) > 0) {
            actualGridSize *= 2;
        }

        return actualGridSize;
    }

    @Override
    public HmFolderElementsVo findFolderElements(Long folderId) throws Exception {
        logger.debug(String.format("find elements under folder %s", folderId));
        HmFolder folder = rep.findOne(folderId);
        if (null == folder) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        HmFolderElementsVo vo = new HmFolderElementsVo();
        List<HmWall> walls = folder.getWalls();
        HmWallsInFolderVo wallsInFolder = prepareWalls(walls);
        vo.setWalls(wallsInFolder.getWalls());

        List<HmVertex> perimeters = folder.getPerimeter();
        HmPerimetersInFolderVo perimetersInFolder = preparePerimeters(perimeters);
        vo.setPerimeters(perimetersInFolder.getPerimeters());

        List<HmDeviceLocationEx> liveDevices = deviceLocationExRep.findAllHmDevices(folderId);
        ArrayList<HmDeviceLocationExVo> liveDevicesInFolder = prepareLiveDevices(liveDevices);
        vo.setLiveDevices(liveDevicesInFolder);

        List<HmDevicePlanningEx> plannedDevices = devicePlanningExRep.findAllPlannedDevices(folderId);
        ArrayList<HmDevicePlanningExVo> plannedDevicesInFolder = preparePlannedDevices(plannedDevices);
        vo.setPlannedDevices(plannedDevicesInFolder);

        List<HmFolder> children = rep.findChildFolders(folderId);
        ArrayList<HmFolderVo> childInFolder = prepareChildFolders(children);
        vo.setChildFolders(childInFolder);
        return vo;
    }

    private HmWallsInFolderVo prepareWalls(List<HmWall> walls) {
        HmWallsInFolderVo vo = new HmWallsInFolderVo();
        int from = -1;
        HmWall lastWall = null;
        int[] close = new int[walls.size()];
        for (int i = 0; i < walls.size(); i++) {
            HmWall wall = walls.get(i);
            close[i] = -1;
            if (lastWall != null) {
                if (lastWall.getX2().floatValue() == wall.getX1().floatValue()
                        && lastWall.getY2().floatValue() == wall.getY1().floatValue()) {
                    if (from < 0) {
                        from = i - 1;
                    } else if (wall.getX2().floatValue() == walls.get(from).getX1().floatValue()
                            && wall.getY2().floatValue() == walls.get(from).getY1().floatValue()) {
                        close[i] = from;
                        close[from] = i;
                        from = -1;
                    }
                } else {
                    from = -1;
                }
            }
            if (close[i] < 0) {
                lastWall = wall;
            } else {
                lastWall = null;
            }
        }
        for (int i = 0; i < walls.size(); i++) {
            HmWall wall = walls.get(i);
            logger.debug("Wall close[" + close[i] + "] (" + wall.getX1() + ", " + wall.getY1() + ")  ("
                    + wall.getX2() + ", " + wall.getY2() + ")");
            HmWallVo wallVo = new HmWallVo();
            wallVo.setX1(wall.getX1());
            wallVo.setY1(wall.getY1());
            wallVo.setX2(wall.getX2());
            wallVo.setY2(wall.getY2());
            wallVo.setType(wall.getType().toString());
            if (close[i] != -1) {
                if (close[i] < i) {
                    // From last wall to first in the loop
                    wallVo.setClose(close[i]);
                } else {
                    // At starting wall of the loop
                    wallVo.setClosed(true);
                }
            }
            vo.getWalls().add(wallVo);
        }
        return vo;
    }

    private HmPerimetersInFolderVo preparePerimeters(List<HmVertex> perimeters) {
        HmPerimetersInFolderVo vo = new HmPerimetersInFolderVo();
        int perimId = -1;
        String perimType = null;
        ArrayList<HmVertexVo> vertexVos = null;
        for (HmVertex vertex : perimeters) {
            if (vertex.getId().intValue() != perimId) {
                if (vertexVos != null) {
                    HmPerimeterVo perimeterVo = new HmPerimeterVo();
                    perimeterVo.setId(perimId);
                    perimeterVo.setType(perimType);
                    perimeterVo.setVertexes(vertexVos);
                    vo.getPerimeters().add(perimeterVo);
                }
                vertexVos = new ArrayList<>();
                perimId = vertex.getId();
                perimType = vertex.getType().toString();
            }
            HmVertexVo vertexVo = new HmVertexVo();
            vertexVo.setX(vertex.getX());
            vertexVo.setY(vertex.getY());
            vertexVos.add(vertexVo);
        }
        if (vertexVos != null) {
            HmPerimeterVo perimeterVo = new HmPerimeterVo();
            perimeterVo.setId(perimId);
            perimeterVo.setType(perimType);
            perimeterVo.setVertexes(vertexVos);
            vo.getPerimeters().add(perimeterVo);
        }
        return vo;
    }

    private ArrayList<HmDeviceLocationExVo> prepareLiveDevices(List<HmDeviceLocationEx> devices) throws Exception {
        ArrayList<HmDeviceLocationExVo> vos = new ArrayList<>(devices.size());
        for (HmDeviceLocationEx device : devices) {
            HmDeviceLocationExVo vo = voDoUtil.toVo(HmDeviceLocationExVo.class, device);
            vos.add(vo);
            Long deviceId = device.getDevice().getId();
            String name = device.getDevice().getHostname();
            String ip = device.getDevice().getIpAddress();
            String mask = device.getDevice().getNetMask();
            // check device is wired or not
            boolean wired = true; // assume wired by default for all devices
            HmApPlatformSpec apSpec = apRep.findByDeviceId(deviceId);
            if (null != apSpec) {
                if (AP_MODEL.MP == apSpec.getApType()) {
                    wired = false;
                }
            }
            vo.setWired(wired);
            // subnet information
            if (!StringUtils.isEmpty(ip) && !StringUtils.isEmpty(mask)) {
                String subnet = AhDecoder.int2IP(AhEncoder.ip2Int(ip) & AhEncoder.ip2Int(mask));
                vo.setSubnet(subnet);
            } else {
                logger.warn(
                        String.format("Cannot get subnet information for device %s with its ip %s and netmask %s",
                                name, ip, mask));
            }
            // channel/power and eirp
            List<HmWifiInterfaceStat> stats = getLatestWifiInterfaceStats(deviceId);
            if (null == stats || stats.isEmpty()) {
                logger.info(String.format("No wifi interface information from device %d", deviceId));
                continue;
            }
            for (HmWifiInterfaceStat stat : stats) {
                if (stat.getRadioType() == 1) {// TODO change to constants
                    // 2.4GHz
                    vo.setRadio2GHzChannel((short) stat.getChannelNumber());
                    vo.setRadio2GHzPower((short) stat.getTxPower());
                    vo.setRadio2GHzEirp(stat.getEirp());
                } else {
                    // 5GHz
                    vo.setRadio5GHzChannel((short) stat.getChannelNumber());
                    vo.setRadio5GHzPower((short) stat.getTxPower());
                    vo.setRadio5GHzEirp(stat.getEirp());
                }
            }
            // query active client count
            HmActiveClientsPerDevice stats1 = statsService.getNewestStat(deviceId, HmActiveClientsPerDevice.class);
            if (null != stats1 && null != stats1.getIds()) {
                // right now active client stats is not distinguished with frequency!
                vo.setRadio2GHzClientCount(stats1.getIds().size());
                vo.setRadio5GHzClientCount(stats1.getIds().size());
            }
        }
        return vos;
    }

    private List<HmWifiInterfaceStat> getLatestWifiInterfaceStats(Long deviceId) {
        HmWifiInterfaceStats newest = statsService.getNewestStat(deviceId, HmWifiInterfaceStats.class);
        if (null == newest) {
            return null;
        }
        List<HmWifiInterfaceStat> stats = newest.getStats();
        if (null == stats) {
            return null;
        }
        return stats;
    }

    private ArrayList<HmDevicePlanningExVo> preparePlannedDevices(List<HmDevicePlanningEx> devices)
            throws Exception {
        ArrayList<HmDevicePlanningExVo> vos = new ArrayList<>(devices.size());
        for (HmDevicePlanningEx device : devices) {
            HmDevicePlanningExVo vo = voDoUtil.toVo(HmDevicePlanningExVo.class, device);
            vos.add(vo);
        }
        return vos;
    }

    private ArrayList<HmFolderVo> prepareChildFolders(List<HmFolder> folders) throws Exception {
        ArrayList<HmFolderVo> vos = new ArrayList<>(folders.size());
        for (HmFolder folder : folders) {
            HmFolderVo vo = voDoUtil.toVo(HmFolderVo.class, folder);
            vos.add(vo);
        }
        return vos;
    }

    @Override
    public HmFolderLinksVo findFolderLinks(Long folderId, Long[] deviceIds) throws Exception {
        HmFolder f = rep.findOne(folderId);
        if (null == f) {
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        long start = System.currentTimeMillis();
        HmFolderLinksVo vo = new HmFolderLinksVo();
        Map<String, HmDevice> cachedDevices = new HashMap<>();
        Map<String, HmDeviceLocationEx> cachedDeviceInFolders = new HashMap<>();
        Map<String, HmWirelessLinkVo> links = new HashMap<>();
        for (Long deviceId : deviceIds) {
            if (null == deviceId) {
                continue;
            }
            HmNeighborStats newest = statsService.getNewestStat(deviceId, HmNeighborStats.class);
            if (null == newest) {
                logger.info(String.format("No neighbor information from device %d", deviceId));
                continue;
            }
            List<HmNeighborStat> stats = newest.getStats();
            if (null == stats) {
                logger.info(String.format("No neighbor information from device %d", deviceId));
                continue;
            }
            for (HmNeighborStat stat : stats) {
                if (stat.getLinkType() != (byte) 1) {// TODO change to constant
                    continue;
                }
                String macAddress = stat.getNeighborApId();
                int rssi = stat.getRssi();
                HmDevice neighbor;
                if (cachedDevices.containsKey(macAddress)) {
                    neighbor = cachedDevices.get(macAddress);
                } else {
                    // query from cache
                    neighbor = devRep.findByMacAddress(macAddress);
                    cachedDevices.put(macAddress, neighbor);
                }
                if (null == neighbor) {
                    logger.info(String.format("Cannot find device by macAddress %s", macAddress));
                    continue;
                }
                // neighbor existed
                Long neighborId = neighbor.getId();
                String reverseKey = neighborId + "|" + deviceId;
                HmWirelessLinkVo linkVo = links.get(reverseKey);
                if (null == linkVo) {
                    // not existed
                    linkVo = new HmWirelessLinkVo();
                    linkVo.setFromId(deviceId);
                    linkVo.setFromFolderId(folderId);
                    linkVo.setFromRssi(rssi);
                    linkVo.setToId(neighborId);
                    String key = deviceId + "|" + neighborId;
                    links.put(key, linkVo);
                } else {
                    // already existed, bidirectional link
                    linkVo.setToRssi(rssi);
                }

                if (ArrayUtils.contains(deviceIds, neighborId)) {
                    // neighbor in same folder
                    linkVo.setToFolderId(folderId);
                } else {
                    HmDeviceLocationEx neighborInFolder;
                    if (cachedDeviceInFolders.containsKey(macAddress)) {
                        neighborInFolder = cachedDeviceInFolders.get(macAddress);
                    } else {
                        // query from db
                        neighborInFolder = deviceLocationExRep.findByDeviceId(neighborId);
                        cachedDeviceInFolders.put(macAddress, neighborInFolder);
                    }
                    if (null == neighborInFolder) {
                        logger.info(String.format("Device id %d, macAddress %s is not in any folder", neighborId,
                                macAddress));
                        continue;
                    }
                    // neighbor in other folder
                    linkVo.setToFolderId(neighborInFolder.getParent().getId());
                }
            }
        }
        // discard no 'to folder' link
        for (String key : links.keySet()) {
            HmWirelessLinkVo link = links.get(key);
            if (null == link.getToFolderId()) {
                logger.info(
                        String.format("Discard link which toId is %d, as its toFolderId is null", link.getToId()));
                continue;
            }
            vo.getLinks().add(link);
        }
        long cost = System.currentTimeMillis() - start;
        if (cost > 500) {
            logger.warn(String.format("Too slow to query neighbor info for folder %s, it cost %d ms.", f.getName(),
                    cost));
        }
        return vo;
    }

    @Override
    public BufferedImage getFloorNail(Long folderId, Long[] deviceIds, Integer[] channels, Integer[] colors)
            throws Exception {
        HmFolder floor = rep.findOne(folderId);
        if (null == floor) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        if (FolderType.FLOOR != floor.getFolderType()) {
            logger.error(
                    String.format("Cannot show floor nail for a node %s which is not floor type", floor.getName()));
            throw new BadRequestException(ErrorCodes.folderCannotRequestFloorNail,
                    new Object[][] { { "name", floor.getName() } });
        }
        HmFolder building = floor.getParent();
        List<HmFolder> floors = findChildNodes(building.getId());
        // sort
        Collections.sort(floors, new Comparator<HmFolder>() {
            @Override
            public int compare(HmFolder o1, HmFolder o2) {
                return o2.getMapOrder() - o1.getMapOrder();
            }
        });
        HmFolderBuildingViewVo vo = calculateBuildingSize(floors);
        double actualWidth = vo.getFloorWidth();
        if (null != vo.getFloorScale() && vo.getFloorScale().intValue() > 0) {
            actualWidth = vo.getFloorWidth() / vo.getFloorScale();
        }
        if (LengthUnit.FEET == floor.getLengthUnit()) {
            actualWidth /= HmFolder.FEET_TO_METERS;
        }
        double gridSize = getActualGridSize(actualWidth);
        Map<Long, Integer> channelMap = new HashMap<Long, Integer>();
        Map<Long, Integer> colorMap = new HashMap<Long, Integer>();
        if (deviceIds != null && colors != null && channels != null) {
            for (int i = 0; i < deviceIds.length; i++) {
                channelMap.put(deviceIds[i], channels[i]);
                colorMap.put(deviceIds[i], colors[i]);
            }
        }
        return createFloorImage(floor, vo.getFloorScale(), vo.getFloorWidth(), vo.getFloorHeight(), channelMap,
                colorMap, vo.getBorderX(), vo.getBorderY(), gridSize);
    }

    private BufferedImage createFloorImage(HmFolder floor, double scale, int floorWidth, int floorHeight,
            Map<Long, Integer> channelMap, Map<Long, Integer> colorMap, int borderX, int borderY, double gridSize)
            throws Exception {
        BufferedImage image = new BufferedImage(floorWidth + borderX + 1, floorHeight + borderY + 1,
                BufferedImage.TYPE_INT_ARGB);
        if (floor == null) {
            return image;
        }
        double metricWidth = 0.0, metricHeight = 0.0, offsetX = 0.0, offsetY = 0.0;
        int imageWidth = 0;
        LengthUnit lengthUnit = LengthUnit.METERS;

        if (null != floor.getMetricWidth()) {
            metricWidth = floor.getMetricWidth().doubleValue();
        }
        if (null != floor.getMetricHeight()) {
            metricHeight = floor.getMetricHeight().doubleValue();
        }
        if (null != floor.getImageWidth()) {
            imageWidth = floor.getImageWidth().intValue();
        }
        if (null != floor.getOffsetX()) {
            offsetX = floor.getOffsetX().doubleValue();
        }
        if (null != floor.getOffsetY()) {
            offsetY = floor.getOffsetY().doubleValue();
        }
        if (null != floor.getLengthUnit()) {
            lengthUnit = floor.getLengthUnit();
        }

        Graphics2D g2 = image.createGraphics();
        g2.setStroke(new BasicStroke(1));
        if (getDistanceMetric(metricWidth, lengthUnit) == 0) {
            g2.setColor(new Color(255, 255, 255));
            g2.fillRect(borderX, 0, floorWidth + 1, borderY);
            g2.fillRect(0, 0, borderX, borderY + floorHeight + 1);
            g2.setColor(new Color(120, 120, 120));
            g2.drawLine(0, borderY, floorWidth + borderX, borderY);
            g2.drawLine(borderX, 0, borderX, floorHeight + borderY);
            g2.setColor(new Color(255, 255, 204));
            g2.fillRect(borderX + 2, borderY + 2, 162, 25);
            g2.setColor(new Color(0, 51, 102));
            g2.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12));
            g2.drawString("Please size this floor plan.", borderX + 8, borderY + 19);
            return image;
        }
        double screenWidth = scale * getDistanceMetric(metricWidth, lengthUnit);
        double imageScale = screenWidth / imageWidth;
        int originX = (int) (getDistanceMetric(offsetX, lengthUnit) * scale);
        int originY = (int) (getDistanceMetric(offsetY, lengthUnit) * scale);
        g2.setColor(new Color(255, 255, 255));
        if (floor.getBackground() != null && floor.getBackground().length() > 0) {
            LinkedMultiValueMap<String, String> metadata = new LinkedMultiValueMap<>();
            String url = getImageBaseUrl(floor.getOwnerId()) + floor.getBackground();
            try {
                BufferedImage map = ImageIO.read(clientFileService.getFile(url, "AFS_TOKEN", metadata));
                AffineTransform transform = new AffineTransform();
                transform.scale(imageScale, imageScale);
                g2.drawImage(map, new AffineTransformOp(transform, null), getFloorX(0, borderX, originX),
                        getFloorY(0, borderY, originY));
            } catch (Exception e) {
                logger.error(String.format("image file not found with url: %s", url));
                double screenHeight = scale * getDistanceMetric(metricHeight, lengthUnit);
                g2.fillRect(getFloorX(0, borderX, originX), getFloorY(0, borderY, originY), (int) screenWidth,
                        (int) screenHeight);
            }
        } else {
            double screenHeight = scale * getDistanceMetric(metricHeight, lengthUnit);
            g2.fillRect(getFloorX(0, borderX, originX), getFloorY(0, borderY, originY), (int) screenWidth,
                    (int) screenHeight);
        }
        g2.setColor(new Color(204, 204, 204));
        // Right edge border
        g2.drawLine(borderX + floorWidth, borderY + 1, borderX + floorWidth, borderY + floorHeight);
        // Left edge border (right of tick marks)
        g2.drawLine(borderX + 1, borderY + floorHeight, borderX + floorWidth, borderY + floorHeight);
        g2.setColor(new Color(255, 255, 255));
        g2.fillRect(borderX, 0, floorWidth + 1, borderY);
        g2.fillRect(0, 0, borderX, borderY + floorHeight + 1);
        g2.setColor(new Color(120, 120, 120));
        g2.drawLine(0, borderY, floorWidth + borderX, borderY);
        g2.drawLine(borderX, 0, borderX, floorHeight + borderY);

        Font font = new Font(Font.SANS_SERIF, Font.BOLD, 12);
        double actualWidth = floorWidth / scale;
        double actualHeight = floorHeight / scale;
        String firstLabel;
        double unitScale = scale;
        if (LengthUnit.FEET == lengthUnit) {
            firstLabel = "0 feet";
            actualWidth /= HmFolder.FEET_TO_METERS;
            actualHeight /= HmFolder.FEET_TO_METERS;
            unitScale *= HmFolder.FEET_TO_METERS;
        } else {
            firstLabel = "0 meters";
        }
        g2.drawString(firstLabel, borderX + 4, 12);
        double gridX = gridSize;
        while (gridX < actualWidth) {
            int x = (int) (gridX * unitScale) + borderX;
            g2.drawLine(x, 0, x, borderY);
            boolean label = true;
            if (gridX + gridSize >= actualWidth) {
                // Last mark
                if (x + getNumberPixelWidth(gridX) + 2 > floorWidth) {
                    label = false;
                }
            }
            if (label) {
                g2.drawString("" + (int) gridX, x + 4, 12);
            }
            gridX += gridSize;
        }

        double gridY = 0;
        while (gridY < actualHeight) {
            int y = (int) (gridY * unitScale) + borderY;
            g2.drawLine(0, y, borderX, y);
            double lx = gridY;
            int dx = 1;
            for (int bx = borderX; bx >= 16; bx -= 7) {
                if (lx < 10) {
                    dx += 7;
                } else {
                    lx /= 10;
                }
            }
            boolean label = true;
            if (gridY + gridSize >= actualHeight) {
                // Last mark
                if (y - borderY + 13 > floorHeight) {
                    label = false;
                }
            }
            if (label) {
                g2.drawString("" + (int) gridY, dx, y + 13);
            }
            gridY += gridSize;
        }

        double mapToImage = getMapToMetric(metricWidth, imageWidth, lengthUnit) * scale;
        if (floor.getPerimeter().size() > 0) {
            g2.setStroke(new BasicStroke(2));
            g2.setColor(new Color(2, 159, 245));
            int[] xPoints = new int[floor.getPerimeter().size()];
            int[] yPoints = new int[floor.getPerimeter().size()];
            int nPoints = 0;
            int perimId = floor.getPerimeter().get(0).getId();
            for (int i = 0; i < floor.getPerimeter().size(); i++) {
                HmVertex vertex = floor.getPerimeter().get(i);
                if (vertex.getId() != perimId) {
                    g2.drawPolygon(xPoints, yPoints, nPoints);
                    nPoints = 0;
                    perimId = vertex.getId();
                }
                xPoints[nPoints] = getFloorX((int) (vertex.getX() * mapToImage), borderX, originX);
                yPoints[nPoints++] = getFloorY((int) (vertex.getY() * mapToImage), borderY, originY);
            }
            g2.drawPolygon(xPoints, yPoints, nPoints);
        }

        g2.setStroke(new BasicStroke(1));
        g2.setColor(new Color(0, 170, 0));
        g2.setFont(font.deriveFont(Font.BOLD, 11));

        List<HmDeviceLocationEx> devices = deviceLocationExRep.findAllHmDevices(floor.getId());
        if (null != devices && !devices.isEmpty()) {
            for (HmDeviceLocationEx device : devices) {
                double x = device.getX() * mapToImage;
                double y = device.getY() * mapToImage;
                createNodeImage(device.getId(), channelMap, colorMap, getFloorX((int) x, borderX, originX),
                        getFloorY((int) y, borderY, originY), g2);
            }
        } else {
            List<HmDevicePlanningEx> plannedDevices = devicePlanningExRep.findAllPlannedDevices(floor.getId());
            for (HmDevicePlanningEx plannedDevice : plannedDevices) {
                double x = plannedDevice.getX() * mapToImage;
                double y = plannedDevice.getY() * mapToImage;
                createNodeImage(plannedDevice.getId(), channelMap, colorMap, getFloorX((int) x, borderX, originX),
                        getFloorY((int) y, borderY, originY), g2);
            }
        }
        return image;
    }

    private int getFloorX(int x, int borderX, int origin) {
        return x + borderX + origin;
    }

    private int getFloorY(int y, int borderY, int origin) {
        return y + borderY + origin;
    }

    private int getNumberPixelWidth(double d) {
        int size = 0;
        while (d > 100) {
            size += 7;
            d /= 10;
        }
        return size;
    }

    private void createNodeImage(long id, Map<Long, Integer> channelMap, Map<Long, Integer> colorMap, int x, int y,
            Graphics2D g2) {
        Integer channel = channelMap.get(id);
        Integer colorIndex = colorMap.get(id);
        int radius = 11;
        if (channel == null || channel == 0 || colorIndex == null || colorIndex > startChannelColors.length - 1) {
            g2.setColor(new Color(180, 180, 180));
            g2.fillOval(x - radius, y - radius, 2 * radius, 2 * radius);
            return;
        }
        g2.setColor(startChannelColors[colorIndex]);
        g2.fillOval(x - radius, y - radius, 2 * radius, 2 * radius);
        int dy = 6, w = 21;
        g2.fillRect(x, y - dy, w, dy + dy);
        dy = 5;
        g2.setColor(new Color(255, 255, 255));
        g2.fillRect(x + 1, y - dy, w - 2, dy + dy);
        g2.setColor(new Color(0, 51, 102));
        int chx = x + 2;
        if (channel < 100) {
            chx += 3;
        }
        if (channel < 10) {
            chx += 3;
        }
        g2.drawString("" + channel, chx, y + 4);
    }

    @Override
    public HmFolderBuildingViewVo findFolderBuildingView(Long folderId) throws Exception {
        HmFolder f = rep.findOne(folderId);
        if (null == f) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        if (FolderType.BUILDING != f.getFolderType()) {
            logger.error(String.format("Cannot show building view for a node %s which is not building type",
                    f.getName()));
            throw new BadRequestException(ErrorCodes.folderCannotRequestBuildingView,
                    new Object[][] { { "name", f.getName() } });
        }
        List<HmFolder> floors = findChildNodes(folderId);
        // sort
        Collections.sort(floors, new Comparator<HmFolder>() {
            @Override
            public int compare(HmFolder o1, HmFolder o2) {
                return o2.getMapOrder() - o1.getMapOrder();
            }
        });
        HmFolderBuildingViewVo vo = calculateBuildingSize(floors);
        vo.setId(folderId);
        for (HmFolder floor : floors) {
            HmFolderFloorViewVo floorView = calculateFloorView(floor);
            vo.addFloorViewVo(floorView);
        }
        return vo;
    }

    private HmFolderBuildingViewVo calculateBuildingSize(List<HmFolder> floors) {
        HmFolderBuildingViewVo vo = new HmFolderBuildingViewVo();
        double maxWidthInMeter = 0, maxHeightInMeter = 0;
        boolean hasFeetUnit = false;
        for (HmFolder floor : floors) {
            Double actualX = getDistanceMetric(floor.getMetricWidth(), floor.getLengthUnit());
            Double offsetX = getDistanceMetric(floor.getOffsetX(), floor.getLengthUnit());
            Double actualY = getDistanceMetric(floor.getMetricHeight(), floor.getLengthUnit());
            Double offsetY = getDistanceMetric(floor.getOffsetY(), floor.getLengthUnit());
            double newWidth = actualX + offsetX;
            double newHeight = actualY + offsetY;
            if (newWidth > maxWidthInMeter) {
                maxWidthInMeter = newWidth;
            }
            if (newHeight > maxHeightInMeter) {
                maxHeightInMeter = newHeight;
            }
            if (LengthUnit.FEET == floor.getLengthUnit()) {
                hasFeetUnit = true;
            }
        }
        int floorWidth = 500;
        double scale = 0;
        if (maxWidthInMeter > 0) {
            scale = floorWidth / maxWidthInMeter;
        }
        int floorHeight = (int) (scale * maxHeightInMeter);
        if (0 == floorHeight) {
            floorHeight = 300;
        }
        int borderX = 16, borderY = 15;
        if (hasFeetUnit) {
            maxHeightInMeter /= HmFolder.FEET_TO_METERS;
        }
        borderX += getNumberWidth(maxHeightInMeter);

        vo.setFloorWidth(floorWidth);
        vo.setFloorHeight(floorHeight);
        vo.setFloorScale(Double.valueOf(scale));
        vo.setBorderX(borderX);
        vo.setBorderY(borderY);
        return vo;
    }

    private int getNumberWidth(double d) {
        int size = 0;
        while (d > 100) {
            size += 7;
            d /= 10;
        }
        return size;
    }

    private HmFolderFloorViewVo calculateFloorView(HmFolder floor) {
        Long id = floor.getId();
        HmFolderFloorViewVo floorVo = new HmFolderFloorViewVo();
        floorVo.setId(id);
        floorVo.setName(floor.getName());
        floorVo.setMetricWidth(floor.getMetricWidth());
        floorVo.setAreaUnit(floor.getLengthUnit().toString());
        floorVo.setOffsetX(floor.getOffsetX());
        floorVo.setOffsetY(floor.getOffsetY());
        // area
        int count = getPerimeterCount(floor);
        if (count > 0) {
            Perimeter[] perimeters = new Perimeter[count];
            calculatePerimeterAreas(floor, perimeters);
            calculatePerimeterHierarchy(floor, perimeters);
            floorVo.setArea(calculateCoverageArea(perimeters));
            floorVo.setPerimeterCount(count);
            logger.info(floor.getName() + " coverage area: " + floorVo.getArea() + " " + floorVo.getAreaUnit());
        }

        // count
        List<Object[]> productTypes = deviceLocationExRep.findAllHmDeviceProductTypes(id);
        if (null != productTypes && !productTypes.isEmpty()) {
            // Live devices
            getFloorDeviceCount(floorVo, productTypes);
        } else {
            // Planned devices
            productTypes = devicePlanningExRep.findAllPlannedDeviceProductTypes(id);
            getFloorDeviceCount(floorVo, productTypes);
        }
        return floorVo;
    }

    private void getFloorDeviceCount(HmFolderFloorViewVo floorVo, List<Object[]> productTypes) {
        if (null != productTypes && !productTypes.isEmpty()) {
            int apCount = 0, brCount = 0, srCount = 0, cvgCount = 0, rfCount = 0;
            Long[] deviceIds = new Long[productTypes.size()];
            for (int i = 0; i < productTypes.size(); i++) {
                boolean hasRf = false;
                Object[] object = productTypes.get(i);
                Long id = (Long) object[0];
                AhProductType productType = (AhProductType) object[1];
                if (AhProductType.FAMILY_AP.contains(productType)) {
                    apCount++;
                    hasRf = true;
                } else if (AhProductType.FAMILY_BR.contains(productType)) {
                    brCount++;
                    if (AhProductType.BR_200 != productType) {
                        hasRf = true;
                    }
                } else if (AhProductType.FAMILY_SR.contains(productType)) {
                    srCount++;
                } else if (AhProductType.FAMILY_CVG.contains(productType)) {
                    cvgCount++;
                } else {
                    logger.error("Not AP, BR, Switch and CVG for product type : " + productType.toString());
                }
                if (hasRf) {
                    rfCount++;
                }
                deviceIds[i] = id;
            }
            floorVo.setApCount(Integer.valueOf(apCount));
            floorVo.setBrCount(Integer.valueOf(brCount));
            floorVo.setSrCount(Integer.valueOf(srCount));
            floorVo.setVpnCount(Integer.valueOf(cvgCount));
            floorVo.setRfCount(Integer.valueOf(rfCount));
            floorVo.setDeviceIds(deviceIds);
        }
    }

    private int getPerimeterCount(HmFolder floor) {
        List<HmVertex> perimeters = floor.getPerimeter();
        if (perimeters.size() == 0) {
            return 0;
        }
        int count = 1;
        HmVertex vertex = perimeters.get(0);
        int perimId = vertex.getId();
        for (int i = 1; i < perimeters.size(); i++) {
            vertex = perimeters.get(i);
            if (vertex.getId() != perimId) {
                perimId = vertex.getId();
                count++;
            }
        }
        return count;
    }

    private void calculatePerimeterAreas(HmFolder floor, Perimeter[] perimeters) {
        if (floor.getPerimeter().size() == 0) {
            return;
        }
        double mapToMetric = getMapToMetric(floor.getMetricWidth(), floor.getImageWidth(), floor.getLengthUnit());
        int start = 0;
        double area = 0;
        HmVertex vertex = floor.getPerimeter().get(0);
        double x1 = vertex.getX();
        double y1 = vertex.getY();
        double first_x = x1;
        double first_y = y1;
        int perimId = vertex.getId();
        int perimIndex = 0;
        for (int i = 1; i < floor.getPerimeter().size(); i++) {
            vertex = floor.getPerimeter().get(i);
            double x2 = vertex.getX();
            double y2 = vertex.getY();
            if (vertex.getId() != perimId) {
                area = (area + x1 * first_y - first_x * y1) / 2 * mapToMetric * mapToMetric;
                perimeters[perimIndex++] = new Perimeter(start, i - 1, area, perimeters.length);
                start = i;
                area = 0;
                first_x = x2;
                first_y = y2;
                perimId = vertex.getId();
            } else {
                area += x1 * y2 - x2 * y1;
            }
            x1 = x2;
            y1 = y2;
        }
        area = (area + x1 * first_y - first_x * y1) / 2 * mapToMetric * mapToMetric;
        perimeters[perimIndex] = new Perimeter(start, floor.getPerimeter().size() - 1, area, perimeters.length);
    }

    private void calculatePerimeterHierarchy(HmFolder floor, Perimeter[] perimeters) {
        for (int i = 0; i < perimeters.length; i++) {
            HmVertex vertex = floor.getPerimeter().get(perimeters[i].start);
            double x = vertex.getX();
            double y = vertex.getY();
            short[] inside = perimeters[i].inside;
            short depth = 0;
            for (int j = 0; j < inside.length; j++) {
                if (i == j) {
                    inside[j] = 1;
                    continue;
                }
                if (inside[j] == 0) {
                    if (inside(floor, x, y, perimeters[j])) {
                        inside[j] = 2;
                        depth++;
                        if (j < i) {
                            for (int k = j + 1; k < inside.length; k++) {
                                if (perimeters[j].inside[k] == 2) {
                                    inside[k] = 2;
                                    depth++;
                                }
                            }
                        }
                    } else {
                        inside[j] = 1;
                    }
                }
            }
            perimeters[i].depth = depth;
        }
    }

    private boolean inside(HmFolder floor, double x, double y, Perimeter perimeter) {
        List<Double> edgesX = findEdgesX(floor, y, perimeter);
        boolean inside = false;
        for (int leftEdge = 0; leftEdge < edgesX.size() && edgesX.get(leftEdge) <= x; leftEdge++) {
            inside = !inside;
        }
        return inside;
    }

    private List<Double> findEdgesX(HmFolder floor, double y, Perimeter perimeter) {
        List<Double> edges = new ArrayList<Double>();
        HmVertex vertex = floor.getPerimeter().get(perimeter.start);
        double x1 = vertex.getX();
        double y1 = vertex.getY();
        double first_x = x1;
        double first_y = y1;
        for (int i = perimeter.start + 1; i <= perimeter.end; i++) {
            vertex = floor.getPerimeter().get(i);
            double x2 = vertex.getX();
            double y2 = vertex.getY();
            findIntersectionX(x1, y1, x2, y2, y, edges);
            x1 = x2;
            y1 = y2;
        }
        findIntersectionX(x1, y1, first_x, first_y, y, edges);
        Collections.sort(edges, new Comparator<Double>() {
            @Override
            public int compare(Double o1, Double o2) {
                return o1.compareTo(o2);
            }
        });
        return edges;
    }

    private void findIntersectionX(double x1, double y1, double x2, double y2, double cy, List<Double> edges) {
        double tol = 1e-6;
        double dy = y2 - y1;
        if (Math.abs(dy) < tol) {
            // This wall is parallel to x axis.
            return;
        }
        if (dy > 0) {
            if (cy < y1 || cy >= y2) {
                // Candidate is outside the Y boundaries.
                return;
            }
        } else {
            if (cy < y2 || cy >= y1) {
                // Candidate is outside the Y boundaries.
                return;
            }
        }
        Double xi = llix(x1, y1, x2, y2, cy);
        edges.add(xi);
    }

    private double llix(double x1, double y1, double x2, double y2, double cy) {
        // Line between l1p1 and l1p2
        double a1 = y2 - y1;
        double b1 = x1 - x2;
        double c1 = a1 * x1 + b1 * y1;
        // Intersection point
        double x = (c1 - b1 * cy) / a1;
        return x;
    }

    private double calculateCoverageArea(Perimeter[] perimeters) {
        double aggregate = 0;
        for (int i = 0; i < perimeters.length; i++) {
            if (perimeters[i].depth % 2 != 0) {
                aggregate -= perimeters[i].area;
            } else {
                aggregate += perimeters[i].area;
            }
        }
        return aggregate;
    }

    @SuppressWarnings("unused")
    private Double reverseScale(Integer x, Double scale) {
        return x / scale;
    }

    @SuppressWarnings("unused")
    private Integer scale(Double d, Double scale) {
        return (int) Math.round(d * scale);
    }

    private double getDistanceMetric(Double distance, LengthUnit unit) {
        if (null == distance) {
            return 0;
        }
        if (null == unit) {
            return distance.doubleValue();
        }
        if (LengthUnit.FEET == unit) {
            return distance * HmFolder.FEET_TO_METERS;
        } else {
            return distance.doubleValue();
        }
    }

    private double getMapToMetric(Double actualWidth, Integer width, LengthUnit unit) {
        if (null == width) {
            return 0;
        }
        if (null == actualWidth) {
            actualWidth = Double.valueOf(0);
        }
        double mapToMetric = actualWidth / width;
        if (LengthUnit.FEET == unit) {
            mapToMetric *= HmFolder.FEET_TO_METERS;
        }
        return mapToMetric;
    }

    private List<HmFolder> findChildNodes(final Long parentId) {
        Specification<HmFolder> spec = new Specification<HmFolder>() {
            @Override
            public Predicate toPredicate(Root<HmFolder> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Predicate[] predicates = new Predicate[1];
                predicates[0] = cb.equal(root.get("parent"), parentId);
                return cb.and(predicates);
            }
        };
        return rep.findAll(spec);
    }

    private long findChildNodeCount(final Long parentId) {
        Specification<HmFolder> spec = new Specification<HmFolder>() {
            @Override
            public Predicate toPredicate(Root<HmFolder> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Predicate[] predicates = new Predicate[1];
                predicates[0] = cb.equal(root.get("parent"), parentId);
                return cb.and(predicates);
            }
        };
        return rep.count(spec);
    }

    public void removeAllFolders() throws Exception {
        logger.info("Removing all folders.");
        List<HmFolder> folders = findRootFolders();
        logger.info("# of root folders: " + folders.size());
        for (HmFolder f : folders) {
            removeFolder(f);
        }
    }

    private List<HmFolder> findRootFolders() {
        Specification<HmFolder> spec = new Specification<HmFolder>() {
            @Override
            public Predicate toPredicate(Root<HmFolder> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Predicate[] predicates = new Predicate[1];
                predicates[0] = cb.isNull(root.get("parent"));
                return cb.and(predicates);
            }
        };
        return rep.findAll(spec);
    }

    private HmFolder findRootFolder(final Long ownerId) {
        Specification<HmFolder> spec = new Specification<HmFolder>() {
            @Override
            public Predicate toPredicate(Root<HmFolder> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Predicate[] predicates = new Predicate[2];
                predicates[0] = cb.isNull(root.get("parent"));
                predicates[1] = cb.equal(root.get("ownerId"), ownerId);
                return cb.and(predicates);
            }
        };
        List<HmFolder> all = rep.findAll(spec);
        return all.isEmpty() ? null : all.get(0);
    }

    private void removeFolder(HmFolder f) throws Exception {
        List<HmFolder> folders = rep.findChildFolders(f.getId());
        List<HmDevicePlanningEx> devices = devicePlanningExRep.findAllPlannedDevices(f.getId());
        List<HmDeviceLocationEx> liveDevices = deviceLocationExRep.findAllHmDevices(f.getId());
        if (folders != null)
            for (HmFolder c : folders) {
                removeFolder(c);
            }
        if (devices != null)
            for (HmDevicePlanningEx pd : devices) {
                logger.info(String.format("Removing planned device: %d", pd.getId()));
                devicePlanningExRep.delete(pd);
            }
        if (liveDevices != null)
            for (HmDeviceLocationEx df : liveDevices) {
                logger.info(String.format("Removing live device: %d", df.getId()));
                deviceLocationExRep.delete(df);
            }
        logger.info(String.format("Removing folder: %d, %s", f.getId(), f.getName()));
        rep.delete(f);
    }

    public void setCoverage() {
        cvCache.setCoverage();
    }

    protected List<HmFolder> sortList(List<HmFolder> list) {
        Collections.sort(list, new Comparator<HmFolder>() {
            @Override
            public int compare(HmFolder o1, HmFolder o2) {
                return o1.getMapOrder() - o2.getMapOrder();
            }
        });
        return list;
    }

    @Override
    @Transactional
    public void saveWalls(Long folderId, HmWallsInFolderVo vo) throws Exception {
        logger.info(String.format("save walls in folder %s", folderId));
        HmFolder folder = rep.findOne(folderId);
        if (null == folder) {
            logger.error(String.format("cannot find folder %s, save walls failed.", folderId));
            return;
        }
        List<HmWall> walls = new ArrayList<>();
        List<HmWallVo> wallVos = vo.getWalls();
        if (null != wallVos && wallVos.size() > 0) {
            for (HmWallVo wallVo : wallVos) {
                HmWall wall = new HmWall();
                wall.setType(WallType.valueOf(wallVo.getType()));
                wall.setX1(wallVo.getX1());
                wall.setY1(wallVo.getY1());
                wall.setX2(wallVo.getX2());
                wall.setY2(wallVo.getY2());
                walls.add(wall);
            }
        }
        folder.setWalls(walls);
        rep.saveAndFlush(folder);
        logger.info(String.format("saved count of %d walls into folder %s.", walls.size(), folderId));
    }

    @Override
    public HmPerimeterInvalidPointsVo validatePerimeter(Long folderId, HmPerimeterVo vo) throws Exception {
        HmFolder folder = rep.findOne(folderId);
        if (null == folder) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }

        List<HmVertexVo> vos = vo.getVertexes();
        int size = vos.size();
        logger.info(String.format("vertex count: %d", size));
        Point2D[] vertexs = new Point2D[size];
        for (int i = 0; i < size; i++) {
            vertexs[i] = new Point2D.Double(vos.get(i).getX(), vos.get(i).getY());
            logger.info(String.format("perimeter node: %s", vertexs[i]));
        }
        return validatePerimeter(vertexs, folder.getPerimeter());
    }

    private HmPerimeterInvalidPointsVo validatePerimeter(Point2D[] walls, List<HmVertex> perimeter)
            throws Exception {
        HmPerimeterInvalidPointsVo vo = new HmPerimeterInvalidPointsVo();
        if (perimeter.size() > 0 && walls.length > 1) {
            HmVertex vertex = perimeter.get(0);
            Point2D first = new Point2D.Double(vertex.getX(), vertex.getY());
            Point2D v1 = first;
            int perimId = vertex.getId();
            for (int i = 1; i < perimeter.size(); i++) {
                vertex = perimeter.get(i);
                Point2D v2 = new Point2D.Double(vertex.getX(), vertex.getY());
                if (vertex.getId() != perimId) {
                    // Consider vertex v1 -> first
                    validatePerimeter(walls, v1, first, vo);
                    first = v2;
                    perimId = vertex.getId();
                } else {
                    // Consider vertex v1 -> v2
                    validatePerimeter(walls, v1, v2, vo);
                }
                v1 = v2;
            }
            // Consider vertex v1 -> first
            validatePerimeter(walls, v1, first, vo);
        }
        if (walls.length < 4) {
            return vo;
        }
        for (int i = 0; i < walls.length - 2; i++) {
            Point2D l1p1 = walls[i];
            Point2D l1p2 = walls[i + 1];
            for (int j = i + 2; j < walls.length; j++) {
                int l2p2i = (j + 1) % walls.length;
                if (i == l2p2i) {
                    continue;
                }
                validatePerimeter(l1p1, l1p2, walls[j], walls[l2p2i], vo);
            }
        }
        return vo;
    }

    private void validatePerimeter(Point2D[] walls, Point2D l1p1, Point2D l1p2, HmPerimeterInvalidPointsVo vo)
            throws Exception {
        Point2D l2p1 = walls[0];
        Point2D first = l2p1;
        for (int i = 1; i < walls.length; i++) {
            Point2D l2p2 = walls[i];
            validatePerimeter(l1p1, l1p2, l2p1, l2p2, vo);
            l2p1 = l2p2;
        }
        validatePerimeter(l1p1, l1p2, first, l2p1, vo);
    }

    private void validatePerimeter(Point2D l1p1, Point2D l1p2, Point2D l2p1, Point2D l2p2,
            HmPerimeterInvalidPointsVo vo) throws Exception {
        Point2D ip = llis(l1p1, l1p2, l2p1, l2p2);
        if (ip != null) {
            vo.addInvalidPoint(ip.getX(), ip.getY());
        }
    }

    private static Point2D llis(Point2D l1p1, Point2D l1p2, Point2D l2p1, Point2D l2p2) {
        double l1x1 = l1p1.getX();
        double l1x2 = l1p2.getX();
        if (l1x2 < l1x1) {
            l1x1 = l1x2;
            l1x2 = l1p1.getX();
        }
        double l1y1 = l1p1.getY();
        double l1y2 = l1p2.getY();
        if (l1y2 < l1y1) {
            l1y1 = l1y2;
            l1y2 = l1p1.getY();
        }
        double l2x1 = l2p1.getX();
        double l2x2 = l2p2.getX();
        if (l2x2 < l2x1) {
            l2x1 = l2x2;
            l2x2 = l2p1.getX();
        }
        double l2y1 = l2p1.getY();
        double l2y2 = l2p2.getY();
        if (l2y2 < l2y1) {
            l2y1 = l2y2;
            l2y2 = l2p1.getY();
        }
        Point2D ip = llip(l1p1, l1p2, l2p1, l2p2);
        double ipx = ip.getX();
        double ipy = ip.getY();
        double tol = 1e-6;
        if (Math.abs(l1x1 - l1x2) < tol) {
            ipx = l1x1;
        } else if (Math.abs(l2x1 - l2x2) < tol) {
            ipx = l2x1;
        }
        if (Math.abs(l1y1 - l1y2) < tol) {
            ipy = l1y1;
        } else if (Math.abs(l2y1 - l2y2) < tol) {
            ipy = l2y1;
        }
        if (ipx >= l1x1 && ipx <= l1x2 && ipx >= l2x1 && ipx <= l2x2 && ipy >= l1y1 && ipy <= l1y2 && ipy >= l2y1
                && ipy <= l2y2) {
            return ip;
        } else {
            return null;
        }
    }

    private static Point2D llip(Point2D l1p1, Point2D l1p2, Point2D l2p1, Point2D l2p2) {
        // Line between l1p1 and l1p2
        double a1 = l1p2.getY() - l1p1.getY();
        double b1 = l1p1.getX() - l1p2.getX();
        double c1 = a1 * l1p1.getX() + b1 * l1p1.getY();

        // Line between l2p1 and l2p2
        double a2 = l2p2.getY() - l2p1.getY();
        double b2 = l2p1.getX() - l2p2.getX();
        double c2 = a2 * l2p1.getX() + b2 * l2p1.getY();

        // Intersection point
        double det = a1 * b2 - a2 * b1;
        double x = (b2 * c1 - b1 * c2) / det;
        double y = (a1 * c2 - a2 * c1) / det;

        return new Point2D.Double(x, y);
    }

    @Override
    @Transactional
    public HmPerimeterVo createPerimeter(Long folderId, HmPerimeterVo vo) throws Exception {
        HmFolder folder = rep.findOne(folderId);
        if (null == folder) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        List<HmVertex> perimeters = folder.getPerimeter();
        int count = vo.getVertexes().size();
        if (count < 3) {
            logger.error(String.format("Invalid perimeter to create under folder %s", folder.getName()));
            throw new BadRequestException(ErrorCodes.invalidPerimeter);
        }
        int maxId = findMaxPerimeterId(perimeters);
        logger.info(String.format("Max perimeter id is %d", maxId));
        for (HmVertexVo vertexVo : vo.getVertexes()) {
            logger.info("perimeter node : (" + vertexVo.getX() + ", " + vertexVo.getY() + ")");
            HmVertex vertex = new HmVertex();
            vertex.setId(maxId + 1);
            vertex.setType(VertexType.valueOf(vo.getType()));
            vertex.setX(vertexVo.getX());
            vertex.setY(vertexVo.getY());
            perimeters.add(vertex);
        }
        rep.saveAndFlush(folder);
        vo.setId(maxId + 1);
        return vo;
    }

    private int findMaxPerimeterId(List<HmVertex> perimeters) {
        int maxId = -1;
        for (HmVertex perimeter : perimeters) {
            if (perimeter.getId().intValue() > maxId) {
                maxId = perimeter.getId().intValue();
            }
        }
        return maxId;
    }

    @Override
    @Transactional
    public HmPerimeterVo updatePerimeter(Long folderId, Integer perimeterId, HmPerimeterVo vo) throws Exception {
        HmFolder folder = rep.findOne(folderId);
        if (null == folder) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        List<HmVertex> perimeters = folder.getPerimeter();
        for (HmVertex vertex : perimeters) {
            if (vertex.getId().equals(perimeterId)) {
                vertex.setType(VertexType.valueOf(vo.getType()));
            }
        }
        rep.saveAndFlush(folder);
        vo.setId(perimeterId);
        return vo;
    }

    @Override
    @Transactional
    public void removePerimeter(Long folderId, Integer perimeterId) throws Exception {
        HmFolder folder = rep.findOne(folderId);
        if (null == folder) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        List<HmVertex> perimeters = folder.getPerimeter();
        List<HmVertex> newPerimeters = new ArrayList<>();
        for (HmVertex vertex : perimeters) {
            if (!vertex.getId().equals(perimeterId)) {
                newPerimeters.add(vertex);
            }
        }
        folder.setPerimeter(newPerimeters);
        rep.saveAndFlush(folder);
    }

    @Override
    public HmGeoPerimeterInvalidVo validateGeoPerimeter(Long folderId, HmGeoPerimeterVo vo) throws Exception {
        HmFolder folder = rep.findOne(folderId);
        if (null == folder) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        boolean valid = validateGeoPerimeter((HmFolder) null, vo);
        HmGeoPerimeterInvalidVo ivo = new HmGeoPerimeterInvalidVo();
        ivo.setInvalid(!valid);
        return ivo;
    }

    @Override
    @Transactional
    public HmFolderVo createGeoPerimeter(Long folderId, HmGeoPerimeterVo vo) throws Exception {
        HmFolder folder = rep.findOne(folderId);
        if (null == folder) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        if (!FolderType.BUILDING.equals(folder.getFolderType())) {
            logger.error(String.format("Cannot create floor plan on folder with id %s as it's not a building",
                    folderId));
            throw new BadRequestException(ErrorCodes.folderCannotCreateFloorPlan,
                    new Object[][] { { "folderName", folder.getName() } });
        }
        List<HmFolder> childFolders = rep.findChildFolders(folderId);
        if (!childFolders.isEmpty()) {
            logger.error(String.format("Cannot create floor plan on folder with id %s as it already has floors",
                    folderId));
            throw new BadRequestException(ErrorCodes.folderAlreadyHasFloorPlan,
                    new Object[][] { { "folderName", folder.getName() } });
        }
        HmFolderVo floorVo = newHmFolderVo(folder.getOwnerId(), folder, "floor1");
        HmFolder floor = (HmFolder) voDoUtil.toDo(floorVo);
        floor.setFolderType(FolderType.FLOOR);
        floor.setIconType(IconType.FLOOR);
        if (!validateGeoPerimeter(floor, vo)) {
            logger.error(String.format("Cannot create floor plan on folder with id %s as it has invalid perimeter",
                    folderId));
            throw new BadRequestException(ErrorCodes.invalidGeoPerimeter,
                    new Object[][] { { "folderName", folder.getName() } });
        }
        floor = rep.saveAndFlush(floor);
        HmFolderVo fvo = voDoUtil.toVo(HmFolderVo.class, floor);
        return fvo;
    }

    private boolean validateGeoPerimeter(HmFolder floor, HmGeoPerimeterVo vo) throws Exception {
        double degToRad = Math.PI / 180.0;
        ArrayList<HmGeoVertexVo> latLngs = vo.getVertexes();
        double lat1 = latLngs.get(0).getLatitude().doubleValue();
        double lng1 = latLngs.get(0).getLongitude().doubleValue();
        double firstLat = lat1;
        double firstLng = lng1;
        double dlat1cos = Math.pow(Math.cos(degToRad * lat1), 2);
        double x_min = 0, x_max = 0, y_min = 0, y_max = 0;
        List<HmVertex> perimeter = new ArrayList<HmVertex>();
        int perimId = 0;
        HmVertex v = new HmVertex();
        v.setId(0);
        v.setX(0.0);
        v.setY(0.0);
        perimeter.add(v);
        for (int i = 1; i < latLngs.size() - 1; i++) {
            double lat2 = latLngs.get(i).getLatitude().doubleValue();
            double dLat = degToRad * (lat2 - lat1);
            double lng2 = latLngs.get(i).getLongitude().doubleValue();
            double dLon = degToRad * (lng2 - lng1);

            if (lat2 == firstLat && lng2 == firstLng) {
                perimId++;
                i++;
                firstLat = latLngs.get(i).getLatitude().doubleValue();
                firstLng = latLngs.get(i).getLongitude().doubleValue();
                lat2 = latLngs.get(i).getLatitude().doubleValue();
                dLat = degToRad * (lat2 - lat1);
                lng2 = latLngs.get(i).getLongitude().doubleValue();
                dLon = degToRad * (lng2 - lng1);
            }

            double adx = dlat1cos * Math.sin(dLon / 2) * Math.sin(dLon / 2);
            double cdx = 2 * Math.atan2(Math.sqrt(adx), Math.sqrt(1 - adx));
            double dx = 6371000.0 * cdx;
            if (lng2 < lng1) {
                dx = -dx;
            }
            if (dx < x_min) {
                x_min = dx;
            }
            if (dx > x_max) {
                x_max = dx;
            }

            double ady = Math.pow(Math.sin(dLat / 2), 2);
            double cdy = 2 * Math.atan2(Math.sqrt(ady), Math.sqrt(1 - ady));
            double dy = 6371000.0 * cdy;
            if (lat1 < lat2) {
                dy = -dy;
            }
            if (dy < y_min) {
                y_min = dy;
            }
            if (dy > y_max) {
                y_max = dy;
            }
            v = new HmVertex();
            v.setId(perimId);
            v.setX(dx);
            v.setY(dy);
            logger.info("adding vertex[" + perimeter.size() + "]: (" + v.getX() + ", " + v.getY() + ")");
            perimeter.add(v);
        }
        double width = x_max - x_min;
        double px = width * 0.1;
        double height = y_max - y_min;
        double py = height * 0.1;
        for (HmVertex vertex : perimeter) {
            vertex.setType(VertexType.DRY_WALL);
            vertex.setX(vertex.getX() - x_min + px);
            vertex.setY(vertex.getY() - y_min + py);
        }
        if (!validateGeoPerimeter(perimeter)) {
            return false;
        }
        if (floor != null) {
            floor.setMetricWidth(width + px * 2);
            floor.setMetricHeight(height + py * 2);
            floor.setImageWidth(floor.getMetricWidth().intValue());
            floor.setImageHeight(floor.getMetricHeight().intValue());
            floor.setPerimeter(perimeter);
        }
        return true;
    }

    private boolean validateGeoPerimeter(List<HmVertex> perimeter) throws Exception {
        int lastIndex = perimeter.size() - 1;
        HmVertex vertex = perimeter.get(lastIndex);
        Point2D lastPoint = new Point2D.Double(vertex.getX(), vertex.getY());
        Point2D l1p1 = lastPoint;
        int perimId = vertex.getId();
        for (int i = lastIndex - 1; i > 0; i--) {
            vertex = perimeter.get(i);
            Point2D l1p2 = new Point2D.Double(vertex.getX(), vertex.getY());
            logger.info("Compare wall (" + (i + 1) + ", " + i + ")");
            if (!validateGeoPerimeter(l1p1, l1p2, perimeter, perimId)) {
                return false;
            }
            vertex = perimeter.get(i - 1);
            if (vertex.getId() != perimId) {
                logger.info("Not last perimeter anymore, almost done " + vertex.getId());
                logger.info("Compare also wall (" + i + ", " + lastIndex + ")");
                return validateGeoPerimeter(l1p2, lastPoint, perimeter, perimId);
            }
            Point2D l2p1 = new Point2D.Double(vertex.getX(), vertex.getY());
            int l2p1i = i - 1;
            for (int j = i - 2; j >= 0; j--) {
                vertex = perimeter.get(j);
                if (vertex.getId() != perimId) {
                    logger.info("Not last perimeter anymore, break " + vertex.getId());
                    break;
                }
                Point2D l2p2 = new Point2D.Double(vertex.getX(), vertex.getY());
                logger.info("with wall (" + (j + 1) + ", " + j + ")");
                if (!validateGeoPerimeter(l1p1, l1p2, l2p1, l2p2)) {
                    return false;
                }
                l2p1 = l2p2;
                l2p1i = j;
            }
            if (i + 1 != lastIndex) {
                // One more wall
                logger.info("and wall (" + l2p1i + ", " + lastIndex + ")");
                if (!validateGeoPerimeter(l1p1, l1p2, l2p1, lastPoint)) {
                    return false;
                }
            }
            l1p1 = l1p2;
        }
        return true;
    }

    private boolean validateGeoPerimeter(Point2D l1p1, Point2D l1p2, List<HmVertex> perimeter, int lastPerimId)
            throws Exception {
        Point2D l2p1 = null;
        Point2D firstPoint = null;
        int firstPointIndex = 0;
        int perimId = -1;
        for (int i = 0; i < perimeter.size(); i++) {
            HmVertex vertex = perimeter.get(i);
            Point2D l2p2 = new Point2D.Double(vertex.getX(), vertex.getY());
            if (vertex.getId() != perimId) {
                perimId = vertex.getId();
                logger.info("New perimeter: " + perimId);
                if (firstPoint != null) {
                    logger.info("compare last wall (" + (i - 1) + ", " + firstPointIndex + ")");
                    if (!validateGeoPerimeter(l1p1, l1p2, l2p1, firstPoint)) {
                        return false;
                    }
                }
                if (perimId == lastPerimId) {
                    logger.info("This is the last perimeter, we're done.");
                    return true;
                }
                firstPoint = l2p2;
                firstPointIndex = i;
            } else {
                logger.info("compare with wall (" + (i - 1) + ", " + i + ")");
                if (!validateGeoPerimeter(l1p1, l1p2, l2p1, l2p2)) {
                    return false;
                }
            }
            l2p1 = l2p2;
        }
        return true;
    }

    private boolean validateGeoPerimeter(Point2D l1p1, Point2D l1p2, Point2D l2p1, Point2D l2p2) throws Exception {
        Point2D ip = llis(l1p1, l1p2, l2p1, l2p2);
        if (ip != null) {
            logger.info("Intersection at: " + ip);
            return false;
        }
        return true;
    }

    //folder and building name is unique among VHM, floor name is unique among building.
    private boolean isNameDuplicated(String name, HmFolder parent) {
        if (name == null || name.equals("") || parent == null) {
            return true;
        }
        if (FolderType.BUILDING.equals(parent.getFolderType())) {
            List<HmFolder> childrenList = findChildNodes(parent.getId());
            for (HmFolder hmFolder : childrenList) {
                if (name.equalsIgnoreCase(hmFolder.getName())) {
                    return true;
                }
            }
        } else if (FolderType.GENERIC.equals(parent.getFolderType())) {
            List<HmFolder> childrenList = findFolderBuildingNode(name, parent.getOwnerId());
            if (childrenList.size() > 0) {
                return true;
            }
        } else {
            //error type
            return true;
        }
        return false;
    }

    private List<HmFolder> findFolderBuildingNode(final String name, final Long ownerId) {
        Specification<HmFolder> spec = new Specification<HmFolder>() {
            @Override
            public Predicate toPredicate(Root<HmFolder> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Predicate[] predicates = new Predicate[2];
                predicates[0] = cb.equal(root.get("name"), name);
                predicates[1] = cb.equal(root.get("ownerId"), ownerId);
                return cb.and(predicates);
            }
        };
        return rep.findAll(spec);
    }

    public String getImageBaseUrl(Long ownerId) {
        String url = urlFactory.getTopoBackgroundImageBaseUrl(ownerId);

        return url;
    }

    private void setFolderImageSize(HmFolder folder) throws Exception {
        String backgroundImage = folder.getBackground();
        if (StringUtils.isEmpty(backgroundImage)) {
            // no background image attached
            Double sizeX = folder.getMetricWidth();
            Double sizeY = folder.getMetricHeight();
            if (null != sizeX && null != sizeY && sizeX.doubleValue() > 0 && sizeY.doubleValue() > 0) {
                folder.setImageWidth(sizeX.intValue());
                folder.setImageHeight(sizeY.intValue());
            } else {
                folder.setImageWidth(0);
                folder.setImageHeight(0);
            }
        } else {
            // background image attached
            HmImageMetadata o = imageRep.findByImageNameAndOwnerId(backgroundImage, folder.getOwnerId());
            if (null != o) {
                folder.setImageWidth(o.getImageWidth());
                folder.setImageHeight(o.getImageHeight());
                logger.info("Set " + folder.getName() + " background image Height : " + o.getImageHeight()
                        + "  Width : " + o.getImageWidth());
                // calculate metric height if metric width assigned
                if (null != folder.getMetricWidth()) {
                    folder.setMetricHeight(
                            folder.getMetricWidth() * folder.getImageHeight() / folder.getImageWidth());
                } else {
                    folder.setMetricHeight(null);
                }
            } else {
                folder.setImageWidth(0);
                folder.setImageHeight(0);
                folder.setMetricWidth(null);
                folder.setMetricHeight(null);
                folder.setBackground(null);
                logger.error("Set " + folder.getName()
                        + " background image Height and width to 0, as no image metadata found with image: "
                        + backgroundImage);
            }
        }
    }

    private void setDeviceElevation(HmFolder folder) {
        Double apElevation = folder.getDeviceElevation();
        if (apElevation == null) {
            folder.setDeviceElevation(getDefaultApElevation(folder));
        } else {
            double maxApElevation = MAX_AP_ELEVATION;
            if (folder.getLengthUnit() == LengthUnit.FEET) {
                maxApElevation /= FEET_TO_METERS;
            }
            double elevation = Math.min(apElevation.doubleValue(), maxApElevation);
            folder.setDeviceElevation(elevation);
        }
    }

    @Override
    @Transactional(readOnly = true)
    public ArrayList<HmFolderVo> getFolderPath(Long folderId) throws Exception {
        ArrayList<HmFolderVo> ret = new ArrayList<HmFolderVo>();
        HmFolder f = rep.findOne(folderId);
        HmFolder parent = null;
        if (null == f) {
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        parent = f.getParent();
        //1 add itself
        ret.add(voDoUtil.toVo(HmFolderVo.class, f));
        //2 add parents and root
        while (parent != null) {
            ret.add(voDoUtil.toVo(HmFolderVo.class, parent));
            parent = parent.getParent();
        }
        return ret;
    }

    @Override
    public HmFolderNetworkSummaryVo getFolderNetworkSummary(Long folderId) throws Exception {
        HmFolder f = rep.findOne(folderId);
        if (null == f) {
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        long start = System.currentTimeMillis();
        HmFolderNetworkSummaryVo vo = new HmFolderNetworkSummaryVo();
        int nup = 0, ndn = 0, mup = 0, mdn = 0;
        long actn = 0;
        List<Long> folderIds = new ArrayList<>();
        folderIds.add(folderId);
        loadChildFolderIds(folderId, folderIds);
        logger.info(String.format("Total folder count is %d", folderIds.size()));
        List<Long> deviceIds = deviceLocationExRep.findAllHmDeviceIdsInFolder(folderIds);
        logger.info(String.format("Total devices count on these folder is %d", deviceIds.size()));
        for (Long deviceId : deviceIds) {
            HmDevice device = devRep.find(deviceId);
            if (null == device) {
                logger.warn(String.format("device cannot be found from device repository by id %d, may be removed?",
                        deviceId));
                continue;
            }
            // query manage status, connection status
            if (AhDeviceAdminState.MANAGED == device.getAdminState()) {
                if (device.getIsConnected()) {
                    mup++;
                } else {
                    mdn++;
                }
            } else {
                if (device.getIsConnected()) {
                    nup++;
                } else {
                    ndn++;
                }
            }
            // query active client count
            HmActiveClientsPerDevice stats = statsService.getNewestStat(deviceId, HmActiveClientsPerDevice.class);
            if (null != stats && null != stats.getIds()) {
                actn += stats.getIds().size();
            }
        }
        vo.setManagedConnectedCount(mup);
        vo.setManagedCount(mup + mdn);
        vo.setUnManagedConnectedCount(nup);
        vo.setUnManagedCount(nup + ndn);
        vo.setActiveClientCount(actn);

        long cost = System.currentTimeMillis() - start;
        if (cost > 500) {
            logger.warn(String.format("Too slow to query network summary for folder %s, it cost %d ms.",
                    f.getName(), cost));
        }
        return vo;
    }

    @Override
    public Long[] getDeviceIdsInFolder(Long folderId, boolean includeChildFolder) throws Exception {
        HmFolder f = rep.findOne(folderId);
        if (null == f) {
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        List<Long> folderIds = new ArrayList<>();
        folderIds.add(folderId);
        if (includeChildFolder) {
            loadChildFolderIds(folderId, folderIds);
        }
        logger.info(String.format("Total folder count is %d", folderIds.size()));
        List<Long> deviceIds = deviceLocationExRep.findAllHmDeviceIdsInFolder(folderIds);
        logger.info(String.format("Total devices count on these folder is %d", deviceIds.size()));
        return deviceIds.toArray(new Long[0]);
    }

    private void loadChildFolderIds(Long id, List<Long> list) {
        List<HmFolder> children = rep.findChildFolders(id);
        if (null != children && !children.isEmpty()) {
            for (HmFolder child : children) {
                list.add(child.getId());
                if (FolderType.FLOOR.equals(child.getFolderType())) {
                    continue;
                }
                loadChildFolderIds(child.getId(), list);
            }
        }
    }

    private static Double getDefaultApElevation(HmFolder folder) {
        if (folder.getLengthUnit() == LengthUnit.FEET) {
            return 3 / FEET_TO_METERS;
        } else {
            return 3.0;
        }
    }

    private static class Perimeter {
        private Perimeter(int start, int end, double area, int count) {
            this.start = start;
            this.end = end;
            this.area = Math.abs(area);
            inside = new short[count];
        }

        private int start, end;
        private double area;
        private short[] inside;
        private short depth;
    }

    @Override
    public HmFileUploadResponsedVo exportFolder(Long folderId, Long ownerId) throws Exception {
        HmFolderDataConvertVo vo = prepareFolder(folderId);
        HmFileUploadResponsedVo responseVo = new HmFileUploadResponsedVo();
        String regex = "[/?*|<>:\"\\\\]";
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(vo.getName());
        String str = m.replaceAll("_").trim();
        final String file_prefix = str + "_" + new Date().getTime();
        Set<String> imageNames = new HashSet<>();
        collectImageNamesFromNode(vo, imageNames);
        File downloadFile = generateXML(file_prefix, vo, ownerId, responseVo);
        if (downloadFile != null && imageNames.size() > 0) {
            downloadFile = makeXMLBgImages2Tar(file_prefix, ownerId, imageNames, responseVo);
        }
        try (FileInputStream input = new FileInputStream(downloadFile)) {
            responseVo.setFileInput(IOUtils.toByteArray(input));
            responseVo.setFileName(downloadFile.getName());
        } catch (Exception e) {
            logger.error(
                    String.format("Error to download the file : %s: %s", downloadFile.getName(), e.getMessage()));
            throw new AhCarrierException(e, ErrorCodes.fileDownloadFailed,
                    new Object[][] { { "fileName", downloadFile.getName() }, });
        } finally {
            if (downloadFile != null && imageNames.size() > 0) {
                FileUtils.deleteQuietly(downloadFile);
                if (downloadFile.getParentFile().isDirectory()) {
                    FileUtils.deleteDirectory(downloadFile.getParentFile());
                }
            } else {
                FileUtils.deleteQuietly(downloadFile);
                File baseDirectory = new File(getFolderFileBasePath(file_prefix, ownerId));
                FileUtils.forceDelete(baseDirectory);
            }
        }
        return responseVo;
    }

    private HmFolderDataConvertVo prepareFolder(Long folderId) throws Exception {
        logger.debug(String.format("find elements under folder %s", folderId));
        HmFolderVo folder = findFolder(folderId);
        if (null == folder) {
            logger.error(String.format("Cannot find folder with id %s", folderId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", folderId } });
        }
        HmFolderDataConvertVo vo = new HmFolderDataConvertVo();
        vo.setFolderType(folder.getFolderType().toString());
        vo.setName(folder.getName());
        vo.setBackground(folder.getBackground());
        vo.setAddress(folder.getAddress());
        vo.setIconType(folder.getIconType().toString());
        vo.setImageWidth(folder.getImageWidth());
        vo.setImageHeight(folder.getImageHeight());
        vo.setMetricWidth(folder.getMetricWidth());
        vo.setMetricHeight(folder.getMetricHeight());
        vo.setDeviceElevation(folder.getDeviceElevation());
        vo.setFloorLoss(folder.getFloorLoss());
        vo.setEnvironmentType(folder.getEnvironmentType().toString());
        vo.setLengthUnit(folder.getLengthUnit().toString());
        vo.setX(folder.getX());
        vo.setY(folder.getY());
        vo.setCenterZoom(folder.getCenterZoom());
        vo.setOffsetX(folder.getOffsetX());
        vo.setOffsetY(folder.getOffsetY());

        vo.setLatitude(folder.getLatitude());
        vo.setLongitude(folder.getLongitude());
        vo.setCenterLatitude(folder.getCenterLatitude());
        vo.setCenterLongitude(folder.getCenterLongitude());

        HmFolderElementsVo folderElements = findFolderElements(folderId);
        vo.setWalls(folderElements.getWalls());
        vo.setPerimeters(folderElements.getPerimeters());

        ArrayList<HmFolderDataConvertVo> childrFolders = new ArrayList<>();
        for (HmFolderVo node : folderElements.getChildFolders()) {
            childrFolders.add(prepareFolder(node.getId()));
        }
        vo.setChildren(childrFolders);
        return vo;
    }

    private File generateXML(final String file_prefix, HmFolderDataConvertVo folder, Long ownerId,
            HmFileUploadResponsedVo responseVo) throws Exception {
        String ext = ".xml";
        String destFilePath = getFolderDataFilePath(file_prefix, ownerId);
        File file = null;
        Marshaller marshaller = null;
        try {
            JAXBContext context = JAXBContext.newInstance(HmFolderDataConvertVo.class);
            marshaller = context.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            file = new File(destFilePath + file_prefix + ext);
            if (!file.getParentFile().exists()) {
                file.getParentFile().mkdirs();
            }
        } catch (Exception e) {
            logger.error(String.format("File can not be created: %s", e.getMessage()));
            throw new InternalServerException(e, ErrorCodes.fileCannotBeCreated);
        }

        try (FileOutputStream fs = new FileOutputStream(file);
                BufferedOutputStream bos = new BufferedOutputStream(fs)) {
            marshaller.marshal(folder, bos);
        } catch (Exception e) {
            logger.error(String.format("File can not be created: %s", e.getMessage()));
            throw new InternalServerException(e, ErrorCodes.fileCannotBeCreated);
        }

        return file;
    }

    private File makeXMLBgImages2Tar(final String file_prefix, Long ownerId, Set<String> imageNames,
            HmFileUploadResponsedVo responseVo) throws AhCarrierException, InternalServerException {
        if (!(null == imageNames || imageNames.isEmpty())) {
            final String ext_tar = ".tar";
            final String ext_xml = ".xml";

            String tarFilePath = getTarFolderPath(file_prefix, ownerId);
            File tarFolder = new File(tarFilePath);
            if (!tarFolder.exists()) {
                tarFolder.mkdirs();
            }

            // copy the background images to subfolder 'bgImages' under tar
            // folder
            String srcPath = getFolderBgImagePath(file_prefix, ownerId);
            String srcFileName = "";
            for (String imageName : imageNames) {
                srcFileName = srcPath + imageName;
                File file = new File(srcFileName);
                if (!file.getParentFile().exists()) {
                    file.getParentFile().mkdirs();
                } else {
                    file.getParentFile().delete();
                }

                try (FileOutputStream stream = new FileOutputStream(file)) {
                    //move image file
                    stream.write(imageService.getImage(ownerId, imageName));
                    stream.flush();
                    stream.close();
                    FileUtils.moveFileToDirectory(file, new File(tarFolder, "bgImages"), true);

                    //remove image directory
                    File imageDirectory = new File(srcPath);
                    if (imageDirectory.exists() && imageDirectory.isDirectory()
                            && imageDirectory.list().length == 0) {
                        imageDirectory.delete();
                    }

                    //move xml file
                    srcFileName = getFolderDataFilePath(file_prefix, ownerId) + file_prefix + ext_xml;
                    final File xmlFile = new File(srcFileName);
                    FileUtils.moveFileToDirectory(xmlFile, tarFolder, true);
                    if (xmlFile.getParentFile().exists() && xmlFile.getParentFile().isDirectory()
                            && xmlFile.getParentFile().list().length == 0) {
                        xmlFile.getParentFile().delete();
                    }

                    final String destTarFilePath = getFolderFileBasePath(file_prefix, ownerId) + file_prefix
                            + ext_tar;
                    if (new TarArchive().createTarZip(tarFilePath, destTarFilePath)) {
                        // copy tar file to maps folder
                        final File tarFile = new File(destTarFilePath);

                        // delete files and tar directory under /tmp/maps
                        FileUtils.deleteQuietly(tarFolder);
                        FileUtils.deleteQuietly(tarFile);
                        final File gzFile = new File(destTarFilePath + TarArchive.SUFFIX_ZIP);
                        return gzFile;
                    }
                } catch (Exception e) {
                    logger.error(String.format("Error to download background-image file %s: %s", imageName,
                            e.getMessage()));
                    throw new AhCarrierException(e, ErrorCodes.fileDownloadFailed,
                            new Object[][] { { "fileName", imageName }, });
                }

            }
        }
        return null;
    }

    private Set<String> collectImageNamesFromNode(HmFolderDataConvertVo vo, Set<String> imageNames) {
        if (StringUtils.isNotEmpty(vo.getBackground())) {
            imageNames.add(vo.getBackground());
        }
        for (HmFolderDataConvertVo child : vo.getChildren()) {
            collectImageNamesFromNode(child, imageNames);
        }
        return imageNames;
    }

    private String getFolderFileBasePath(final String file_prefix, final Long ownerId) {
        return getFolderBasePath() + ownerId + File.separator + file_prefix + File.separator;
    }

    private String getFolderBasePath() {
        return "tmp" + File.separator + "maps" + File.separator;
    }

    private String getFolderBgImagePath(final String file_prefix, final Long ownerId) {
        return getFolderFileBasePath(file_prefix, ownerId) + "images" + File.separator;
    }

    private String getFolderDataFilePath(final String file_prefix, final Long ownerId) {
        return getFolderFileBasePath(file_prefix, ownerId) + "xml" + File.separator;
    }

    private String getTarFolderPath(final String file_prefix, final Long ownerId) {
        return getFolderFileBasePath(file_prefix, ownerId) + "tar" + File.separator;
    }

    private HmFolderVo parseDataForXML(HmFolderDataConvertVo folder, Long parentId, Long ownerId,
            boolean overrideBg, Map<String, String> imageNameMapping) throws Exception {
        HmFolder parent = rep.findOne(parentId);

        if (null == parent) {
            logger.error(String.format("Cannot find folder with id %s", parentId));
            throw new BadRequestException(ErrorCodes.folderNotExisted, new Object[][] { { "id", parentId } });
        }
        if (FolderType.FLOOR.equals(parent.getFolderType())) {
            logger.error(String.format("Cannot be create folder %s under the floor %s", folder.getName(),
                    parent.getName()));
            throw new BadRequestException(ErrorCodes.cannotCreateFolderUnderFloor);
        }

        if (FolderType.BUILDING.equals(parent.getFolderType())
                && (!FolderType.FLOOR.toString().equals(folder.getFolderType()))) {
            logger.error(String.format("Cannot be create building %s under the building %s", folder.getName(),
                    parent.getName()));
            throw new BadRequestException(ErrorCodes.cannotCreateBuildingUnderBuilding);
        }
        if (FolderType.GENERIC.equals(parent.getFolderType())
                && FolderType.FLOOR.toString().equals(folder.getFolderType())) {
            logger.error(String.format("Cannot be create floor %s under the folder %s", folder.getName(),
                    parent.getName()));
            throw new BadRequestException(ErrorCodes.cannotCreateFloorUnderFolder);
        }
        if (isNameDuplicated(folder.getName(), parent)) {
            logger.error(String.format(
                    "You cannot create a duplicate name child with other children, parent name %s, child name %s",
                    parent.getName(), folder.getName()));
            throw new BadRequestException(ErrorCodes.duplicateNameCannotBeCreated,
                    new Object[][] { { "name", folder.getName() } });
        }

        HmFolderVo vo = new HmFolderVo();
        vo.setFolderType(folder.getFolderType().toString());
        vo.setName(folder.getName());
        vo.setUniqueName(folder.getName());
        vo.setAddress(folder.getAddress());
        vo.setIconType(folder.getIconType().toString());
        vo.setImageWidth(folder.getImageWidth());
        vo.setImageHeight(folder.getImageHeight());
        vo.setMetricWidth(folder.getMetricWidth());
        vo.setMetricHeight(folder.getMetricHeight());
        vo.setDeviceElevation(folder.getDeviceElevation());
        vo.setFloorLoss(folder.getFloorLoss());
        vo.setEnvironmentType(folder.getEnvironmentType().toString());
        vo.setLengthUnit(folder.getLengthUnit().toString());
        vo.setX(folder.getX());
        vo.setY(folder.getY());
        vo.setCenterZoom(folder.getCenterZoom());
        vo.setOffsetX(folder.getOffsetX());
        vo.setOffsetY(folder.getOffsetY());

        if (overrideBg) {
            vo.setBackground(folder.getBackground());
        } else {
            if (folder.getBackground() == null || folder.getBackground().isEmpty()
                    || imageNameMapping.get(folder.getBackground()) == null) {
                vo.setBackground(folder.getBackground());
            } else {
                vo.setBackground(imageNameMapping.get(folder.getBackground()));
            }
        }

        vo.setLatitude(folder.getLatitude());
        vo.setLongitude(folder.getLongitude());
        vo.setCenterLatitude(folder.getCenterLatitude());
        vo.setCenterLongitude(folder.getCenterLongitude());

        vo.setParentId(parentId);
        vo.setOwnerId(ownerId);

        HmFolder folderDo = (HmFolder) voDoUtil.toDo(vo);
        folderDo = rep.save(folderDo);

        if (!folder.getPerimeters().isEmpty()) {
            for (HmPerimeterVo perimeterVo : folder.getPerimeters()) {
                createPerimeter(folderDo.getId(), perimeterVo);
            }
        }

        if (!folder.getWalls().isEmpty()) {
            HmWallsInFolderVo wallObj = new HmWallsInFolderVo();
            wallObj.setWalls(folder.getWalls());
            saveWalls(folderDo.getId(), wallObj);
        }

        if (!folder.getChildren().isEmpty()) {
            for (HmFolderDataConvertVo folderVo : folder.getChildren()) {
                parseDataForXML(folderVo, folderDo.getId(), ownerId, overrideBg, imageNameMapping);
            }
        }

        vo = voDoUtil.toVo(HmFolderVo.class, folderDo);
        return vo;
    }

    private boolean parseXmlFile(Long folderId, Long ownerId, String originalFilename, InputStream fileInput,
            boolean overrideBg, Map<String, String> imageNameMapping) throws Exception {
        boolean flag = false;
        HmFolderDataConvertVo folder = null;
        try (InputStreamReader inputReader = new InputStreamReader(fileInput)) {
            JAXBContext context = JAXBContext.newInstance(HmFolderDataConvertVo.class);
            Unmarshaller unmarshaller = context.createUnmarshaller();
            folder = (HmFolderDataConvertVo) unmarshaller.unmarshal(inputReader);

        } catch (Exception e) {
            logger.error(String.format("Parse the XML file %s failed: %s", originalFilename, e.getMessage()));
            throw new InternalServerException(e, ErrorCodes.fileUploadFailed,
                    new Object[][] { { "fileName", originalFilename } });
        }
        if (folder != null) {
            HmFolderVo folderVo = parseDataForXML(folder, folderId, ownerId, overrideBg, imageNameMapping);
            if (folderVo != null) {
                flag = true;
            }
        }

        return flag;
    }

    @Override
    @Transactional
    public HmFileUploadResponsedVo importFolder(Long folderId, Long ownerId, boolean overrideBg,
            String originalFilename, InputStream fileInput) throws Exception {
        final String tar_ext = ".tar.gz";
        final String xml_ext = ".xml";
        boolean status = false;
        HmFileUploadResponsedVo vo = new HmFileUploadResponsedVo();
        Map<String, String> imageNameMapping = new HashMap<String, String>();

        vo.setFileName(originalFilename);
        if (originalFilename.toLowerCase().endsWith(tar_ext)) {
            status = extractTarFile(folderId, ownerId, overrideBg, originalFilename, fileInput, imageNameMapping);
        } else if (originalFilename.toLowerCase().endsWith(xml_ext)) {
            status = parseXmlFile(folderId, ownerId, originalFilename, fileInput, overrideBg, imageNameMapping);
        } else {
            vo.setMessage("The file type is not supported.");
        }
        vo.setStatus(status);
        if (status) {
            vo.setMessage("The file uploaded successfully.");
        } else {
            vo.setMessage("The file upload failed.");
        }
        return vo;
    }

    private boolean extractTarFile(Long folderId, Long ownerId, boolean overrideBg, String originalFilename,
            InputStream fileInput, Map<String, String> imageNameMapping) throws InternalServerException {
        boolean flag = false;
        final String tar_gz_ext = ".tar.gz";
        final String file_prefix = "" + new Date().getTime();
        String destPath = getFolderFileBasePath(file_prefix, ownerId);
        String filePath = destPath + file_prefix + tar_gz_ext;
        File folder = new File(filePath);
        if (!folder.getParentFile().exists()) {
            folder.getParentFile().mkdirs();
        }

        InputStream input = null;
        try (FileOutputStream outputStream = new FileOutputStream(folder)) {
            int read = 0;
            byte[] bytes = new byte[2048];

            while ((read = fileInput.read(bytes)) != -1) {
                outputStream.write(bytes, 0, read);
            }
            outputStream.flush();
            outputStream.close();
            new TarArchive().unzipGZipFile(filePath, destPath);

            //delete gzip file
            if (folder.exists()) {
                FileUtils.deleteQuietly(folder);
            }

            folder = new File(destPath);
            File[] files = folder.listFiles();

            for (File file : files) {
                if (file.isFile()) {
                    try {
                        input = new FileInputStream(file);
                        parseXmlFile(folderId, ownerId, file.getName(), input, overrideBg, imageNameMapping);
                    } catch (Exception e) {
                        logger.error(String.format("Upload file %s failed: %s", originalFilename, e.getMessage()));
                        throw new InternalServerException(e, ErrorCodes.fileUploadFailed,
                                new Object[][] { { "fileName", originalFilename } });
                    } finally {
                        if (input != null) {
                            try {
                                input.close();
                            } catch (Exception e) {
                                logger.error(String.format("Upload file %s failed: %s", originalFilename,
                                        e.getMessage()));
                                throw new InternalServerException(e, ErrorCodes.fileUploadFailed,
                                        new Object[][] { { "fileName", originalFilename } });
                            }
                        }
                    }

                } else {
                    File[] imageFiles = file.listFiles();
                    // copy map background images
                    if (null != imageFiles) {
                        String imagepath = destPath + "bgImages" + File.separator;
                        if (overrideBg) {
                            for (File imageFile : imageFiles) {
                                try {
                                    input = new FileInputStream(imageFile);
                                    imageService.uploadImage(ownerId, imageFile.getName(),
                                            IOUtils.toByteArray(input));
                                } catch (Exception e) {
                                    logger.error(String.format("Upload file %s failed: %s", originalFilename,
                                            e.getMessage()));
                                    throw new InternalServerException(e, ErrorCodes.fileUploadFailed,
                                            new Object[][] { { "fileName", originalFilename } });
                                } finally {
                                    if (input != null) {
                                        try {
                                            input.close();
                                        } catch (Exception e) {
                                            logger.error(String.format("Upload file %s failed: %s",
                                                    originalFilename, e.getMessage()));
                                            throw new InternalServerException(e, ErrorCodes.fileUploadFailed,
                                                    new Object[][] { { "fileName", originalFilename } });
                                        }
                                    }
                                }
                            }
                        } else {
                            for (File imageFile : imageFiles) {
                                String fileName = checkImageNameForImportFolder(imageFile.getName(), ownerId,
                                        imageNameMapping);
                                if (!fileName.equalsIgnoreCase(imageFile.getName())) {
                                    try {
                                        File destFile = new File(imagepath + fileName);
                                        FileUtils.moveFile(imageFile, destFile);
                                        input = new FileInputStream(destFile);
                                        imageService.uploadImage(ownerId, destFile.getName(),
                                                IOUtils.toByteArray(input));
                                    } catch (Exception e) {
                                        logger.error(String.format("Upload file %s failed: %s", originalFilename,
                                                e.getMessage()));
                                        throw new InternalServerException(e, ErrorCodes.fileUploadFailed,
                                                new Object[][] { { "fileName", originalFilename } });
                                    } finally {
                                        if (input != null) {
                                            try {
                                                input.close();
                                            } catch (Exception e) {
                                                logger.error(String.format("Upload file %s failed: %s",
                                                        originalFilename, e.getMessage()));
                                                throw new InternalServerException(e, ErrorCodes.fileUploadFailed,
                                                        new Object[][] { { "fileName", originalFilename } });
                                            }
                                        }
                                    }
                                } else {
                                    try {
                                        input = new FileInputStream(imageFile);
                                        imageService.uploadImage(ownerId, imageFile.getName(),
                                                IOUtils.toByteArray(input));
                                    } catch (Exception e) {
                                        logger.error(String.format("Upload file %s failed: %s", originalFilename,
                                                e.getMessage()));
                                        throw new InternalServerException(e, ErrorCodes.fileUploadFailed,
                                                new Object[][] { { "fileName", originalFilename } });
                                    } finally {
                                        if (input != null) {
                                            try {
                                                input.close();
                                            } catch (Exception e) {
                                                logger.error(String.format("Upload file %s failed: %s",
                                                        originalFilename, e.getMessage()));
                                                throw new InternalServerException(e, ErrorCodes.fileUploadFailed,
                                                        new Object[][] { { "fileName", originalFilename } });
                                            }
                                        }
                                    }
                                }
                            }
                        }

                    }
                }
            }
            flag = true;
        } catch (Exception e) {
            logger.error(String.format("Upload file %s failed: %s", originalFilename, e.getMessage()));
            throw new InternalServerException(e, ErrorCodes.fileUploadFailed,
                    new Object[][] { { "fileName", originalFilename } });
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    logger.error(String.format("Upload file %s failed: %s", originalFilename, e.getMessage()));
                    throw new InternalServerException(e, ErrorCodes.fileUploadFailed,
                            new Object[][] { { "fileName", originalFilename } });
                }
            }
            try {
                FileUtils.cleanDirectory(folder);
                FileUtils.deleteDirectory(folder);
            } catch (IOException e) {
                logger.error(String.format("Upload file %s failed: %s", originalFilename, e.getMessage()));
                throw new InternalServerException(e, ErrorCodes.fileUploadFailed,
                        new Object[][] { { "fileName", originalFilename } });
            }
        }
        return flag;
    }

    private String checkImageNameForImportFolder(String imageFileName, Long ownerId,
            Map<String, String> imageNameMapping) throws Exception {
        HmImageMetadata o = imageRep.findByImageNameAndOwnerId(imageFileName, ownerId);
        int index = 0;
        String newFileName = "";
        String fileName = imageFileName.substring(0, imageFileName.lastIndexOf("."));
        String[] arr = imageFileName.split("\\.");
        String extension = arr[arr.length - 1];
        while (o != null && index < 100) {
            newFileName = fileName + new Date().getTime() + "." + extension;
            o = imageRep.findByImageNameAndOwnerId(newFileName, ownerId);
            index++;
        }
        if (!newFileName.isEmpty()) {
            imageNameMapping.put(imageFileName, newFileName);
            return newFileName;
        }
        return imageFileName;
    }

    // CUSTOM_CODE_BLOCK 1 END

    @Component
    static public class ErrorCodes extends BaseErrorCodes {

        // CUSTOM_CODE_BLOCK 2 BEGIN
        static public String folderAlreadyInitialized;
        static public String folderNotExisted;
        static public String invalidPerimeter;
        static public String folderCannotBeCloned;
        static public String folderCannotCreateFloorPlan;
        static public String folderAlreadyHasFloorPlan;
        static public String invalidGeoPerimeter;
        static public String buildingCannotBeAssigned;
        static public String folderCannotBeResized;
        static public String duplicateNameCannotBeCreated;
        static public String duplicateNameCannotBeUpdated;
        static public String floorCannotBeMoved;
        static public String targetParentMustBeFolder;
        static public String cannotMoveToOwnSubFolder;
        static public String rootFolderCannotBeRemoved;
        static public String removeFolderNotExist;
        static public String folderCannotRequestBuildingView;
        static public String folderCannotRequestFloorNail;
        static public String folderCannotBeAligned;
        static public String cannotCreateFolderUnderFloor;
        static public String cannotCreateFloorUnderFolder;
        static public String cannotCreateBuildingUnderBuilding;

        static public String fileCannotBeCreated;
        static public String fileUploadFailed;
        static public String fileDownloadFailed;

        // CUSTOM_CODE_BLOCK 2 END

        @Override
        protected String getErrorCodePrefix() {
            return "folder.service";
        }

    }
}