diff --git a/diylc/diylc-library/src/org/diylc/components/electromechanical/SwitchLatchingSymbol.java b/diylc/diylc-library/src/org/diylc/components/electromechanical/SwitchLatchingSymbol.java
new file mode 100644
index 00000000..c44ec464
--- /dev/null
+++ b/diylc/diylc-library/src/org/diylc/components/electromechanical/SwitchLatchingSymbol.java
@@ -0,0 +1,341 @@
+/*
+ *
+ * DIY Layout Creator (DIYLC). Copyright (c) 2009-2018 held jointly by the individual authors.
+ *
+ * This file is part of DIYLC.
+ *
+ * DIYLC is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * DIYLC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with DIYLC. If not, see
+ * .
+ *
+ */
+package org.diylc.components.electromechanical;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import org.diylc.common.ObjectCache;
+import org.diylc.common.Orientation;
+import org.diylc.components.AbstractComponent;
+import org.diylc.core.ComponentState;
+import org.diylc.core.IDIYComponent;
+import org.diylc.core.IDrawingObserver;
+import org.diylc.core.ISwitch;
+import org.diylc.core.Project;
+import org.diylc.core.VisibilityPolicy;
+import org.diylc.core.annotations.ComponentDescriptor;
+import org.diylc.core.annotations.EditableProperty;
+import org.diylc.core.annotations.KeywordPolicy;
+import org.diylc.core.measures.Size;
+import org.diylc.core.measures.SizeUnit;
+import org.diylc.utils.Constants;
+
+@ComponentDescriptor(name = "Switch (Latching)", author = "Branislav Stojkovic",
+ category = "Schematic Symbols", instanceNamePrefix = "SW",
+ description = "Schematic symbol of various types of latching switches",
+ zOrder = IDIYComponent.COMPONENT, keywordPolicy = KeywordPolicy.SHOW_TAG_AND_VALUE,
+ keywordTag = "Schematic")
+public class SwitchLatchingSymbol
+ extends AbstractComponent implements ISwitch {
+
+ private static final long serialVersionUID = 1L;
+
+ public static Color COLOR = Color.black;
+ public static Size SIZE = new Size(0.15d, SizeUnit.in);
+ public static Size THROW_SPACING = new Size(0.1d, SizeUnit.in);
+ public static Size SPACING = new Size(0.3d, SizeUnit.in);
+ public static Size TERMINAL_SIZE = new Size(0.05d, SizeUnit.in);
+ public static int ARC_ANGLE = 24;
+
+ private Orientation orientation = Orientation.DEFAULT;
+ private SwitchConfiguration configuration = SwitchConfiguration._2x2;
+ private PoleCount poleCount = PoleCount.ONE;
+
+ private Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)};
+
+ public SwitchLatchingSymbol() {
+ super();
+ updateControlPoints();
+ }
+
+ @Override
+ public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
+ Project project, IDrawingObserver drawingObserver) {
+
+ double terminalSize = getClosestOdd(TERMINAL_SIZE);
+
+ g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke((float) (terminalSize / 4)));
+
+ for (int i = 0; i < controlPoints.length; i++) {
+
+ Ellipse2D.Double terminal = new Ellipse2D.Double(controlPoints[i].getX() - terminalSize / 2,
+ controlPoints[i].getY() - terminalSize / 2, terminalSize, terminalSize);
+
+ g2d.setColor(Constants.TRANSPARENT_COLOR);
+ g2d.fill(terminal);
+
+ g2d.setColor(
+ componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
+ ? SELECTION_COLOR
+ : COLOR);
+ g2d.draw(terminal);
+ }
+
+ double theta = orientation.toRadians();
+
+ g2d.setColor(
+ componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
+ ? SELECTION_COLOR
+ : COLOR);
+
+ // draw latch
+ int throwCount = configuration.getThrowCount();
+ for (int j = 0; j < poleCount.getCount(); j++) {
+ double offset = terminalSize * 3 / 5;
+
+ Point2D startPoint = new Point2D.Double(controlPoints[(1 + throwCount) * j].getX() + Math.cos(theta) * offset,
+ controlPoints[(1 + throwCount) * j].getY() + Math.sin(theta) * offset);
+
+ double offsetX;
+ double offsetY;
+ if ("SHORT".equalsIgnoreCase(configuration.getInBetween())) {
+ offsetX = terminalSize * 6 / 5;
+ offsetY = 0;
+ } else {
+ offsetX = terminalSize * 2 / 3;
+ offsetY = terminalSize / 2;
+ }
+
+ Point2D endPoint = new Point2D.Double(
+ controlPoints[(1 + throwCount) * j + 1].getX() + Math.cos(theta + Math.PI) * offsetX
+ + Math.cos(theta + Math.PI * 3 / 2) * offsetY,
+ controlPoints[(1 + throwCount) * j + 1].getY() + Math.sin(theta + Math.PI) * offsetX
+ + Math.sin(theta + Math.PI * 3 / 2) * offsetY);
+
+ g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke((float) (terminalSize / 4)));
+
+ Line2D latch =
+ new Line2D.Double(startPoint.getX(), startPoint.getY(), endPoint.getX(), endPoint.getY());
+ g2d.draw(latch);
+
+ if ("SHORT".equalsIgnoreCase(configuration.getInBetween())) {
+ g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke((float) (terminalSize / 2)));
+
+ double d = startPoint.distance(endPoint) + terminalSize / 3;
+ double latchAngle =
+ Math.atan2(endPoint.getY() - startPoint.getY(), endPoint.getX() - startPoint.getX());
+ int startAngle = (int) Math.toDegrees(-latchAngle) - ARC_ANGLE / 2;
+
+ g2d.drawArc((int) (startPoint.getX() - d), (int) (startPoint.getY() - d), (int) (d * 2),
+ (int) (d * 2), startAngle, ARC_ANGLE);
+ }
+ }
+ if (poleCount.getCount() > 1) {
+ g2d.setStroke(ObjectCache.getInstance().fetchStroke(1f, new float[] {4f, 6f}, 0f, BasicStroke.CAP_SQUARE));
+ Line2D line = new Line2D.Double(
+ (controlPoints[0].getX() + controlPoints[1].getX()) / 2,
+ (controlPoints[0].getY() + controlPoints[1].getY()) / 2,
+ (controlPoints[(1 + throwCount) * (poleCount.getCount() - 1)].getX() +
+ controlPoints[(1 + throwCount) * (poleCount.getCount() - 1) + 1].getX()) / 2,
+ (controlPoints[(1 + throwCount) * (poleCount.getCount() - 1)].getY() +
+ controlPoints[(1 + throwCount) * (poleCount.getCount() - 1) + 1].getY()) / 2);
+ g2d.draw(line);
+ }
+ }
+
+ @Override
+ public void drawIcon(Graphics2D g2d, int width, int height) {
+ int margin = 3 * width / 32;
+ g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
+ g2d.setColor(COLOR);
+
+ int diameter = 5;
+ g2d.drawOval(margin, width / 2 - diameter / 2, diameter, diameter);
+ g2d.drawOval(width - margin - diameter, (int) (width / 2 - 1.5 * diameter), diameter, diameter);
+ g2d.drawOval(width - margin - diameter, (int) (width / 2 + 0.5 * diameter), diameter, diameter);
+
+ g2d.drawLine(margin + diameter, width / 2 - diameter / 2, width - margin - diameter - 1,
+ (int) (width / 2 - 1.5 * diameter));
+ }
+
+ @EditableProperty
+ public Orientation getOrientation() {
+ if (orientation == null)
+ orientation = Orientation.DEFAULT;
+ return orientation;
+ }
+
+ public void setOrientation(Orientation orientation) {
+ this.orientation = orientation;
+
+ updateControlPoints();
+ }
+
+ @Override
+ public Point2D getControlPoint(int index) {
+ return controlPoints[index];
+ }
+
+ @Override
+ public int getControlPointCount() {
+ return controlPoints.length;
+ }
+
+ @Override
+ public boolean isControlPointSticky(int index) {
+ return true;
+ }
+
+ @Override
+ public void setControlPoint(Point2D point, int index) {
+ controlPoints[index].setLocation(point);
+ }
+
+ @Override
+ public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
+ return VisibilityPolicy.NEVER;
+ }
+
+ @EditableProperty(name = "Configuration")
+ @Override
+ public SwitchConfiguration getValue() {
+ return configuration;
+ }
+
+ @Override
+ public void setValue(SwitchConfiguration value) {
+ this.configuration = value;
+
+ updateControlPoints();
+ }
+
+ @EditableProperty(name = "Poles")
+ public PoleCount getPoleCount() {
+ return poleCount;
+ }
+
+ public void setPoleCount(PoleCount poleCount) {
+ this.poleCount = poleCount;
+
+ updateControlPoints();
+ }
+
+ protected void updateControlPoints() {
+ Point2D firstPoint = controlPoints[0];
+
+ int throwCount = configuration.getThrowCount();
+ double spacing = SPACING.convertToPixels();
+ double throwSpacing = THROW_SPACING.convertToPixels();
+
+ controlPoints = new Point2D[(1 + throwCount) * poleCount.getCount()];
+
+ double theta = orientation.toRadians();
+
+ for (int j = 0; j < poleCount.getCount(); j++) {
+ Point2D commonPoint = new Point2D.Double(firstPoint.getX() + Math.cos(theta + Math.PI / 2) * j * (throwCount + 1) * throwSpacing,
+ firstPoint.getY() + Math.sin(theta + Math.PI / 2) * j * (throwCount + 1) * throwSpacing);
+ controlPoints[(1 + throwCount) * j] = commonPoint;
+
+ AffineTransform tx = null;
+ if (theta != 0) {
+ tx = AffineTransform.getRotateInstance(theta, commonPoint.getX(), commonPoint.getY());
+ }
+
+ for (int i = 0; i < throwCount; i++) {
+ double x = commonPoint.getX() + spacing;
+ double y = commonPoint.getY() - (throwCount - 1) * throwSpacing / 2 + throwSpacing * i;
+ Point2D point = new Point2D.Double(x, y);
+ if (tx != null) {
+ tx.transform(point, point);
+ }
+ controlPoints[(1 + throwCount) * j + i + 1] = point;
+ }
+ }
+ }
+
+ @Override
+ public boolean canPointMoveFreely(int pointIndex) {
+ return false;
+ }
+
+ @Override
+ public int getPositionCount() {
+ return configuration.getPositionCount();
+ }
+
+ @Override
+ public String getPositionName(int position) {
+ return Integer.toString(position);
+ }
+
+ @Override
+ public boolean arePointsConnected(int index1, int index2, int position) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ public static enum SwitchConfiguration {
+ _1x1, _2x2, _2x3xOFF, _3x3, _3x5xSHORT, _4x4, _5x5, _6x6, _7x7, _8x8, _9x9, _10x10, _11x11, _12x12;
+
+ private int throwCount;
+ private int positionCount;
+ private String inBetween;
+
+ private SwitchConfiguration() {
+ String[] split = this.name().replaceAll("_", "").split("x");
+ this.throwCount = Integer.parseInt(split[0]);
+ this.positionCount = Integer.parseInt(split[1]);
+ if (split.length > 2) {
+ inBetween = split[2];
+ }
+ }
+
+ public int getPositionCount() {
+ return positionCount;
+ }
+
+ public int getThrowCount() {
+ return throwCount;
+ }
+
+ public String getInBetween() {
+ return inBetween;
+ }
+
+ @Override
+ public String toString() {
+ return throwCount + " throws / " + positionCount + " positions"
+ + (inBetween == null ? "" : " / " + inBetween + " even positions");
+ }
+ }
+
+ public static enum PoleCount {
+ ONE(1), TWO(2), THREE(3), FOUR(4), FIVE(5), SIX(6);
+
+ private int count;
+
+ private PoleCount(int count) {
+ this.count = count;
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ @Override
+ public String toString() {
+ return name().substring(0, 1) + name().substring(1).toLowerCase();
+ }
+ }
+}