<commit_before>package de.gurkenlabs.utiliti.components;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;

import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;

import de.gurkenlabs.core.Align;
import de.gurkenlabs.core.Valign;
import de.gurkenlabs.litiengine.Game;
import de.gurkenlabs.litiengine.Resources;
import de.gurkenlabs.litiengine.SpriteSheetInfo;
import de.gurkenlabs.litiengine.entities.CollisionEntity;
import de.gurkenlabs.litiengine.entities.DecorMob.MovementBehavior;
import de.gurkenlabs.litiengine.environment.Environment;
import de.gurkenlabs.litiengine.environment.tilemap.IImageLayer;
import de.gurkenlabs.litiengine.environment.tilemap.IMap;
import de.gurkenlabs.litiengine.environment.tilemap.IMapLoader;
import de.gurkenlabs.litiengine.environment.tilemap.IMapObject;
import de.gurkenlabs.litiengine.environment.tilemap.IMapObjectLayer;
import de.gurkenlabs.litiengine.environment.tilemap.ITileset;
import de.gurkenlabs.litiengine.environment.tilemap.MapObjectProperty;
import de.gurkenlabs.litiengine.environment.tilemap.MapObjectType;
import de.gurkenlabs.litiengine.environment.tilemap.MapUtilities;
import de.gurkenlabs.litiengine.environment.tilemap.TmxMapLoader;
import de.gurkenlabs.litiengine.environment.tilemap.xml.Map;
import de.gurkenlabs.litiengine.environment.tilemap.xml.MapObject;
import de.gurkenlabs.litiengine.environment.tilemap.xml.MapObjectLayer;
import de.gurkenlabs.litiengine.graphics.ImageCache;
import de.gurkenlabs.litiengine.graphics.LightSource;
import de.gurkenlabs.litiengine.graphics.RenderEngine;
import de.gurkenlabs.litiengine.graphics.Spritesheet;
import de.gurkenlabs.litiengine.gui.GuiComponent;
import de.gurkenlabs.litiengine.input.Input;
import de.gurkenlabs.util.MathUtilities;
import de.gurkenlabs.util.geom.GeometricUtilities;
import de.gurkenlabs.util.io.FileUtilities;
import de.gurkenlabs.util.io.ImageSerializer;
import de.gurkenlabs.utiliti.EditorScreen;
import de.gurkenlabs.utiliti.Program;
import de.gurkenlabs.utiliti.UndoManager;

public class MapComponent extends EditorComponent {
  public enum TransformType {
    UP,
    DOWN,
    LEFT,
    RIGHT,
    UPLEFT,
    UPRIGHT,
    DOWNLEFT,
    DOWNRIGHT,
    NONE
  }

  private static final String DEFAULT_MAPOBJECTLAYER_NAME = "default";
  private static final int TRANSFORM_RECT_SIZE = 6;
  private static final int BASE_SCROLL_SPEED = 50;
  private double currentTransformRectSize = TRANSFORM_RECT_SIZE;
  private final java.util.Map<TransformType, Rectangle2D> transformRects;
  private TransformType currentTransform;

  public static final int EDITMODE_CREATE = 0;
  public static final int EDITMODE_EDIT = 1;
  public static final int EDITMODE_MOVE = 2;
  private final List<Consumer<Integer>> editModeChangedConsumer;
  private final List<Consumer<IMapObject>> focusChangedConsumer;
  private final List<Consumer<Map>> mapLoadedConsumer;
  private int currentEditMode = EDITMODE_EDIT;
  private static final float[] zooms = new float[] { 0.1f, 0.25f, 0.5f, 1, 1.5f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 16f, 32f, 50f, 80f, 100f };
  private int currentZoomIndex = 7;

  private final java.util.Map<String, Point2D> cameraFocus;
  private final java.util.Map<String, IMapObject> focusedObjects;

  private final List<Map> maps;

  private float scrollSpeed = BASE_SCROLL_SPEED;

  private Point2D startPoint;
  private Point2D dragPoint;
  private Point2D dragLocationMapObject;
  private boolean isMoving;
  private boolean isTransforming;
  private Dimension dragSizeMapObject;
  private Rectangle2D newObject;
  private IMapObject copiedMapObject;
  private int gridSize;

  public boolean snapToGrid = true;
  public boolean renderGrid = false;
  public boolean renderCollisionBoxes = true;

  private final EditorScreen screen;

  private boolean isLoading;

  public MapComponent(final EditorScreen screen) {
    super(ComponentType.MAP);
    this.editModeChangedConsumer = new CopyOnWriteArrayList<>();
    this.focusChangedConsumer = new CopyOnWriteArrayList<>();
    this.mapLoadedConsumer = new CopyOnWriteArrayList<>();
    this.focusedObjects = new ConcurrentHashMap<>();
    this.maps = new ArrayList<>();
    this.cameraFocus = new ConcurrentHashMap<>();
    this.transformRects = new ConcurrentHashMap<>();
    this.screen = screen;
    Game.getCamera().onZoomChanged(zoom -> {
      this.currentTransformRectSize = TRANSFORM_RECT_SIZE / zoom;
      this.updateTransformControls();
    });
    this.gridSize = Program.USER_PREFERNCES.getGridSize();
  }

  public void onEditModeChanged(Consumer<Integer> cons) {
    this.editModeChangedConsumer.add(cons);
  }

  public void onFocusChanged(Consumer<IMapObject> cons) {
    this.focusChangedConsumer.add(cons);
  }

  public void onMapLoaded(Consumer<Map> cons) {
    this.mapLoadedConsumer.add(cons);
  }

  @Override
  public void render(Graphics2D g) {
    if (Game.getEnvironment() == null) {
      return;
    }

    Color COLOR_BOUNDING_BOX_FILL = new Color(0, 0, 0, 35);
    Color COLOR_NAME_FILL = new Color(0, 0, 0, 60);
    final Color COLOR_FOCUS_FILL = new Color(0, 0, 0, 50);
    final Color COLOR_FOCUS_BORDER = Color.BLACK;
    final Color COLOR_COLLISION_FILL = new Color(255, 0, 0, 15);
    final Color COLOR_NOCOLLISION_FILL = new Color(255, 100, 0, 15);
    final Color COLOR_COLLISION_BORDER = Color.RED;
    final Color COLOR_NOCOLLISION_BORDER = Color.ORANGE;
    final Color COLOR_SPAWNPOINT = Color.GREEN;
    final Color COLOR_LANE = Color.YELLOW;
    final Color COLOR_NEWOBJECT_FILL = new Color(0, 255, 0, 50);
    final Color COLOR_NEWOBJECT_BORDER = Color.GREEN.darker();
    final Color COLOR_TRANSFORM_RECT_FILL = new Color(255, 255, 255, 100);
    final Color COLOR_SHADOW_FILL = new Color(85, 130, 200, 15);
    final Color COLOR_SHADOW_BORDER = new Color(30, 85, 170);

    if (this.renderCollisionBoxes) {
      // render all entities
      for (final IMapObjectLayer layer : Game.getEnvironment().getMap().getMapObjectLayers()) {
        if (layer == null) {
          continue;
        }

        if (!EditorScreen.instance().getMapSelectionPanel().isSelectedMapObjectLayer(layer.getName())) {
          continue;
        }

        if (layer.getColor() != null) {
          COLOR_BOUNDING_BOX_FILL = new Color(layer.getColor().getRed(), layer.getColor().getGreen(), layer.getColor().getBlue(), 15);
        } else {
          COLOR_BOUNDING_BOX_FILL = new Color(0, 0, 0, 35);
        }

        for (final IMapObject mapObject : layer.getMapObjects()) {
          if (mapObject == null) {
            continue;
          }
          String objectName = mapObject.getName();
          if (objectName != null && !objectName.isEmpty()) {
            Rectangle2D nameBackground = new Rectangle2D.Double(mapObject.getX(), mapObject.getBoundingBox().getMaxY() - 3,
                mapObject.getDimension().getWidth(), 3);
            g.setColor(COLOR_NAME_FILL);
            RenderEngine.fillShape(g, nameBackground);
          }

          MapObjectType type = MapObjectType.get(mapObject.getType());

          // render spawn points
          if (type == MapObjectType.SPAWNPOINT) {
            g.setColor(COLOR_SPAWNPOINT);
            RenderEngine.fillShape(g,
                new Rectangle2D.Double(mapObject.getBoundingBox().getCenterX() - 1, mapObject.getBoundingBox().getCenterY() - 1, 2, 2));
            RenderEngine.drawShape(g, mapObject.getBoundingBox());
          } else if (type == MapObjectType.COLLISIONBOX) {
            g.setColor(COLOR_COLLISION_FILL);
            RenderEngine.fillShape(g, mapObject.getBoundingBox());
            g.setColor(COLOR_COLLISION_BORDER);
            RenderEngine.drawShape(g, mapObject.getBoundingBox());
          } else if (type == MapObjectType.STATICSHADOW) {
            g.setColor(COLOR_SHADOW_FILL);
            RenderEngine.fillShape(g, mapObject.getBoundingBox());
            g.setColor(COLOR_SHADOW_BORDER);
            RenderEngine.drawShape(g, mapObject.getBoundingBox());
          } else if (type == MapObjectType.LANE) {
            // render lane

            if (mapObject.getPolyline() == null || mapObject.getPolyline().getPoints().size() == 0) {
              continue;
            }
            // found the path for the rat
            final Path2D path = MapUtilities.convertPolylineToPath(mapObject);
            if (path == null) {
              continue;
            }

            g.setColor(COLOR_LANE);
            RenderEngine.drawShape(g, path);
            Point2D start = new Point2D.Double(mapObject.getLocation().getX(), mapObject.getLocation().getY());
            RenderEngine.fillShape(g, new Ellipse2D.Double(start.getX() - 1, start.getY() - 1, 3, 3));
            RenderEngine.drawMapText(g, "#" + mapObject.getId() + "(" + mapObject.getName() + ")", start.getX(), start.getY() - 5);
          }

          // render bounding boxes
          final IMapObject focusedMapObject = this.getFocusedMapObject();
          if (focusedMapObject == null || mapObject.getId() != focusedMapObject.getId()) {
            g.setColor(COLOR_BOUNDING_BOX_FILL);

            // don't fill rect for lightsource because it is important to judge
            // the color
            if (type != MapObjectType.LIGHTSOURCE) {
              if (type == MapObjectType.TRIGGER) {
                g.setColor(COLOR_NOCOLLISION_FILL);
              }
              RenderEngine.fillShape(g, mapObject.getBoundingBox());
            }

            if (type == MapObjectType.TRIGGER) {
              g.setColor(COLOR_NOCOLLISION_BORDER);
            }

            RenderEngine.drawShape(g, mapObject.getBoundingBox());
          }

          // render collision boxes
          String coll = mapObject.getCustomProperty(MapObjectProperty.COLLISION);
          final String collisionBoxWidthFactor = mapObject.getCustomProperty(MapObjectProperty.COLLISIONBOXWIDTH);
          final String collisionBoxHeightFactor = mapObject.getCustomProperty(MapObjectProperty.COLLISIONBOXHEIGHT);
          final Align align = Align.get(mapObject.getCustomProperty(MapObjectProperty.COLLISIONALGIN));
          final Valign valign = Valign.get(mapObject.getCustomProperty(MapObjectProperty.COLLISIONVALGIN));

          if (coll != null && collisionBoxWidthFactor != null && collisionBoxHeightFactor != null) {
            boolean collision = Boolean.valueOf(coll);
            final Color collisionColor = collision ? COLOR_COLLISION_FILL : COLOR_NOCOLLISION_FILL;
            final Color collisionShapeColor = collision ? COLOR_COLLISION_BORDER : COLOR_NOCOLLISION_BORDER;

            float collisionBoxWidth = 0, collisionBoxHeight = 0;
            try {
              collisionBoxWidth = Float.parseFloat(collisionBoxWidthFactor);
              collisionBoxHeight = Float.parseFloat(collisionBoxHeightFactor);
            } catch (NumberFormatException | NullPointerException e) {
              e.printStackTrace();
            }
            g.setColor(collisionColor);
            Rectangle2D collisionBox = CollisionEntity.getCollisionBox(mapObject.getLocation(), mapObject.getDimension().getWidth(),
                mapObject.getDimension().getHeight(), collisionBoxWidth, collisionBoxHeight, align, valign);

            RenderEngine.fillShape(g, collisionBox);
            g.setColor(collisionShapeColor);
            RenderEngine.drawShape(g, collisionBox);
          }
          g.setColor(Color.WHITE);
          float textSize = 3 * zooms[this.currentZoomIndex];
          g.setFont(Program.TEXT_FONT.deriveFont(textSize));
          RenderEngine.drawMapText(g, objectName, mapObject.getX() + 1, mapObject.getBoundingBox().getMaxY());
        }
      }
    }

    switch (this.currentEditMode) {
    case EDITMODE_CREATE:
      if (newObject != null) {
        g.setColor(COLOR_NEWOBJECT_FILL);
        RenderEngine.fillShape(g, newObject);
        g.setColor(COLOR_NEWOBJECT_BORDER);
        RenderEngine.drawShape(g, newObject);
        g.setFont(g.getFont().deriveFont(Font.BOLD));
        RenderEngine.drawMapText(g, newObject.getWidth() + "", newObject.getX() + newObject.getWidth() / 2 - 3, newObject.getY() - 5);
        RenderEngine.drawMapText(g, newObject.getHeight() + "", newObject.getX() - 10, newObject.getY() + newObject.getHeight() / 2);
      }
      break;
    case EDITMODE_EDIT:
      // draw selection
      final Point2D start = this.startPoint;
      if (start != null && !Input.keyboard().isPressed(KeyEvent.VK_CONTROL)) {
        final Rectangle2D rect = this.getCurrentMouseSelectionArea();

        g.setColor(new Color(0, 130, 152, 30));
        RenderEngine.fillShape(g, rect);
        if (rect.getWidth() > 0 || rect.getHeight() > 0) {
          g.setColor(new Color(0, 130, 152, 255));
          g.setFont(g.getFont().deriveFont(Font.BOLD));
          RenderEngine.drawMapText(g, rect.getWidth() + "", rect.getX() + rect.getWidth() / 2 - 3, rect.getY() - 5);
          RenderEngine.drawMapText(g, rect.getHeight() + "", rect.getX() - 10, rect.getY() + rect.getHeight() / 2);
        }
        g.setColor(new Color(0, 130, 152, 150));
        RenderEngine.drawShape(g, rect,
            new BasicStroke(2 / Game.getCamera().getRenderScale(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] { 1 }, 0));
      }
      break;
    }

    // render the focus and the transform rects
    final Rectangle2D focus = this.getFocus();
    final IMapObject focusedMapObject = this.getFocusedMapObject();
    if (focus != null && focusedMapObject != null) {
      Stroke stroke = new BasicStroke(2 / Game.getCamera().getRenderScale(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] { 1f }, 0);
      if (MapObjectType.get(focusedMapObject.getType()) != MapObjectType.LIGHTSOURCE) {
        g.setColor(COLOR_FOCUS_FILL);
        RenderEngine.fillShape(g, focus);
      }

      g.setColor(COLOR_FOCUS_BORDER);
      RenderEngine.drawShape(g, focus, stroke);

      // render transform rects
      if (!Input.keyboard().isPressed(KeyEvent.VK_CONTROL)) {
        Stroke transStroke = new BasicStroke(1 / Game.getCamera().getRenderScale());
        for (Rectangle2D trans : this.transformRects.values()) {
          g.setColor(COLOR_TRANSFORM_RECT_FILL);
          RenderEngine.fillShape(g, trans);
          g.setColor(COLOR_FOCUS_BORDER);
          RenderEngine.drawShape(g, trans, transStroke);
        }
      }
    }

    if (focusedMapObject != null) {
      Point2D loc = Game.getCamera()
          .getViewPortLocation(new Point2D.Double(focusedMapObject.getX() + focusedMapObject.getDimension().getWidth() / 2, focusedMapObject.getY()));
      g.setFont(Program.TEXT_FONT.deriveFont(Font.BOLD, 15f));
      g.setColor(COLOR_FOCUS_BORDER);
      String id = "#" + focusedMapObject.getId();
      RenderEngine.drawText(g, id, loc.getX() * Game.getCamera().getRenderScale() - g.getFontMetrics().stringWidth(id) / 2,
          loc.getY() * Game.getCamera().getRenderScale() - (5 * this.currentTransformRectSize));
      if (MapObjectType.get(focusedMapObject.getType()) == MapObjectType.TRIGGER) {
        g.setColor(COLOR_NOCOLLISION_BORDER);
        g.setFont(Program.TEXT_FONT.deriveFont(11f));
        RenderEngine.drawMapText(g, focusedMapObject.getName(), focusedMapObject.getX() + 2, focusedMapObject.getY() + 5);
      }
    }

    // render the grid
    if (this.renderGrid && Game.getCamera().getRenderScale() >= 1) {

      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

      g.setColor(new Color(255, 255, 255, 100));
      Stroke stroke = new BasicStroke(1 / Game.getCamera().getRenderScale(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] { 0.5f }, 0);
      final double viewPortX = Math.max(0, Game.getCamera().getViewPort().getX());
      final double viewPortMaxX = Math.min(Game.getEnvironment().getMap().getSizeInPixels().getWidth(),
          Game.getCamera().getViewPort().getMaxX());

      final double viewPortY = Math.max(0, Game.getCamera().getViewPort().getY());
      final double viewPortMaxY = Math.min(Game.getEnvironment().getMap().getSizeInPixels().getHeight(),
          Game.getCamera().getViewPort().getMaxY());
      final int startX = Math.max(0, (int) (viewPortX / gridSize) * gridSize);
      final int startY = Math.max(0, (int) (viewPortY / gridSize) * gridSize);
      for (int x = startX; x <= viewPortMaxX; x += gridSize) {
        RenderEngine.drawShape(g, new Line2D.Double(x, viewPortY, x, viewPortMaxY), stroke);
      }

      for (int y = startY; y <= viewPortMaxY; y += gridSize) {
        RenderEngine.drawShape(g, new Line2D.Double(viewPortX, y, viewPortMaxX, y), stroke);
      }

      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
    }

    super.render(g);
  }

  public void loadMaps(String projectPath) {
    final List<String> files = FileUtilities.findFiles(new ArrayList<>(), Paths.get(projectPath), "tmx");
    System.out.println(files.size() + " maps found in folder " + projectPath);
    final List<Map> maps = new ArrayList<>();
    for (final String mapFile : files) {
      final IMapLoader tmxLoader = new TmxMapLoader();<commit_msg>Fix performance for the tile gridSize<commit_after>package de.gurkenlabs.utiliti.components;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;

import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;

import de.gurkenlabs.core.Align;
import de.gurkenlabs.core.Valign;
import de.gurkenlabs.litiengine.Game;
import de.gurkenlabs.litiengine.Resources;
import de.gurkenlabs.litiengine.SpriteSheetInfo;
import de.gurkenlabs.litiengine.entities.CollisionEntity;
import de.gurkenlabs.litiengine.entities.DecorMob.MovementBehavior;
import de.gurkenlabs.litiengine.environment.Environment;
import de.gurkenlabs.litiengine.environment.tilemap.IImageLayer;
import de.gurkenlabs.litiengine.environment.tilemap.IMap;
import de.gurkenlabs.litiengine.environment.tilemap.IMapLoader;
import de.gurkenlabs.litiengine.environment.tilemap.IMapObject;
import de.gurkenlabs.litiengine.environment.tilemap.IMapObjectLayer;
import de.gurkenlabs.litiengine.environment.tilemap.ITileset;
import de.gurkenlabs.litiengine.environment.tilemap.MapObjectProperty;
import de.gurkenlabs.litiengine.environment.tilemap.MapObjectType;
import de.gurkenlabs.litiengine.environment.tilemap.MapUtilities;
import de.gurkenlabs.litiengine.environment.tilemap.TmxMapLoader;
import de.gurkenlabs.litiengine.environment.tilemap.xml.Map;
import de.gurkenlabs.litiengine.environment.tilemap.xml.MapObject;
import de.gurkenlabs.litiengine.environment.tilemap.xml.MapObjectLayer;
import de.gurkenlabs.litiengine.graphics.ImageCache;
import de.gurkenlabs.litiengine.graphics.LightSource;
import de.gurkenlabs.litiengine.graphics.RenderEngine;
import de.gurkenlabs.litiengine.graphics.Spritesheet;
import de.gurkenlabs.litiengine.gui.GuiComponent;
import de.gurkenlabs.litiengine.input.Input;
import de.gurkenlabs.util.MathUtilities;
import de.gurkenlabs.util.geom.GeometricUtilities;
import de.gurkenlabs.util.io.FileUtilities;
import de.gurkenlabs.util.io.ImageSerializer;
import de.gurkenlabs.utiliti.EditorScreen;
import de.gurkenlabs.utiliti.Program;
import de.gurkenlabs.utiliti.UndoManager;

public class MapComponent extends EditorComponent {
  public enum TransformType {
    UP,
    DOWN,
    LEFT,
    RIGHT,
    UPLEFT,
    UPRIGHT,
    DOWNLEFT,
    DOWNRIGHT,
    NONE
  }

  private static final String DEFAULT_MAPOBJECTLAYER_NAME = "default";
  private static final int TRANSFORM_RECT_SIZE = 6;
  private static final int BASE_SCROLL_SPEED = 50;
  private double currentTransformRectSize = TRANSFORM_RECT_SIZE;
  private final java.util.Map<TransformType, Rectangle2D> transformRects;
  private TransformType currentTransform;

  public static final int EDITMODE_CREATE = 0;
  public static final int EDITMODE_EDIT = 1;
  public static final int EDITMODE_MOVE = 2;
  private final List<Consumer<Integer>> editModeChangedConsumer;
  private final List<Consumer<IMapObject>> focusChangedConsumer;
  private final List<Consumer<Map>> mapLoadedConsumer;
  private int currentEditMode = EDITMODE_EDIT;
  private static final float[] zooms = new float[] { 0.1f, 0.25f, 0.5f, 1, 1.5f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 10f, 16f, 32f, 50f, 80f, 100f };
  private int currentZoomIndex = 7;

  private final java.util.Map<String, Point2D> cameraFocus;
  private final java.util.Map<String, IMapObject> focusedObjects;

  private final List<Map> maps;

  private float scrollSpeed = BASE_SCROLL_SPEED;

  private Point2D startPoint;
  private Point2D dragPoint;
  private Point2D dragLocationMapObject;
  private boolean isMoving;
  private boolean isTransforming;
  private Dimension dragSizeMapObject;
  private Rectangle2D newObject;
  private IMapObject copiedMapObject;
  private int gridSize;

  public boolean snapToGrid = true;
  public boolean renderGrid = false;
  public boolean renderCollisionBoxes = true;

  private final EditorScreen screen;

  private boolean isLoading;

  public MapComponent(final EditorScreen screen) {
    super(ComponentType.MAP);
    this.editModeChangedConsumer = new CopyOnWriteArrayList<>();
    this.focusChangedConsumer = new CopyOnWriteArrayList<>();
    this.mapLoadedConsumer = new CopyOnWriteArrayList<>();
    this.focusedObjects = new ConcurrentHashMap<>();
    this.maps = new ArrayList<>();
    this.cameraFocus = new ConcurrentHashMap<>();
    this.transformRects = new ConcurrentHashMap<>();
    this.screen = screen;
    Game.getCamera().onZoomChanged(zoom -> {
      this.currentTransformRectSize = TRANSFORM_RECT_SIZE / zoom;
      this.updateTransformControls();
    });
    this.gridSize = Program.USER_PREFERNCES.getGridSize();
  }

  public void onEditModeChanged(Consumer<Integer> cons) {
    this.editModeChangedConsumer.add(cons);
  }

  public void onFocusChanged(Consumer<IMapObject> cons) {
    this.focusChangedConsumer.add(cons);
  }

  public void onMapLoaded(Consumer<Map> cons) {
    this.mapLoadedConsumer.add(cons);
  }

  @Override
  public void render(Graphics2D g) {
    if (Game.getEnvironment() == null) {
      return;
    }

    Color COLOR_BOUNDING_BOX_FILL = new Color(0, 0, 0, 35);
    Color COLOR_NAME_FILL = new Color(0, 0, 0, 60);
    final Color COLOR_FOCUS_FILL = new Color(0, 0, 0, 50);
    final Color COLOR_FOCUS_BORDER = Color.BLACK;
    final Color COLOR_COLLISION_FILL = new Color(255, 0, 0, 15);
    final Color COLOR_NOCOLLISION_FILL = new Color(255, 100, 0, 15);
    final Color COLOR_COLLISION_BORDER = Color.RED;
    final Color COLOR_NOCOLLISION_BORDER = Color.ORANGE;
    final Color COLOR_SPAWNPOINT = Color.GREEN;
    final Color COLOR_LANE = Color.YELLOW;
    final Color COLOR_NEWOBJECT_FILL = new Color(0, 255, 0, 50);
    final Color COLOR_NEWOBJECT_BORDER = Color.GREEN.darker();
    final Color COLOR_TRANSFORM_RECT_FILL = new Color(255, 255, 255, 100);
    final Color COLOR_SHADOW_FILL = new Color(85, 130, 200, 15);
    final Color COLOR_SHADOW_BORDER = new Color(30, 85, 170);
    final BasicStroke shapeStroke = new BasicStroke(1 / Game.getCamera().getRenderScale());

    if (this.renderCollisionBoxes) {
      // render all entities
      for (final IMapObjectLayer layer : Game.getEnvironment().getMap().getMapObjectLayers()) {
        if (layer == null) {
          continue;
        }

        if (!EditorScreen.instance().getMapSelectionPanel().isSelectedMapObjectLayer(layer.getName())) {
          continue;
        }

        if (layer.getColor() != null) {
          COLOR_BOUNDING_BOX_FILL = new Color(layer.getColor().getRed(), layer.getColor().getGreen(), layer.getColor().getBlue(), 15);
        } else {
          COLOR_BOUNDING_BOX_FILL = new Color(0, 0, 0, 35);
        }

        for (final IMapObject mapObject : layer.getMapObjects()) {
          if (mapObject == null) {
            continue;
          }
          String objectName = mapObject.getName();
          if (objectName != null && !objectName.isEmpty()) {
            Rectangle2D nameBackground = new Rectangle2D.Double(mapObject.getX(), mapObject.getBoundingBox().getMaxY() - 3,
                mapObject.getDimension().getWidth(), 3);
            g.setColor(COLOR_NAME_FILL);
            RenderEngine.fillShape(g, nameBackground);
          }

          MapObjectType type = MapObjectType.get(mapObject.getType());

          // render spawn points
          if (type == MapObjectType.SPAWNPOINT) {
            g.setColor(COLOR_SPAWNPOINT);
            RenderEngine.fillShape(g,
                new Rectangle2D.Double(mapObject.getBoundingBox().getCenterX() - 1, mapObject.getBoundingBox().getCenterY() - 1, 2, 2));
            RenderEngine.drawShape(g, mapObject.getBoundingBox(), shapeStroke);
          } else if (type == MapObjectType.COLLISIONBOX) {
            g.setColor(COLOR_COLLISION_FILL);
            RenderEngine.fillShape(g, mapObject.getBoundingBox());
            g.setColor(COLOR_COLLISION_BORDER);
            RenderEngine.drawShape(g, mapObject.getBoundingBox(), shapeStroke);
          } else if (type == MapObjectType.STATICSHADOW) {
            g.setColor(COLOR_SHADOW_FILL);
            RenderEngine.fillShape(g, mapObject.getBoundingBox());
            g.setColor(COLOR_SHADOW_BORDER);
            RenderEngine.drawShape(g, mapObject.getBoundingBox(), shapeStroke);
          } else if (type == MapObjectType.LANE) {
            // render lane

            if (mapObject.getPolyline() == null || mapObject.getPolyline().getPoints().size() == 0) {
              continue;
            }
            // found the path for the rat
            final Path2D path = MapUtilities.convertPolylineToPath(mapObject);
            if (path == null) {
              continue;
            }

            g.setColor(COLOR_LANE);
            RenderEngine.drawShape(g, path, shapeStroke);
            Point2D start = new Point2D.Double(mapObject.getLocation().getX(), mapObject.getLocation().getY());
            RenderEngine.fillShape(g, new Ellipse2D.Double(start.getX() - 1, start.getY() - 1, 3, 3));
            RenderEngine.drawMapText(g, "#" + mapObject.getId() + "(" + mapObject.getName() + ")", start.getX(), start.getY() - 5);
          }

          // render bounding boxes
          final IMapObject focusedMapObject = this.getFocusedMapObject();
          if (focusedMapObject == null || mapObject.getId() != focusedMapObject.getId()) {
            g.setColor(COLOR_BOUNDING_BOX_FILL);

            // don't fill rect for lightsource because it is important to judge
            // the color
            if (type != MapObjectType.LIGHTSOURCE) {
              if (type == MapObjectType.TRIGGER) {
                g.setColor(COLOR_NOCOLLISION_FILL);
              }
              RenderEngine.fillShape(g, mapObject.getBoundingBox());
            }

            if (type == MapObjectType.TRIGGER) {
              g.setColor(COLOR_NOCOLLISION_BORDER);
            }

            RenderEngine.drawShape(g, mapObject.getBoundingBox(), shapeStroke);
          }

          // render collision boxes
          String coll = mapObject.getCustomProperty(MapObjectProperty.COLLISION);
          final String collisionBoxWidthFactor = mapObject.getCustomProperty(MapObjectProperty.COLLISIONBOXWIDTH);
          final String collisionBoxHeightFactor = mapObject.getCustomProperty(MapObjectProperty.COLLISIONBOXHEIGHT);
          final Align align = Align.get(mapObject.getCustomProperty(MapObjectProperty.COLLISIONALGIN));
          final Valign valign = Valign.get(mapObject.getCustomProperty(MapObjectProperty.COLLISIONVALGIN));

          if (coll != null && collisionBoxWidthFactor != null && collisionBoxHeightFactor != null) {
            boolean collision = Boolean.valueOf(coll);
            final Color collisionColor = collision ? COLOR_COLLISION_FILL : COLOR_NOCOLLISION_FILL;
            final Color collisionShapeColor = collision ? COLOR_COLLISION_BORDER : COLOR_NOCOLLISION_BORDER;

            float collisionBoxWidth = 0, collisionBoxHeight = 0;
            try {
              collisionBoxWidth = Float.parseFloat(collisionBoxWidthFactor);
              collisionBoxHeight = Float.parseFloat(collisionBoxHeightFactor);
            } catch (NumberFormatException | NullPointerException e) {
              e.printStackTrace();
            }
            g.setColor(collisionColor);
            Rectangle2D collisionBox = CollisionEntity.getCollisionBox(mapObject.getLocation(), mapObject.getDimension().getWidth(),
                mapObject.getDimension().getHeight(), collisionBoxWidth, collisionBoxHeight, align, valign);

            RenderEngine.fillShape(g, collisionBox);
            g.setColor(collisionShapeColor);
            RenderEngine.drawShape(g, collisionBox, shapeStroke);
          }
          g.setColor(Color.WHITE);
          float textSize = 3 * zooms[this.currentZoomIndex];
          g.setFont(Program.TEXT_FONT.deriveFont(textSize));
          RenderEngine.drawMapText(g, objectName, mapObject.getX() + 1, mapObject.getBoundingBox().getMaxY());
        }
      }
    }

    switch (this.currentEditMode) {
    case EDITMODE_CREATE:
      if (newObject != null) {
        g.setColor(COLOR_NEWOBJECT_FILL);
        RenderEngine.fillShape(g, newObject);
        g.setColor(COLOR_NEWOBJECT_BORDER);
        RenderEngine.drawShape(g, newObject, shapeStroke);
        g.setFont(g.getFont().deriveFont(Font.BOLD));
        RenderEngine.drawMapText(g, newObject.getWidth() + "", newObject.getX() + newObject.getWidth() / 2 - 3, newObject.getY() - 5);
        RenderEngine.drawMapText(g, newObject.getHeight() + "", newObject.getX() - 10, newObject.getY() + newObject.getHeight() / 2);
      }
      break;
    case EDITMODE_EDIT:
      // draw selection
      final Point2D start = this.startPoint;
      if (start != null && !Input.keyboard().isPressed(KeyEvent.VK_CONTROL)) {
        final Rectangle2D rect = this.getCurrentMouseSelectionArea();

        g.setColor(new Color(0, 130, 152, 30));
        RenderEngine.fillShape(g, rect);
        if (rect.getWidth() > 0 || rect.getHeight() > 0) {
          g.setColor(new Color(0, 130, 152, 255));
          g.setFont(g.getFont().deriveFont(Font.BOLD));
          RenderEngine.drawMapText(g, rect.getWidth() + "", rect.getX() + rect.getWidth() / 2 - 3, rect.getY() - 5);
          RenderEngine.drawMapText(g, rect.getHeight() + "", rect.getX() - 10, rect.getY() + rect.getHeight() / 2);
        }
        g.setColor(new Color(0, 130, 152, 150));
        RenderEngine.drawShape(g, rect,
            new BasicStroke(2 / Game.getCamera().getRenderScale(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] { 1 }, 0));
      }
      break;
    }

    // render the focus and the transform rects
    final Rectangle2D focus = this.getFocus();
    final IMapObject focusedMapObject = this.getFocusedMapObject();
    if (focus != null && focusedMapObject != null) {
      Stroke stroke = new BasicStroke(2 / Game.getCamera().getRenderScale(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] { 1f }, 0);
      if (MapObjectType.get(focusedMapObject.getType()) != MapObjectType.LIGHTSOURCE) {
        g.setColor(COLOR_FOCUS_FILL);
        RenderEngine.fillShape(g, focus);
      }

      g.setColor(COLOR_FOCUS_BORDER);
      RenderEngine.drawShape(g, focus, stroke);

      // render transform rects
      if (!Input.keyboard().isPressed(KeyEvent.VK_CONTROL)) {
        Stroke transStroke = new BasicStroke(1 / Game.getCamera().getRenderScale());
        for (Rectangle2D trans : this.transformRects.values()) {
          g.setColor(COLOR_TRANSFORM_RECT_FILL);
          RenderEngine.fillShape(g, trans);
          g.setColor(COLOR_FOCUS_BORDER);
          RenderEngine.drawShape(g, trans, transStroke);
        }
      }
    }

    if (focusedMapObject != null) {
      Point2D loc = Game.getCamera()
          .getViewPortLocation(new Point2D.Double(focusedMapObject.getX() + focusedMapObject.getDimension().getWidth() / 2, focusedMapObject.getY()));
      g.setFont(Program.TEXT_FONT.deriveFont(Font.BOLD, 15f));
      g.setColor(COLOR_FOCUS_BORDER);
      String id = "#" + focusedMapObject.getId();
      RenderEngine.drawText(g, id, loc.getX() * Game.getCamera().getRenderScale() - g.getFontMetrics().stringWidth(id) / 2,
          loc.getY() * Game.getCamera().getRenderScale() - (5 * this.currentTransformRectSize));
      if (MapObjectType.get(focusedMapObject.getType()) == MapObjectType.TRIGGER) {
        g.setColor(COLOR_NOCOLLISION_BORDER);
        g.setFont(Program.TEXT_FONT.deriveFont(11f));
        RenderEngine.drawMapText(g, focusedMapObject.getName(), focusedMapObject.getX() + 2, focusedMapObject.getY() + 5);
      }
    }

    // render the grid
    if (this.renderGrid && Game.getCamera().getRenderScale() >= 1) {

      g.setColor(new Color(255, 255, 255, 100));
      final Stroke stroke = new BasicStroke(1 / Game.getCamera().getRenderScale());
      final double viewPortX = Math.max(0, Game.getCamera().getViewPort().getX());
      final double viewPortMaxX = Math.min(Game.getEnvironment().getMap().getSizeInPixels().getWidth(),
          Game.getCamera().getViewPort().getMaxX());

      final double viewPortY = Math.max(0, Game.getCamera().getViewPort().getY());
      final double viewPortMaxY = Math.min(Game.getEnvironment().getMap().getSizeInPixels().getHeight(),
          Game.getCamera().getViewPort().getMaxY());
      final int startX = Math.max(0, (int) (viewPortX / gridSize) * gridSize);
      final int startY = Math.max(0, (int) (viewPortY / gridSize) * gridSize);
      for (int x = startX; x <= viewPortMaxX; x += gridSize) {
        RenderEngine.drawShape(g, new Line2D.Double(x, viewPortY, x, viewPortMaxY), stroke);
      }

      for (int y = startY; y <= viewPortMaxY; y += gridSize) {
        RenderEngine.drawShape(g, new Line2D.Double(viewPortX, y, viewPortMaxX, y), stroke);
      }
    }

    super.render(g);
  }

  public void loadMaps(String projectPath) {
    final List<String> files = FileUtilities.findFiles(new ArrayList<>(), Paths.get(projectPath), "tmx");
    System.out.println(files.size() + " maps found in folder " + projectPath);
    final List<Map> maps = new ArrayList<>();
    for (final String mapFile : files) {
      final IMapLoader tmxLoader = new TmxMapLoader();
{'fraction_non_alphanumeric': 0.11414515504947212, 'fraction_numerical': 0.013183350837432977, 'mean_word_length': 3.422808947561423, 'pattern_counts': {'":': 0, '<': 55, '<?xml version=': 0, '>': 59, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 6}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2648237', 'n_tokens_mistral': 11730, 'n_tokens_neox': 11248, 'n_words': 2131}