]> gerrit.simantics Code Review - simantics/district.git/commitdiff
Initial version of the district network Routes view. 54/2554/5
authorTuukka Lehtonen <tuukka.lehtonen@semantum.fi>
Fri, 14 Dec 2018 22:01:14 +0000 (00:01 +0200)
committerTuukka Lehtonen <tuukka.lehtonen@semantum.fi>
Fri, 14 Dec 2018 22:11:26 +0000 (00:11 +0200)
The backend for the view is an OSGi service called RouteService which
defines an interface for managing routes. The backend handles database
writeback in the background. The UI only talks with the RouteService,
not the database.

Doesn't do much yet and nothing in the UI is finished yet. Just storing
initial work.

gitlab #25

Change-Id: Ifc79f7aebe943bdd5edae3a16052bf44ca3d3cbe

57 files changed:
org.simantics.district.network.ontology/META-INF/MANIFEST.MF
org.simantics.district.network.ui/META-INF/MANIFEST.MF
org.simantics.district.network.ui/src/org/simantics/district/network/ui/DistrictDiagramViewer.java
org.simantics.district.network.ui/src/org/simantics/district/network/ui/internal/Activator.java
org.simantics.district.network.ui/src/org/simantics/district/network/ui/participants/DNPointerInteractor.java
org.simantics.district.network.ui/src/org/simantics/district/network/ui/participants/Modes.java [new file with mode: 0644]
org.simantics.district.network.ui/src/org/simantics/district/network/ui/participants/RoutingMode.java [new file with mode: 0644]
org.simantics.district.region.ontology/.gitignore [new file with mode: 0644]
org.simantics.district.region.ontology/graph.tg [deleted file]
org.simantics.district.route.feature/.project [new file with mode: 0644]
org.simantics.district.route.feature/build.properties [new file with mode: 0644]
org.simantics.district.route.feature/feature.xml [new file with mode: 0644]
org.simantics.district.route.feature/pom.xml [new file with mode: 0644]
org.simantics.district.route.ontology/.classpath [new file with mode: 0644]
org.simantics.district.route.ontology/.gitignore [new file with mode: 0644]
org.simantics.district.route.ontology/.project [new file with mode: 0644]
org.simantics.district.route.ontology/META-INF/MANIFEST.MF [new file with mode: 0644]
org.simantics.district.route.ontology/build.properties [new file with mode: 0644]
org.simantics.district.route.ontology/graph/DistrictNetworkRoutes.pgraph [new file with mode: 0644]
org.simantics.district.route.ontology/pom.xml [new file with mode: 0644]
org.simantics.district.route.ontology/src/org/simantics/district/route/ontology/RouteResource.java [new file with mode: 0644]
org.simantics.district.route.ui/.classpath [new file with mode: 0644]
org.simantics.district.route.ui/.project [new file with mode: 0644]
org.simantics.district.route.ui/META-INF/MANIFEST.MF [new file with mode: 0644]
org.simantics.district.route.ui/OSGI-INF/l10n/bundle.properties [new file with mode: 0644]
org.simantics.district.route.ui/build.properties [new file with mode: 0644]
org.simantics.district.route.ui/fragment.e4xmi [new file with mode: 0644]
org.simantics.district.route.ui/plugin.xml [new file with mode: 0644]
org.simantics.district.route.ui/pom.xml [new file with mode: 0644]
org.simantics.district.route.ui/src/org/simantics/district/route/ui/OpenRouteView.java [new file with mode: 0644]
org.simantics.district.route.ui/src/org/simantics/district/route/ui/RouteTree.java [new file with mode: 0644]
org.simantics.district.route.ui/src/org/simantics/district/route/ui/RouteUI.java [new file with mode: 0644]
org.simantics.district.route.ui/src/org/simantics/district/route/ui/RouteUIConstants.java [new file with mode: 0644]
org.simantics.district.route.ui/src/org/simantics/district/route/ui/RouteView.java [new file with mode: 0644]
org.simantics.district.route.ui/src/org/simantics/district/route/ui/handlers/SelectRouteOnDiagram.java [new file with mode: 0644]
org.simantics.district.route.ui/src/org/simantics/district/route/ui/internal/Activator.java [new file with mode: 0644]
org.simantics.district.route/.classpath [new file with mode: 0644]
org.simantics.district.route/.project [new file with mode: 0644]
org.simantics.district.route/META-INF/MANIFEST.MF [new file with mode: 0644]
org.simantics.district.route/build.properties [new file with mode: 0644]
org.simantics.district.route/pom.xml [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/Route.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/RouteEvent.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/RouteJob.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/RouteService.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/RouteServiceListener.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/Router.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/RouterConfiguration.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/RoutingException.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/Waypoint.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/internal/Activator.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/internal/RouteImpl.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/internal/RoutePersistence.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/internal/RoutePersistenceJob.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/internal/RouteServiceImpl.java [new file with mode: 0644]
org.simantics.district.route/src/org/simantics/district/route/internal/WaypointImpl.java [new file with mode: 0644]
pom.xml

index 9653d5749b02fdbc4768d5d062d3391fa689d56c..55e042f121d8cea193f06a2db3d12d9a608a87d3 100644 (file)
@@ -1,6 +1,6 @@
 Manifest-Version: 1.0
 Bundle-ManifestVersion: 2
-Bundle-Name: http://www.simantics.org/DistrictNetwork-1.0
+Bundle-Name: http://www.simantics.org/DistrictNetwork
 Bundle-SymbolicName: org.simantics.district.network.ontology
 Bundle-Version: 1.0.0.qualifier
 Require-Bundle: org.simantics.layer0,
index a2884512377bf9f9e08c12848bd65663847b84d4..2e77a6c4208f0b42da94b497b70181256bcd206e 100644 (file)
@@ -29,7 +29,9 @@ Require-Bundle: org.eclipse.e4.ui.model.workbench;bundle-version="1.1.100.v20150
  org.eclipse.e4.core.commands,
  org.eclipse.e4.core.contexts,
  org.eclipse.jface,
- org.simantics.scl.osgi
+ org.simantics.scl.osgi,
+ org.simantics.district.route.ui;bundle-version="1.0.0",
+ org.simantics.district.route
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: javax.annotation;version="1.0.0";resolution:=optional,
   javax.inject;version="1.0.0"
index c6c510e0a59d3546743ce68c0bd603003f599d91..642d02be63c575d73ea0aaedb2d8e4ab9a93b5e7 100644 (file)
@@ -6,6 +6,7 @@ import org.simantics.db.ReadGraph;
 import org.simantics.db.common.request.UniqueRead;
 import org.simantics.db.exception.DatabaseException;
 import org.simantics.db.procedure.Listener;
+import org.simantics.diagram.ui.DiagramModelHints;
 import org.simantics.district.network.DistrictNetworkUtil;
 import org.simantics.district.network.ui.participants.DNPointerInteractor;
 import org.simantics.district.network.ui.participants.MapRulerPainter;
@@ -56,6 +57,7 @@ public class DistrictDiagramViewer extends DiagramViewer {
         IHintContext h = ctx.getDefaultHintContext();
         h.setHint(PanZoomRotateHandler.KEY_ZOOM_IN_LIMIT, 10000.0);
         h.setHint(PanZoomRotateHandler.KEY_ZOOM_OUT_LIMIT, 0.01);
+        h.setHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE, diagramResource);
     }
 
     @Override
index e0e7b47564f917a7ca416eda8286cd0d896e3f44..9c56d543578213feaaf8b0e4160a804a295045db 100644 (file)
@@ -1,16 +1,20 @@
 package org.simantics.district.network.ui.internal;
 
-import org.osgi.framework.BundleActivator;
+import org.eclipse.ui.plugin.AbstractUIPlugin;
 import org.osgi.framework.BundleContext;
 import org.osgi.util.tracker.ServiceTracker;
 import org.simantics.district.network.ui.breakdown.SubgraphProvider;
+import org.simantics.district.route.RouteService;
 
-public class Activator implements BundleActivator {
+public class Activator extends AbstractUIPlugin {
 
     public static final String PLUGIN_ID = "org.simantics.district.network.ui";
+
     private static Activator instance;
     private static BundleContext context;
+
     private ServiceTracker<SubgraphProvider, SubgraphProvider> subgraphProviderTracker;
+    private ServiceTracker<RouteService, RouteService> routeServiceTracker;
 
     @Override
     public void start(BundleContext context) throws Exception {
@@ -19,11 +23,14 @@ public class Activator implements BundleActivator {
 
         subgraphProviderTracker = new ServiceTracker<>(context, SubgraphProvider.class.getName(), null);
         subgraphProviderTracker.open();
+        routeServiceTracker = new ServiceTracker<>(context, RouteService.class.getName(), null);
+        routeServiceTracker.open();
     }
 
     @Override
     public void stop(BundleContext context) throws Exception {
         subgraphProviderTracker.close();
+        routeServiceTracker.close();
 
         Activator.instance = null;
         Activator.context = null;
@@ -41,4 +48,8 @@ public class Activator implements BundleActivator {
         return subgraphProviderTracker.getServices(new SubgraphProvider[0]);
     }
 
+    public RouteService getRouteService() {
+        return routeServiceTracker.getService();
+    }
+
 }
index a1a8ae5ac5169ff2216549c3b537137f29ab17f7..ac90af7fb08b79b457231d50958d6be06935aa8d 100644 (file)
@@ -14,6 +14,9 @@ import org.simantics.g2d.diagram.participant.pointertool.TerminalUtil.TerminalIn
 import org.simantics.g2d.element.ElementHints;
 import org.simantics.g2d.element.IElement;
 import org.simantics.scenegraph.g2d.IG2DNode;
+import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;
+import org.simantics.scenegraph.g2d.events.KeyEvent.KeyReleasedEvent;
+import org.simantics.scenegraph.g2d.events.KeyEvent;
 import org.simantics.scenegraph.g2d.nodes.SingleElementNode;
 
 public class DNPointerInteractor extends PointerInteractor {
@@ -62,4 +65,13 @@ public class DNPointerInteractor extends PointerInteractor {
             Set<IElement> elementsToDrag) {
         return new DNTranslateMode(startCanvasPos, curCanvasPos, mouseId, elementsToDrag);
     }
+
+    @EventHandler(priority = 1000)
+    public boolean enterroutingMode(KeyEvent ke) {
+        if (ke.character == 't' && ke instanceof KeyReleasedEvent) {
+            getContext().add(new RoutingMode(0));
+        }
+        return false;
+    }
+
 }
diff --git a/org.simantics.district.network.ui/src/org/simantics/district/network/ui/participants/Modes.java b/org.simantics.district.network.ui/src/org/simantics/district/network/ui/participants/Modes.java
new file mode 100644 (file)
index 0000000..9671f5e
--- /dev/null
@@ -0,0 +1,10 @@
+package org.simantics.district.network.ui.participants;
+
+import org.simantics.g2d.canvas.IToolMode;
+import org.simantics.g2d.canvas.impl.ToolMode;
+
+public class Modes {
+
+    public static final IToolMode ROUTE_TOOL = new ToolMode("RouteTool");
+
+}
diff --git a/org.simantics.district.network.ui/src/org/simantics/district/network/ui/participants/RoutingMode.java b/org.simantics.district.network.ui/src/org/simantics/district/network/ui/participants/RoutingMode.java
new file mode 100644 (file)
index 0000000..d274a61
--- /dev/null
@@ -0,0 +1,212 @@
+package org.simantics.district.network.ui.participants;
+
+import java.awt.AlphaComposite;
+import java.awt.Cursor;
+import java.util.Collection;
+
+import org.eclipse.jface.dialogs.IDialogSettings;
+import org.eclipse.jface.dialogs.IInputValidator;
+import org.eclipse.jface.dialogs.InputDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.PlatformUI;
+import org.simantics.db.Resource;
+import org.simantics.diagram.ui.DiagramModelHints;
+import org.simantics.district.network.ui.internal.Activator;
+import org.simantics.district.route.Route;
+import org.simantics.district.route.RouteService;
+import org.simantics.district.route.Waypoint;
+import org.simantics.g2d.canvas.ICanvasContext;
+import org.simantics.g2d.canvas.IMouseCursorContext;
+import org.simantics.g2d.canvas.IMouseCursorHandle;
+import org.simantics.g2d.canvas.impl.DependencyReflection.Dependency;
+import org.simantics.g2d.canvas.impl.HintReflection.HintListener;
+import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
+import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
+import org.simantics.g2d.diagram.handler.PickContext;
+import org.simantics.g2d.diagram.participant.Selection;
+import org.simantics.g2d.diagram.participant.pointertool.AbstractMode;
+import org.simantics.g2d.element.ElementUtils;
+import org.simantics.g2d.element.IElement;
+import org.simantics.g2d.participant.TransformUtil;
+import org.simantics.scenegraph.g2d.G2DParentNode;
+import org.simantics.scenegraph.g2d.events.Event;
+import org.simantics.scenegraph.g2d.events.EventHandlerReflection.EventHandler;
+import org.simantics.scenegraph.g2d.events.KeyEvent.KeyPressedEvent;
+import org.simantics.scenegraph.g2d.events.command.Command;
+import org.simantics.scenegraph.g2d.events.command.CommandEvent;
+import org.simantics.scenegraph.g2d.events.command.Commands;
+import org.simantics.scenegraph.g2d.nodes.SingleElementNode;
+import org.simantics.utils.datastructures.hints.IHintContext.Key;
+import org.simantics.utils.datastructures.hints.IHintObservable;
+import org.simantics.utils.ui.AdaptionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This participant creates routes on district network diagrams using
+ * {@link RouteService}.
+ *
+ * @author Tuukka Lehtonen
+ * @since 6.09
+ */
+public class RoutingMode extends AbstractMode {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(RoutingMode.class);
+
+    @Dependency
+    protected TransformUtil        util;
+
+    @Dependency
+    protected PickContext          pickContext;
+
+    @Dependency
+    protected Selection            selection;
+
+    protected IMouseCursorHandle   cursor;
+
+    private RouteService           routeService;
+
+    private Route                  route;
+
+    /**
+     * The node under which the mutated diagram is ghosted in the scene graph.
+     */
+    protected G2DParentNode        parent;
+
+    /**
+     * This stays null until the translated diagram parts have been initialized
+     * into the scene graph. After that, only the translations of the nodes in
+     * the scene graph are modified.
+     */
+    protected SingleElementNode    node = null;
+
+    public RoutingMode(int mouseId) {
+        super(mouseId);
+    }
+
+    @Override
+    public void addedToContext(ICanvasContext ctx) {
+        super.addedToContext(ctx);
+        IMouseCursorContext mcc = getContext().getMouseCursorContext();
+        cursor = mcc == null ? null : mcc.setCursor(mouseId, new Cursor(Cursor.CROSSHAIR_CURSOR));
+        routeService = Activator.getInstance().getRouteService();
+        if (routeService != null) {
+            Resource diagramResource = getHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE);
+            LOGGER.info("Diagram resource: " + diagramResource);
+            route = routeService.createRoute("Current", diagramResource);
+            routeService.registerRoute(route);
+        }
+    }
+
+    @Override
+    public void removedFromContext(ICanvasContext ctx) {
+        if (cursor != null) {
+            cursor.remove();
+            cursor = null;
+        }
+        // Discard route if it hasn't been persisted before this
+        if (route != null) {
+            routeService.discardRoute(route);
+            route = null;
+            routeService = null;
+        }
+        super.removedFromContext(ctx);
+    }
+
+    @HintListener(Class = Selection.class, Field = "SELECTION0")
+    public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
+        Collection<IElement> elements = AdaptionUtils.adaptToCollection(newValue, IElement.class);
+        for (IElement element : elements) {
+            addRoutePoint(element);
+        }
+    }
+
+    private void addRoutePoint(IElement element) {
+        if (route == null)
+            return;
+
+        Object o = ElementUtils.getObject(element);
+        if (o instanceof Resource) {
+            Waypoint wp = route.createWaypoint(o);
+            route.addWaypoint(wp);
+        }
+    }
+
+    @SGInit
+    public void initSG(G2DParentNode parent) {
+        this.parent = parent;
+        node = parent.addNode("route highlight", SingleElementNode.class);
+        node.setZIndex(1000);
+        node.setVisible(Boolean.TRUE);
+        node.setComposite(AlphaComposite.SrcOver.derive(0.75f));
+    }
+
+    @SGCleanup
+    public void cleanupSG() {
+        if (node != null) {
+            node.remove();
+            node = null;
+        }
+        parent = null;
+    }
+
+    @EventHandler(priority = 30)
+    public boolean handleEvent(Event e) {
+        if (e instanceof KeyPressedEvent) {
+            if (route == null)
+                return false;
+            KeyPressedEvent kpe = (KeyPressedEvent) e;
+            if (kpe.keyCode == java.awt.event.KeyEvent.VK_ENTER) {
+                routeService.persistRoute(route);
+                route = null;
+                dispose();
+            }
+        } else if (e instanceof CommandEvent) {
+            Command cmd = ((CommandEvent) e).command;
+            if (cmd.equals(Commands.CANCEL)) {
+                return dispose();
+            } else if (cmd.equals(Commands.RENAME)) {
+                InputDialog dialog = new InputDialog(
+                        PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
+                        "Rename Route",
+                        "Route name",
+                        route.getName(),
+                        s -> s.trim().length() > 0 ? null : "Name must be non-empty");
+                if (dialog.open() == Window.OK) {
+                    route.setName(dialog.getValue());
+                }
+            }
+        }
+        return false;
+    }
+
+    protected boolean dispose() {
+        setDirty();
+        remove();
+        return false;
+    }
+
+    static class RenameDialog extends InputDialog {
+
+        public RenameDialog(Shell parent, String title, String message, String initialValue, IInputValidator validator) {
+            super(parent, title, message, initialValue, validator);
+        }
+
+        @Override
+        protected IDialogSettings getDialogBoundsSettings() {
+            String sectionName = getClass().getName() + "_dialogBounds"; //$NON-NLS-1$
+            IDialogSettings settings= Activator.getInstance().getDialogSettings();
+            IDialogSettings section= settings.getSection(sectionName);
+            if (section == null)
+                section= settings.addNewSection(sectionName);
+            return section;
+        }
+
+        @Override
+        protected int getDialogBoundsStrategy() {
+            return DIALOG_PERSISTLOCATION;
+        }
+    }
+
+}
diff --git a/org.simantics.district.region.ontology/.gitignore b/org.simantics.district.region.ontology/.gitignore
new file mode 100644 (file)
index 0000000..eb92844
--- /dev/null
@@ -0,0 +1 @@
+/graph.tg
diff --git a/org.simantics.district.region.ontology/graph.tg b/org.simantics.district.region.ontology/graph.tg
deleted file mode 100644 (file)
index 2cc8f4c..0000000
Binary files a/org.simantics.district.region.ontology/graph.tg and /dev/null differ
diff --git a/org.simantics.district.route.feature/.project b/org.simantics.district.route.feature/.project
new file mode 100644 (file)
index 0000000..cc1eb3c
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.simantics.district.route.feature</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.pde.FeatureBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.FeatureNature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.simantics.district.route.feature/build.properties b/org.simantics.district.route.feature/build.properties
new file mode 100644 (file)
index 0000000..64f93a9
--- /dev/null
@@ -0,0 +1 @@
+bin.includes = feature.xml
diff --git a/org.simantics.district.route.feature/feature.xml b/org.simantics.district.route.feature/feature.xml
new file mode 100644 (file)
index 0000000..794f8a4
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feature
+      id="org.simantics.district.route.feature"
+      label="District Network Routing"
+      version="1.0.0.qualifier"
+      provider-name="Semantum Oy">
+
+   <description url="http://www.example.com/description">
+      [Enter Feature Description here.]
+   </description>
+
+   <copyright url="http://www.example.com/copyright">
+      [Enter Copyright Description here.]
+   </copyright>
+
+   <license url="http://www.example.com/license">
+      [Enter License Description here.]
+   </license>
+
+   <plugin
+         id="org.simantics.district.route.ui"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.simantics.district.route"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+   <plugin
+         id="org.simantics.district.route.ontology"
+         download-size="0"
+         install-size="0"
+         version="0.0.0"
+         unpack="false"/>
+
+</feature>
diff --git a/org.simantics.district.route.feature/pom.xml b/org.simantics.district.route.feature/pom.xml
new file mode 100644 (file)
index 0000000..25d60a1
--- /dev/null
@@ -0,0 +1,15 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+
+       <parent>
+               <groupId>org.simantics.district</groupId>
+               <artifactId>org.simantics.district.root</artifactId>
+               <version>1.0.0-SNAPSHOT</version>
+       </parent>
+
+       <artifactId>org.simantics.district.route.feature</artifactId>
+       <packaging>eclipse-feature</packaging>
+       <version>1.0.0-SNAPSHOT</version>
+
+</project>
diff --git a/org.simantics.district.route.ontology/.classpath b/org.simantics.district.route.ontology/.classpath
new file mode 100644 (file)
index 0000000..eca7bdb
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.simantics.district.route.ontology/.gitignore b/org.simantics.district.route.ontology/.gitignore
new file mode 100644 (file)
index 0000000..eb92844
--- /dev/null
@@ -0,0 +1 @@
+/graph.tg
diff --git a/org.simantics.district.route.ontology/.project b/org.simantics.district.route.ontology/.project
new file mode 100644 (file)
index 0000000..5918c31
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.simantics.district.route.ontology</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.simantics.graph.builder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+               <nature>org.simantics.graph.nature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.simantics.district.route.ontology/META-INF/MANIFEST.MF b/org.simantics.district.route.ontology/META-INF/MANIFEST.MF
new file mode 100644 (file)
index 0000000..c9623fa
--- /dev/null
@@ -0,0 +1,11 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: http://www.simantics.org/DistrictNetworkRoutes
+Bundle-SymbolicName: org.simantics.district.route.ontology
+Bundle-Version: 1.0.0.qualifier
+Bundle-Vendor: Semantum Oy
+Require-Bundle: org.simantics.layer0,
+ org.simantics.district.network.ontology;bundle-version="1.0.0"
+Automatic-Module-Name: fi.vtt.apros.district.route.ontology
+Bundle-RequiredExecutionEnvironment: JavaSE-1.8
+Export-Package: org.simantics.district.route.ontology
diff --git a/org.simantics.district.route.ontology/build.properties b/org.simantics.district.route.ontology/build.properties
new file mode 100644 (file)
index 0000000..e85b630
--- /dev/null
@@ -0,0 +1,5 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .,\
+               graph.tg
diff --git a/org.simantics.district.route.ontology/graph/DistrictNetworkRoutes.pgraph b/org.simantics.district.route.ontology/graph/DistrictNetworkRoutes.pgraph
new file mode 100644 (file)
index 0000000..3d8a95b
--- /dev/null
@@ -0,0 +1,10 @@
+L0 = <http://www.simantics.org/Layer0-1.1>
+DN = <http://www.simantics.org/DistrictNetwork-1.0>
+DIA = <http://www.simantics.org/Diagram-2.2>
+
+DNR = <http://www.simantics.org/DistrictNetworkRoutes-1.0> : L0.Ontology
+    @L0.new
+    L0.HasResourceClass "org.simantics.district.route.ontology.RouteResource"
+
+DNR.RouteFolder <T L0.Entity
+DNR.Route <T L0.List
diff --git a/org.simantics.district.route.ontology/pom.xml b/org.simantics.district.route.ontology/pom.xml
new file mode 100644 (file)
index 0000000..3deb286
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project
+       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+       xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+       <modelVersion>4.0.0</modelVersion>
+
+       <parent>
+               <groupId>org.simantics.district</groupId>
+               <artifactId>org.simantics.district.root</artifactId>
+               <version>1.0.0-SNAPSHOT</version>
+       </parent>
+
+       <artifactId>org.simantics.district.route.ontology</artifactId>
+       <packaging>eclipse-plugin</packaging>
+       <version>1.0.0-SNAPSHOT</version>
+
+</project>
diff --git a/org.simantics.district.route.ontology/src/org/simantics/district/route/ontology/RouteResource.java b/org.simantics.district.route.ontology/src/org/simantics/district/route/ontology/RouteResource.java
new file mode 100644 (file)
index 0000000..def3125
--- /dev/null
@@ -0,0 +1,61 @@
+package org.simantics.district.route.ontology;
+
+import org.simantics.db.RequestProcessor;
+import org.simantics.db.Resource;
+import org.simantics.db.ReadGraph;
+import org.simantics.db.request.Read;
+import org.simantics.db.Session;
+import org.simantics.db.exception.DatabaseException;
+import org.simantics.db.service.QueryControl;
+
+public class RouteResource {
+    
+    public final Resource Route;
+    public final Resource RouteFolder;
+        
+    public static class URIs {
+        public static final String Route = "http://www.simantics.org/DistrictNetworkRoutes-1.0/Route";
+        public static final String RouteFolder = "http://www.simantics.org/DistrictNetworkRoutes-1.0/RouteFolder";
+    }
+    
+    public static Resource getResourceOrNull(ReadGraph graph, String uri) {
+        try {
+            return graph.getResource(uri);
+        } catch(DatabaseException e) {
+            System.err.println(e.getMessage());
+            return null;
+        }
+    }
+    
+    public RouteResource(ReadGraph graph) {
+        Route = getResourceOrNull(graph, URIs.Route);
+        RouteFolder = getResourceOrNull(graph, URIs.RouteFolder);
+    }
+    
+    public static RouteResource getInstance(ReadGraph graph) {
+        Session session = graph.getSession();
+        RouteResource ret = session.peekService(RouteResource.class);
+        if(ret == null) {
+            QueryControl qc = graph.getService(QueryControl.class);
+            ret = new RouteResource(qc.getIndependentGraph(graph));
+            session.registerService(RouteResource.class, ret);
+        }
+        return ret;
+    }
+    
+    public static RouteResource getInstance(RequestProcessor session) throws DatabaseException {
+        RouteResource ret = session.peekService(RouteResource.class);
+        if(ret == null) {
+            ret = session.syncRequest(new Read<RouteResource>() {
+                public RouteResource perform(ReadGraph graph) throws DatabaseException {
+                    QueryControl qc = graph.getService(QueryControl.class);
+                    return new RouteResource(qc.getIndependentGraph(graph));
+                }
+            });
+            session.registerService(RouteResource.class, ret);
+        }
+        return ret;
+    }
+    
+}
+
diff --git a/org.simantics.district.route.ui/.classpath b/org.simantics.district.route.ui/.classpath
new file mode 100644 (file)
index 0000000..eca7bdb
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.simantics.district.route.ui/.project b/org.simantics.district.route.ui/.project
new file mode 100644 (file)
index 0000000..5ea8ac5
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.simantics.district.route.ui</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.simantics.district.route.ui/META-INF/MANIFEST.MF b/org.simantics.district.route.ui/META-INF/MANIFEST.MF
new file mode 100644 (file)
index 0000000..299b241
--- /dev/null
@@ -0,0 +1,23 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: District Network Modelling Route UI
+Bundle-SymbolicName: org.simantics.district.route.ui;singleton:=true
+Bundle-Version: 1.0.0.qualifier
+Bundle-Activator: org.simantics.district.route.ui.internal.Activator
+Bundle-Vendor: Semantum Oy
+Require-Bundle: org.eclipse.e4.ui.di,
+ org.eclipse.e4.ui.model.workbench,
+ org.eclipse.e4.core.di.annotations,
+ org.eclipse.e4.core.services,
+ org.simantics.ui,
+ org.simantics.district.route,
+ org.slf4j.api,
+ org.eclipse.e4.ui.workbench,
+ org.eclipse.e4.core.di,
+ org.eclipse.e4.ui.services
+Bundle-RequiredExecutionEnvironment: JavaSE-1.8
+Automatic-Module-Name: fi.vtt.apros.district.route.ui
+Import-Package: javax.annotation;version="1.0.0";resolution:=optional,
+  javax.inject;version="1.0.0",
+  org.osgi.service.event;version="1.3.1"
+Bundle-ActivationPolicy: lazy
diff --git a/org.simantics.district.route.ui/OSGI-INF/l10n/bundle.properties b/org.simantics.district.route.ui/OSGI-INF/l10n/bundle.properties
new file mode 100644 (file)
index 0000000..6bc47b4
--- /dev/null
@@ -0,0 +1,4 @@
+#Properties file for fi.apros.visualization.table
+view.name = Routes
+command.commandname.1 = Open Routes View
+command.commandname.2 = Select Route on Diagram
diff --git a/org.simantics.district.route.ui/build.properties b/org.simantics.district.route.ui/build.properties
new file mode 100644 (file)
index 0000000..e29652c
--- /dev/null
@@ -0,0 +1,7 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .,\
+               plugin.xml,\
+               fragment.e4xmi,\
+               OSGI-INF/
diff --git a/org.simantics.district.route.ui/fragment.e4xmi b/org.simantics.district.route.ui/fragment.e4xmi
new file mode 100644 (file)
index 0000000..d737ba6
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="ASCII"?>
+<fragment:ModelFragments xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:commands="http://www.eclipse.org/ui/2010/UIModel/application/commands" xmlns:fragment="http://www.eclipse.org/ui/2010/UIModel/fragment" xmlns:menu="http://www.eclipse.org/ui/2010/UIModel/application/ui/menu" xmi:id="_BxaXACerEeWxCPrV0pAZQQ">
+  <fragments xsi:type="fragment:StringModelFragment" xmi:id="_n2jsIMq-EeeUz6Cs9kKeKg" featurename="categories" parentElementId="xpath:/"/>
+  <fragments xsi:type="fragment:StringModelFragment" xmi:id="_4fyr4MqVEeeUz6Cs9kKeKg" featurename="commands" parentElementId="xpath:/">
+    <elements xsi:type="commands:Command" xmi:id="_x1nG0H4WEei74IuRct85qQ" elementId="org.simantics.district.route.ui.commands.openRouteView" commandName="%command.commandname.1"/>
+    <elements xsi:type="commands:Command" xmi:id="_k63rkP6zEeiBo8tg-6EPYA" elementId="org.simantics.district.route.ui.command.selectRouteOnDiagram" commandName="%command.commandname.2"/>
+  </fragments>
+  <fragments xsi:type="fragment:StringModelFragment" xmi:id="_1Ds_gMqVEeeUz6Cs9kKeKg" featurename="handlers" parentElementId="xpath:/">
+    <elements xsi:type="commands:Handler" xmi:id="_1G5RYH4WEei74IuRct85qQ" elementId="org.simantics.district.route.ui.handlers.openRouteView" contributionURI="bundleclass://org.simantics.district.route.ui/org.simantics.district.route.ui.OpenRouteView" command="_x1nG0H4WEei74IuRct85qQ"/>
+    <elements xsi:type="commands:Handler" xmi:id="_O9tbQP60EeiBo8tg-6EPYA" elementId="org.simantics.district.route.ui.handlers.selectRouteOnDiagram" contributionURI="bundleclass://org.simantics.district.route.ui/org.simantics.district.route.ui.handlers.SelectRouteOnDiagram" command="_k63rkP6zEeiBo8tg-6EPYA"/>
+  </fragments>
+  <fragments xsi:type="fragment:StringModelFragment" xmi:id="_Fso08MrIEeeUz6Cs9kKeKg" featurename="toolBarContributions" parentElementId="xpath:/"/>
+  <fragments xsi:type="fragment:StringModelFragment" xmi:id="_WNB8EH4oEei74IuRct85qQ" featurename="trimContributions" parentElementId="xpath:/">
+    <elements xsi:type="menu:TrimContribution" xmi:id="_ZW7FMH4oEei74IuRct85qQ" elementId="org.simantics.district.route.ui.trimcontribution.0" parentId="org.eclipse.ui.main.toolbar">
+      <children xsi:type="menu:ToolBar" xmi:id="_tRL0MH4oEei74IuRct85qQ" elementId="org.simantics.district.route.ui.toolbar.0">
+        <children xsi:type="menu:HandledToolItem" xmi:id="_uWPXwH4oEei74IuRct85qQ" elementId="org.simantics.district.route.ui.handledtoolitem.openRouteView" label="" iconURI="platform:/plugin/com.famfamfam.silk/icons/chart_line.png" command="_x1nG0H4WEei74IuRct85qQ"/>
+      </children>
+    </elements>
+  </fragments>
+  <fragments xsi:type="fragment:StringModelFragment" xmi:id="_YrKV8H4pEei74IuRct85qQ" featurename="menuContributions" parentElementId="xpath:/">
+    <elements xsi:type="menu:MenuContribution" xmi:id="_azWxgH4pEei74IuRct85qQ" elementId="org.simantics.district.route.ui.menucontribution.0" parentId="popup:#DistrictDiagramPopup">
+      <children xsi:type="menu:HandledMenuItem" xmi:id="_laBOMH4pEei74IuRct85qQ" elementId="org.simantics.district.route.ui.handledmenuitem.openRouteView" label="" command="_x1nG0H4WEei74IuRct85qQ"/>
+    </elements>
+    <elements xsi:type="menu:MenuContribution" xmi:id="_Y4vWcP6zEeiBo8tg-6EPYA" elementId="org.simantics.district.route.ui.menucontribution.1" parentId="org.simantics.district.route.ui.contextMenu">
+      <children xsi:type="menu:HandledMenuItem" xmi:id="_fH1V8P6zEeiBo8tg-6EPYA" elementId="org.simantics.district.route.ui.handledmenuitem.selectRouteOnDiagram" command="_k63rkP6zEeiBo8tg-6EPYA"/>
+    </elements>
+  </fragments>
+</fragment:ModelFragments>
diff --git a/org.simantics.district.route.ui/plugin.xml b/org.simantics.district.route.ui/plugin.xml
new file mode 100644 (file)
index 0000000..1d942a2
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?eclipse version="3.4"?>
+<plugin>
+   <extension
+         id="org.simantics.district.route.ui.fragment"
+         point="org.eclipse.e4.workbench.model">
+      <fragment
+            apply="initial"
+            uri="fragment.e4xmi">
+      </fragment>
+   </extension>
+   <extension
+         point="org.eclipse.ui.views">
+      <e4view
+            class="org.simantics.district.route.ui.RouteView"
+            icon="platform:/plugin/com.famfamfam.silk/icons/chart_line.png"
+            id="org.simantics.district.route.ui.routeView"
+            name="%view.name"
+            restorable="true"
+            allowMultiple="true">
+      </e4view>
+   </extension>
+</plugin>
diff --git a/org.simantics.district.route.ui/pom.xml b/org.simantics.district.route.ui/pom.xml
new file mode 100644 (file)
index 0000000..fab4b8f
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project
+       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+       xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+       <modelVersion>4.0.0</modelVersion>
+
+       <parent>
+               <groupId>org.simantics.district</groupId>
+               <artifactId>org.simantics.district.root</artifactId>
+               <version>1.0.0-SNAPSHOT</version>
+       </parent>
+
+       <artifactId>org.simantics.district.route.ui</artifactId>
+       <packaging>eclipse-plugin</packaging>
+       <version>1.0.0-SNAPSHOT</version>
+
+</project>
diff --git a/org.simantics.district.route.ui/src/org/simantics/district/route/ui/OpenRouteView.java b/org.simantics.district.route.ui/src/org/simantics/district/route/ui/OpenRouteView.java
new file mode 100644 (file)
index 0000000..e348eb5
--- /dev/null
@@ -0,0 +1,19 @@
+package org.simantics.district.route.ui;
+
+import org.eclipse.e4.core.di.annotations.CanExecute;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+
+public class OpenRouteView {
+
+    @CanExecute
+    public boolean canExecute(ESelectionService selectionService) {
+        return true;
+    }
+
+    @Execute
+    public void openPropertyTable(ESelectionService selectionService) {
+        RouteUI.openRouteView();
+    }
+
+}
diff --git a/org.simantics.district.route.ui/src/org/simantics/district/route/ui/RouteTree.java b/org.simantics.district.route.ui/src/org/simantics/district/route/ui/RouteTree.java
new file mode 100644 (file)
index 0000000..c275b5b
--- /dev/null
@@ -0,0 +1,221 @@
+package org.simantics.district.route.ui;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.layout.GridLayoutFactory;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerCell;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.simantics.db.Resource;
+import org.simantics.district.route.Route;
+import org.simantics.district.route.RouteEvent;
+import org.simantics.district.route.RouteJob;
+import org.simantics.district.route.RouteService;
+import org.simantics.district.route.RouteServiceListener;
+import org.simantics.district.route.Waypoint;
+import org.simantics.district.route.ui.internal.Activator;
+import org.simantics.utils.ui.SWTUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Tuukka Lehtonen
+ * @since 6.09
+ */
+public class RouteTree extends Composite {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(RouteTree.class);
+
+    ESelectionService selectionService;
+
+    private final LocalResourceManager resourceManager;
+
+    private TreeViewer tree;
+
+    public RouteTree(Composite parent, int style, ESelectionService selectionService) {
+        super(parent, style);
+        this.selectionService = selectionService;
+        this.resourceManager = new LocalResourceManager(JFaceResources.getResources(), parent);
+        defaultInitializeUI();
+        trackRouteInput();
+    }
+
+    private RouteServiceListener routeServiceListener = e -> {
+        System.out.println("Route event: " + e);
+        switch (e.type) {
+            case RouteEvent.TYPE_ROUTE_DISCARDED:
+                runUi(() -> tree.refresh());
+                break;
+
+            case RouteEvent.TYPE_ROUTE_REGISTERED:
+                runUi(() -> {
+                    tree.refresh();
+                    tree.setExpandedState(e.obj, true);
+                });
+                break;
+
+            case RouteEvent.TYPE_ROUTE_MODIFIED:
+                runUi(() -> {
+                    tree.refresh(e.obj);
+                    tree.setExpandedState(e.obj, true);
+                });
+                break;
+
+            case RouteEvent.TYPE_ROUTE_RENAMED:
+                runUi(() -> tree.refresh(e.obj));
+                break;
+
+            case RouteEvent.TYPE_ROUTE_SOURCE_CHANGED:
+                runUi(() -> tree.setInput(e.service.listRoutes()));
+                break;
+        }
+    };
+
+    private void trackRouteInput() {
+        RouteService rs = Activator.getDefault().getRouteService();
+        if (rs != null) {
+            rs.addListener(routeServiceListener);
+            addDisposeListener(e -> rs.removeListener(routeServiceListener));
+            setInput(rs.listRoutes());
+        }
+    }
+
+    private void defaultInitializeUI() {
+        GridDataFactory.fillDefaults().grab(true, true).applyTo(this);
+        GridLayoutFactory.fillDefaults().numColumns(1).applyTo(this);
+
+        createTree(this);
+    }
+
+    private void createTree(Composite parent) {
+        tree = new TreeViewer(parent, SWT.SINGLE);
+        tree.setUseHashlookup(true);
+        GridDataFactory.fillDefaults().grab(true, true).applyTo(tree.getControl());
+        tree.setContentProvider(new ContentProvider());
+        tree.setLabelProvider(new LabelProvider());
+        tree.addSelectionChangedListener(this::treeSelectionChanged);
+        //tree.addPostSelectionChangedListener(this::treeSelectionChanged);
+        tree.addDoubleClickListener(this::itemDoubleClicked);
+        // TODO: add route renaming (F2), DnD reordering support
+    }
+
+    private void itemDoubleClicked(DoubleClickEvent e) {
+        // TODO: default action, i.e. highlight route?
+        LOGGER.info("double click {}", e);
+    }
+
+    private void treeSelectionChanged(SelectionChangedEvent e) {
+        selectionService.setSelection(e.getSelection());
+        /*
+        IStructuredSelection ss = (IStructuredSelection) e.getSelection();
+        Object[] arr = ss.toArray();
+        if (arr.length == 1) {
+            Object o = arr[0];
+            if (o instanceof Route) {
+                CompletableFuture<List<Resource>> callback = new CompletableFuture<>();
+                callback.thenAccept(dnElements -> {
+                    runUi(() -> selectionService.setSelection(new StructuredSelection(dnElements)));
+                });
+                new RouteJob((Route) o, callback).schedule();
+            } else if (o instanceof Waypoint) {
+                selectionService.setSelection(new StructuredSelection(((Waypoint) o).<Object>getObject()));
+            }
+        }
+        */
+    }
+
+    protected void setInput(List<Route> result) {
+        runUi(() -> {
+            TreePath[] paths = tree.getExpandedTreePaths();
+            tree.setInput(result);
+            tree.setExpandedTreePaths(paths);
+        });
+    }
+
+    protected void runUi(Runnable r) {
+        SWTUtils.asyncExec(getDisplay(), () -> {
+            if (!tree.getControl().isDisposed())
+                r.run();
+        });
+    }
+
+    private static class ContentProvider implements ITreeContentProvider {
+
+        private static final Object[] NONE = {};
+        @SuppressWarnings("unused")
+        private List<?> input;
+
+        @Override
+        public void dispose() {
+            input = null;
+        }
+
+        @Override
+        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+            input = newInput instanceof List ? (List<?>) newInput : null;
+        }
+
+        @Override
+        public Object[] getElements(Object inputElement) {
+            if (inputElement instanceof List)
+                return ((List<?>) inputElement).toArray();
+            return NONE;
+        }
+
+        @Override
+        public Object[] getChildren(Object parentElement) {
+            if (parentElement instanceof Route) {
+                return ((Route) parentElement).waypoints().toArray();
+            }
+            return NONE;
+        }
+
+        @Override
+        public Object getParent(Object element) {
+            return null;
+        }
+
+        @Override
+        public boolean hasChildren(Object element) {
+            if (element instanceof Route) {
+                return !((Route) element).waypoints().isEmpty();
+            }
+            return false;
+        }
+
+    }
+
+    private static class LabelProvider extends ColumnLabelProvider {
+        @Override
+        public String getText(Object element) {
+            if (element instanceof Route) {
+                Route r = (Route) element;
+                return NLS.bind("{0} ({1} waypoints)", r.getName(), r.count());
+            } else if (element instanceof Waypoint) {
+            }
+            return element.toString();
+        }
+        @Override
+        public void update(ViewerCell cell) {
+            Object element = cell.getElement();
+            cell.setText(getText(element));
+        }
+    }
+
+}
diff --git a/org.simantics.district.route.ui/src/org/simantics/district/route/ui/RouteUI.java b/org.simantics.district.route.ui/src/org/simantics/district/route/ui/RouteUI.java
new file mode 100644 (file)
index 0000000..48349c0
--- /dev/null
@@ -0,0 +1,17 @@
+package org.simantics.district.route.ui;
+
+import org.simantics.ui.workbench.e4.E4WorkbenchUtils;
+
+/**
+ * Facade
+ * 
+ * @author Tuukka Lehtonen
+ * @since 6.09
+ */
+public class RouteUI {
+
+    public static void openRouteView() {
+        E4WorkbenchUtils.openAndShowPart("org.simantics.district.route.ui.routeView"); //$NON-NLS-1$
+    }
+
+}
diff --git a/org.simantics.district.route.ui/src/org/simantics/district/route/ui/RouteUIConstants.java b/org.simantics.district.route.ui/src/org/simantics/district/route/ui/RouteUIConstants.java
new file mode 100644 (file)
index 0000000..2d211d4
--- /dev/null
@@ -0,0 +1,16 @@
+package org.simantics.district.route.ui;
+
+/**
+ * @author Tuukka Lehtonen
+ */
+public class RouteUIConstants {
+
+       public static final String TOPIC_ROUTE_UI = "TOPIC_ROUTE_UI"; //$NON-NLS-1$
+    public static final String TOPIC_ROUTE_UI_NEW_ROUTE = TOPIC_ROUTE_UI + "/NEW_ROUTE"; //$NON-NLS-1$
+    public static final String TOPIC_ROUTE_UI_ADD_POINT = TOPIC_ROUTE_UI + "/ADD_POINT"; //$NON-NLS-1$
+
+    public static final String EVENT_ROUTE_NAME = "event.route.name"; //$NON-NLS-1$
+    public static final String EVENT_ROUTE = "event.route.instance"; //$NON-NLS-1$
+    public static final String EVENT_ROUTE_POINT = "event.route.point"; //$NON-NLS-1$
+
+}
diff --git a/org.simantics.district.route.ui/src/org/simantics/district/route/ui/RouteView.java b/org.simantics.district.route.ui/src/org/simantics/district/route/ui/RouteView.java
new file mode 100644 (file)
index 0000000..77725b7
--- /dev/null
@@ -0,0 +1,67 @@
+package org.simantics.district.route.ui;
+
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+
+import org.eclipse.e4.core.services.events.IEventBroker;
+import org.eclipse.e4.ui.di.Focus;
+import org.eclipse.e4.ui.model.application.MApplication;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.model.application.ui.menu.MMenuContribution;
+import org.eclipse.e4.ui.model.application.ui.menu.MMenuFactory;
+import org.eclipse.e4.ui.model.application.ui.menu.MPopupMenu;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * @author Tuukka Lehtonen
+ * @since 6.08
+ */
+public class RouteView {
+
+    private static final String POPUP_CONTRIBUTION_ID = "org.simantics.district.route.ui.contextMenu";
+    public static final String POPUP_ELEMENT_ID = "org.simantics.district.route.ui.popup";
+
+    @Inject ESelectionService selectionService;
+    @Inject IEventBroker broker;
+    RouteTree ui;
+
+    /**
+     * See
+     * http://www.vogella.com/tutorials/EclipsePlugin/article.html#adding-eclipse-4-x-parts-to-eclipse-3-x-applications-via-the-code-org-eclipse-ui-views-code-extension-point
+     * 
+     * Cannot contribute context menu via fragment at this point
+     */
+    @Inject
+    public void init(MPart part, MApplication app) {
+        MPopupMenu popupMenu = MMenuFactory.INSTANCE.createPopupMenu();
+        popupMenu.setElementId(POPUP_ELEMENT_ID);
+        List<MMenuContribution> menuContributions = app.getMenuContributions();
+        for (MMenuContribution menuContribution : menuContributions) {
+            if (POPUP_CONTRIBUTION_ID.equals(menuContribution.getParentId())) {
+                // ok, add to menu
+                popupMenu.getChildren().addAll(menuContribution.getChildren());
+            }
+        }
+        part.getMenus().add(popupMenu);
+    }
+
+    @PostConstruct
+    public void postConstruct(Composite parent) {
+        this.ui = new RouteTree(parent, 0, selectionService);
+    }
+
+    @PreDestroy
+    public void dispose() {
+        ui = null;
+    }
+
+    @Focus
+    public void onFocus() {
+        ui.setFocus();
+    }
+
+}
\ No newline at end of file
diff --git a/org.simantics.district.route.ui/src/org/simantics/district/route/ui/handlers/SelectRouteOnDiagram.java b/org.simantics.district.route.ui/src/org/simantics/district/route/ui/handlers/SelectRouteOnDiagram.java
new file mode 100644 (file)
index 0000000..7568ea3
--- /dev/null
@@ -0,0 +1,38 @@
+package org.simantics.district.route.ui.handlers;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import javax.inject.Named;
+
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.swt.widgets.Shell;
+import org.simantics.db.layer0.variable.Variable;
+import org.simantics.district.route.Route;
+import org.simantics.district.route.RouteJob;
+import org.simantics.district.route.RouterConfiguration;
+import org.simantics.utils.ui.ISelectionUtils;
+
+/**
+ * @author Tuukka Lehtonen
+ */
+public class SelectRouteOnDiagram {
+
+    @Execute
+    public void execute(@Named(IServiceConstants.ACTIVE_SHELL) Shell activeShell,
+            @Named(IServiceConstants.ACTIVE_PART) MPart part,
+            @Named(IServiceConstants.ACTIVE_SELECTION) ISelection selection) {
+        // get selected route
+        Route route = ISelectionUtils.filterSingleSelection(selection, Route.class);
+        if (route == null)
+            return;
+
+        CompletableFuture<List<Variable>> result = new CompletableFuture<>();
+        RouterConfiguration config = new RouterConfiguration();
+        new RouteJob(config, route, result).schedule();
+    }
+
+}
\ No newline at end of file
diff --git a/org.simantics.district.route.ui/src/org/simantics/district/route/ui/internal/Activator.java b/org.simantics.district.route.ui/src/org/simantics/district/route/ui/internal/Activator.java
new file mode 100644 (file)
index 0000000..c432508
--- /dev/null
@@ -0,0 +1,60 @@
+package org.simantics.district.route.ui.internal;
+
+import org.eclipse.ui.plugin.AbstractUIPlugin;
+import org.osgi.framework.BundleContext;
+import org.osgi.util.tracker.ServiceTracker;
+import org.simantics.district.route.RouteService;
+
+/**
+ * The activator class controls the plug-in life cycle
+ */
+public class Activator extends AbstractUIPlugin {
+
+    // The plug-in ID
+    public static final String PLUGIN_ID = "org.simantics.district.route.ui"; //$NON-NLS-1$
+
+    // The shared instance
+    private static Activator plugin;
+
+    private ServiceTracker<RouteService, ?> routeServiceTracker;
+
+    /*
+     * (non-Javadoc)
+     * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext)
+     */
+    public void start(BundleContext context) throws Exception {
+        super.start(context);
+        plugin = this;
+
+        // create a tracker and track the service
+        routeServiceTracker = new ServiceTracker<>(context, RouteService.class.getName(), null);
+        routeServiceTracker.open();
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext)
+     */
+    public void stop(BundleContext context) throws Exception {
+        // close the service tracker
+        routeServiceTracker.close();
+        routeServiceTracker = null;
+
+        plugin = null;
+        super.stop(context);
+    }
+
+    /**
+     * Returns the shared instance
+     *
+     * @return the shared instance
+     */
+    public static Activator getDefault() {
+        return plugin;
+    }
+
+    public RouteService getRouteService() {
+        return (RouteService) routeServiceTracker.getService();
+    }
+
+}
diff --git a/org.simantics.district.route/.classpath b/org.simantics.district.route/.classpath
new file mode 100644 (file)
index 0000000..eca7bdb
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.simantics.district.route/.project b/org.simantics.district.route/.project
new file mode 100644 (file)
index 0000000..562165d
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.simantics.district.route</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.simantics.district.route/META-INF/MANIFEST.MF b/org.simantics.district.route/META-INF/MANIFEST.MF
new file mode 100644 (file)
index 0000000..48dce69
--- /dev/null
@@ -0,0 +1,18 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: District Network Routing
+Bundle-SymbolicName: org.simantics.district.route
+Bundle-Version: 1.0.0.qualifier
+Bundle-Activator: org.simantics.district.route.internal.Activator
+Bundle-Vendor: Semantum Oy
+Require-Bundle: org.eclipse.core.runtime,
+ org.simantics.db.layer0;bundle-version="1.1.0",
+ org.simantics;bundle-version="1.0.0",
+ org.simantics.district.route.ontology;bundle-version="1.0.0",
+ org.slf4j.api;bundle-version="1.7.25",
+ org.simantics.diagram.ontology,
+ org.simantics.district.network.ontology
+Bundle-RequiredExecutionEnvironment: JavaSE-1.8
+Automatic-Module-Name: fi.vtt.apros.district.route
+Bundle-ActivationPolicy: lazy
+Export-Package: org.simantics.district.route
diff --git a/org.simantics.district.route/build.properties b/org.simantics.district.route/build.properties
new file mode 100644 (file)
index 0000000..34d2e4d
--- /dev/null
@@ -0,0 +1,4 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .
diff --git a/org.simantics.district.route/pom.xml b/org.simantics.district.route/pom.xml
new file mode 100644 (file)
index 0000000..46943e8
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project
+       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+       xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+       <modelVersion>4.0.0</modelVersion>
+
+       <parent>
+               <groupId>org.simantics.district</groupId>
+               <artifactId>org.simantics.district.root</artifactId>
+               <version>1.0.0-SNAPSHOT</version>
+       </parent>
+
+       <artifactId>org.simantics.district.route</artifactId>
+       <packaging>eclipse-plugin</packaging>
+       <version>1.0.0-SNAPSHOT</version>
+
+</project>
diff --git a/org.simantics.district.route/src/org/simantics/district/route/Route.java b/org.simantics.district.route/src/org/simantics/district/route/Route.java
new file mode 100644 (file)
index 0000000..fcd9117
--- /dev/null
@@ -0,0 +1,47 @@
+package org.simantics.district.route;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Tuukka Lehtonen
+ */
+public interface Route {
+
+    default String getName() {
+        return "empty"; //$NON-NLS-1$
+    }
+
+    void setName(String name);
+
+    Waypoint createWaypoint(Object backend);
+
+    default void addWaypoint(Waypoint r) {
+        addWaypoint(count(), r);
+    }
+
+    default void addWaypoint(int index, Waypoint r) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Removes the specified waypoint from the route
+     */
+    default void removeWaypoint(Waypoint r) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * @return amount of points in the route
+     */
+    default int count() {
+        return waypoints().size();
+    }
+
+    default List<Waypoint> waypoints() {
+        return Collections.emptyList();
+    }
+
+    // TODO: reorder route points
+
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/RouteEvent.java b/org.simantics.district.route/src/org/simantics/district/route/RouteEvent.java
new file mode 100644 (file)
index 0000000..f484596
--- /dev/null
@@ -0,0 +1,54 @@
+package org.simantics.district.route;
+
+/**
+ * @author Tuukka Lehtonen
+ * @since 6.09
+ */
+public class RouteEvent {
+
+    public static final int TYPE_ROUTER_REGISTERED = 1;
+    public static final int TYPE_ROUTER_UNREGISTERED = 2;
+
+    public static final int TYPE_ROUTE_CREATED = 3;
+    public static final int TYPE_ROUTE_REGISTERED = 4;
+    public static final int TYPE_ROUTE_DISCARDING = 5;
+    public static final int TYPE_ROUTE_DISCARDED = 6;
+    public static final int TYPE_ROUTE_RENAMED = 7;
+    public static final int TYPE_ROUTE_MODIFIED = 8;
+    public static final int TYPE_ROUTE_PERSISTING = 9;
+    public static final int TYPE_ROUTE_PERSISTED = 10;
+    public static final int TYPE_ROUTE_SOURCE_CHANGED = 11;
+
+    public final RouteService service;
+    public final int type;
+    public final Object obj;
+
+    public RouteEvent(RouteService service, int type, Object obj) {
+        this.service = service;
+        this.type = type;
+        this.obj = obj;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[type=" + typeString(type) + ", obj=" + obj + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+    }
+
+    private static String typeString(int type) {
+        switch (type) {
+            case TYPE_ROUTER_REGISTERED: return "ROUTER REGISTERED"; //$NON-NLS-1$
+            case TYPE_ROUTER_UNREGISTERED: return "ROUTER UNREGISTERED"; //$NON-NLS-1$
+            case TYPE_ROUTE_CREATED: return "ROUTE CREATED"; //$NON-NLS-1$
+            case TYPE_ROUTE_REGISTERED: return "ROUTE REGISTERED"; //$NON-NLS-1$
+            case TYPE_ROUTE_DISCARDING: return "ROUTE DISCARDING"; //$NON-NLS-1$
+            case TYPE_ROUTE_DISCARDED: return "ROUTE DISCARDED"; //$NON-NLS-1$
+            case TYPE_ROUTE_RENAMED: return "ROUTE RENAMED"; //$NON-NLS-1$
+            case TYPE_ROUTE_MODIFIED: return "ROUTE MODIFIED"; //$NON-NLS-1$
+            case TYPE_ROUTE_PERSISTING: return "ROUTE PERSISTING"; //$NON-NLS-1$
+            case TYPE_ROUTE_PERSISTED: return "ROUTE PERSISTED"; //$NON-NLS-1$
+            case TYPE_ROUTE_SOURCE_CHANGED: return "ROUTE SOURCE CHANGED"; //$NON-NLS-1$
+            default: return "" + type; //$NON-NLS-1$
+        }
+    }
+    
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/RouteJob.java b/org.simantics.district.route/src/org/simantics/district/route/RouteJob.java
new file mode 100644 (file)
index 0000000..8441493
--- /dev/null
@@ -0,0 +1,60 @@
+package org.simantics.district.route;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.simantics.ObjectIdentitySchedulingRule;
+import org.simantics.db.Resource;
+import org.simantics.db.layer0.variable.Variable;
+import org.simantics.district.route.internal.Activator;
+import org.simantics.district.route.internal.RoutePersistence;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Tuukka Lehtonen
+ * @since 6.09
+ */
+public class RouteJob extends Job {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(RouteJob.class);
+
+    private RouterConfiguration config;
+    private List<Resource> waypoints;
+    private CompletableFuture<List<Variable>> callback;
+
+    public RouteJob(RouterConfiguration config, Route route, CompletableFuture<List<Variable>> callback) {
+        super("Compute route");
+        Objects.requireNonNull(callback, "Callback must be non-null");
+        setUser(true);
+        setRule(new ObjectIdentitySchedulingRule(route));
+        this.config = config;
+        this.waypoints = RoutePersistence.toResources(route.waypoints());
+        this.callback = callback;
+    }
+
+    @Override
+    protected IStatus run(IProgressMonitor monitor) {
+        List<Router> routers = Activator.getInstance().getRouteService().routers();
+        for (Router router : routers) {
+            try {
+                List<Variable> path = router.findShortestPath(config, waypoints);
+                if (!path.isEmpty()) {
+                    callback.complete(path);
+                    return Status.OK_STATUS;
+                }
+            } catch (RoutingException e) {
+                LOGGER.error("Routing failed for waypoints {} and router {}", waypoints, router, e);
+                callback.completeExceptionally(e);
+            }
+        }
+        LOGGER.info("No router could calculate route between waypoints {}. Available routers: {}", waypoints, routers);
+        return Status.OK_STATUS;
+    }
+
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/RouteService.java b/org.simantics.district.route/src/org/simantics/district/route/RouteService.java
new file mode 100644 (file)
index 0000000..5c7180b
--- /dev/null
@@ -0,0 +1,31 @@
+package org.simantics.district.route;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * @author Tuukka Lehtonen
+ */
+public interface RouteService {
+
+    void addListener(RouteServiceListener l);
+
+    void removeListener(RouteServiceListener l);
+
+    Route createRoute(String name, Object backendModelEntity);
+
+    void registerRoute(Route route);
+
+    CompletableFuture<Route> persistRoute(Route route);
+
+    CompletableFuture<?> discardRoute(Route route);
+
+    List<Route> listRoutes();
+
+    void registerRouter(Router router);
+
+    void unregisterRouter(Router router);
+
+    List<Router> routers();
+
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/RouteServiceListener.java b/org.simantics.district.route/src/org/simantics/district/route/RouteServiceListener.java
new file mode 100644 (file)
index 0000000..d26ae22
--- /dev/null
@@ -0,0 +1,12 @@
+package org.simantics.district.route;
+
+/**
+ * @author Tuukka Lehtonen
+ * @since 6.09
+ */
+@FunctionalInterface
+public interface RouteServiceListener {
+
+    void handleEvent(RouteEvent e);
+
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/Router.java b/org.simantics.district.route/src/org/simantics/district/route/Router.java
new file mode 100644 (file)
index 0000000..1d24232
--- /dev/null
@@ -0,0 +1,32 @@
+package org.simantics.district.route;
+
+import java.util.List;
+
+import org.simantics.db.Resource;
+import org.simantics.db.layer0.variable.Variable;
+
+/**
+ * @author Tuukka Lehtonen
+ */
+public interface Router {
+
+    /**
+     * @return descriptive name of the router
+     */
+    default String name() {
+        return toString();
+    }
+
+    /**
+     * Must be invoked outside of any transaction realm, like a database request or
+     * experiment thread, preferably from a background job thread.
+     * 
+     * @param wayPoints waypoints for the route to find in visiting order. The
+     *            resources must represents district network diagram elements.
+     * @return the piece-wise shortest path between the specified waypoints as a
+     *         fully baked path of district network diagram element resources
+     * @throws RoutingException in case of any problems in routing
+     */
+    List<Variable> findShortestPath(RouterConfiguration config, List<Resource> wayPoints) throws RoutingException;
+
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/RouterConfiguration.java b/org.simantics.district.route/src/org/simantics/district/route/RouterConfiguration.java
new file mode 100644 (file)
index 0000000..c9ecb3e
--- /dev/null
@@ -0,0 +1,12 @@
+package org.simantics.district.route;
+
+import java.util.HashMap;
+
+/**
+ * @author Tuukka Lehtonen
+ */
+public class RouterConfiguration extends HashMap<String, Object> {
+
+    private static final long serialVersionUID = -6852913957185485390L;
+
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/RoutingException.java b/org.simantics.district.route/src/org/simantics/district/route/RoutingException.java
new file mode 100644 (file)
index 0000000..e47365e
--- /dev/null
@@ -0,0 +1,19 @@
+package org.simantics.district.route;
+
+/**
+ * @author Tuukka Lehtonen
+ * @since 6.09
+ */
+public class RoutingException extends Exception {
+
+    private static final long serialVersionUID = -718352796086955155L;
+
+    public RoutingException(String message) {
+        super(message);
+    }
+
+    public RoutingException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/Waypoint.java b/org.simantics.district.route/src/org/simantics/district/route/Waypoint.java
new file mode 100644 (file)
index 0000000..62fd450
--- /dev/null
@@ -0,0 +1,11 @@
+package org.simantics.district.route;
+
+/**
+ * @author Tuukka Lehtonen
+ */
+public interface Waypoint {
+
+    String getLabel();
+    <T> T getObject();
+
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/internal/Activator.java b/org.simantics.district.route/src/org/simantics/district/route/internal/Activator.java
new file mode 100644 (file)
index 0000000..f26d0cd
--- /dev/null
@@ -0,0 +1,95 @@
+package org.simantics.district.route.internal;
+
+import java.util.Hashtable;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.util.tracker.ServiceTracker;
+import org.simantics.district.route.RouteService;
+import org.simantics.district.route.Router;
+
+/**
+ * @author Tuukka Lehtonen
+ * @since 6.09
+ */
+public class Activator implements BundleActivator, ServiceListener {
+
+    private static BundleContext context;
+    private static Activator instance;
+
+    static BundleContext getContext() {
+        return context;
+    }
+
+    public static Activator getInstance() {
+        return instance;
+    }
+
+    private RouteServiceImpl                routeService;
+    private ServiceRegistration<?>          routeServiceRegistration;
+    private ServiceTracker<RouteService, ?> routeServiceTracker;
+    private ServiceTracker<Router, ?>       routerTracker;
+
+    @Override
+    public void start(BundleContext context) throws Exception {
+        Activator.instance = this;
+        Activator.context = context;
+        routeService = new RouteServiceImpl();
+
+        // register the service
+        routeServiceRegistration = context.registerService(RouteService.class.getName(), routeService, new Hashtable<>());
+
+        // create a tracker and track the service
+        routeServiceTracker = new ServiceTracker<>(context, RouteService.class.getName(), null);
+        routeServiceTracker.open();
+
+        // Register initially availble routers
+        routerTracker = new ServiceTracker<>(context, Router.class.getName(), null);
+        routerTracker.open();
+        for (Object router : routerTracker.getServices()) {
+            routeService.registerRouter((Router) router);
+        }
+
+        // have a service listener to implement the whiteboard pattern
+        context.addServiceListener(this, "(" + Constants.OBJECTCLASS + "=" + Router.class.getName() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
+
+        // grab the service
+        routeService = (RouteServiceImpl) routeServiceTracker.getService();
+    }
+
+    @Override
+    public void stop(BundleContext context) throws Exception {
+        // close the service tracker
+        routeServiceTracker.close();
+        routeServiceTracker = null;
+
+        routeServiceRegistration.unregister();
+
+        routeService.close();
+        routeService = null;
+        context = null;
+        instance = null;
+    }
+
+    public void serviceChanged(ServiceEvent ev) {
+        ServiceReference<?> sr = ev.getServiceReference();
+        switch (ev.getType()) {
+            case ServiceEvent.REGISTERED:
+                routeService.registerRouter((Router) context.getService(sr));
+                break;
+            case ServiceEvent.UNREGISTERING:
+                routeService.unregisterRouter((Router) context.getService(sr));
+                break;
+        }
+    }
+
+    public RouteService getRouteService() {
+        return routeService;
+    }
+
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/internal/RouteImpl.java b/org.simantics.district.route/src/org/simantics/district/route/internal/RouteImpl.java
new file mode 100644 (file)
index 0000000..3619b60
--- /dev/null
@@ -0,0 +1,107 @@
+package org.simantics.district.route.internal;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.simantics.db.Resource;
+import org.simantics.district.route.Route;
+import org.simantics.district.route.RouteEvent;
+import org.simantics.district.route.Waypoint;
+
+public class RouteImpl implements Route {
+
+    private String name;
+    private Resource model;
+    private Resource backend;
+
+    private RouteServiceImpl rs;
+
+    private List<Waypoint> waypoints;
+    private List<Waypoint> unmodifiableWaypoints;
+
+    public RouteImpl(String name) {
+        this.name = name;
+        routeService(rs);
+        waypoints(new ArrayList<>());
+    }
+
+    public RouteImpl modelEntity(Resource model) {
+        this.model = model;
+        return this;
+    }
+
+    public Resource modelEntity() {
+        return model;
+    }
+
+    public RouteImpl backend(Resource backend) {
+        this.backend = backend;
+        return this;
+    }
+
+    public Resource backend() {
+        return backend;
+    }
+
+    public RouteImpl waypoints(List<Waypoint> waypoints) {
+        this.waypoints = waypoints;
+        this.unmodifiableWaypoints = Collections.unmodifiableList(waypoints);
+        return this;
+    }
+
+    public RouteImpl routeService(RouteServiceImpl rs) {
+        this.rs = rs;
+        return this;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public void setName(String name) {
+        this.name = name;
+        fireEvent(RouteEvent.TYPE_ROUTE_RENAMED);
+    }
+
+    @Override
+    public Waypoint createWaypoint(Object backend) {
+        if (backend instanceof Resource)
+            return new WaypointImpl((Resource) backend, "Point 1");
+        throw new IllegalArgumentException("only Resource type waypoints supported, got " + backend); //$NON-NLS-1$
+    }
+
+    @Override
+    public void addWaypoint(int index, Waypoint r) {
+        waypoints.add(index, r);
+        fireEvent(RouteEvent.TYPE_ROUTE_MODIFIED);
+    }
+
+    @Override
+    public void removeWaypoint(Waypoint r) {
+        waypoints.remove(r);
+        fireEvent(RouteEvent.TYPE_ROUTE_MODIFIED);
+    }
+
+    @Override
+    public List<Waypoint> waypoints() {
+        return unmodifiableWaypoints;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s@%x [%s, waypoints=%s]",
+                getClass().getName(),
+                System.identityHashCode(this),
+                name,
+                waypoints.toString());
+    }
+
+    private void fireEvent(int type) {
+        if (rs != null)
+            rs.fireEvent(type, this);
+    }
+
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/internal/RoutePersistence.java b/org.simantics.district.route/src/org/simantics/district/route/internal/RoutePersistence.java
new file mode 100644 (file)
index 0000000..c3ba757
--- /dev/null
@@ -0,0 +1,160 @@
+package org.simantics.district.route.internal;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import org.eclipse.osgi.util.NLS;
+import org.simantics.Simantics;
+import org.simantics.databoard.Bindings;
+import org.simantics.databoard.util.ObjectUtils;
+import org.simantics.db.ReadGraph;
+import org.simantics.db.Resource;
+import org.simantics.db.WriteGraph;
+import org.simantics.db.common.request.ObjectsWithType;
+import org.simantics.db.common.request.UniqueRead;
+import org.simantics.db.common.utils.ListUtils;
+import org.simantics.db.common.utils.NameUtils;
+import org.simantics.db.exception.DatabaseException;
+import org.simantics.db.layer0.QueryIndexUtils;
+import org.simantics.db.layer0.request.PossibleActiveModel;
+import org.simantics.db.layer0.util.RemoverUtil;
+import org.simantics.district.network.ontology.DistrictNetworkResource;
+import org.simantics.district.route.Waypoint;
+import org.simantics.district.route.ontology.RouteResource;
+import org.simantics.layer0.Layer0;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Tuukka Lehtonen
+ */
+public class RoutePersistence {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(RoutePersistence.class);
+
+    public static Resource getRouteFolder(ReadGraph graph, Resource model) throws DatabaseException {
+        List<Resource> diagrams = QueryIndexUtils.searchByType(graph, model, RouteResource.getInstance(graph).RouteFolder);
+        if (diagrams.size() > 0)
+            return diagrams.get(0);
+        return null;
+    }
+
+    public static Resource getOrCreateRouteFolder(WriteGraph graph, Resource model) throws DatabaseException {
+        Resource rf = getRouteFolder(graph, model);
+        if (rf == null) {
+            Layer0 L0 = Layer0.getInstance(graph);
+            RouteResource RR = RouteResource.getInstance(graph);
+            rf = graph.newResource();
+            graph.claim(rf, L0.InstanceOf, null, RR.RouteFolder);
+            graph.claimLiteral(rf, L0.HasName, L0.NameOf, L0.String, UUID.randomUUID().toString(), Bindings.STRING);
+            graph.claim(model, L0.ConsistsOf, L0.PartOf, rf);
+        }
+        return rf;
+    }
+
+    public static Resource createRoute(WriteGraph graph, Resource model, String label, List<Resource> waypoints) throws DatabaseException {
+        Resource rf = getOrCreateRouteFolder(graph, model);
+
+        RouteResource RR = RouteResource.getInstance(graph);
+        Layer0 L0 = Layer0.getInstance(graph);
+
+        Resource route = ListUtils.create(graph, RR.Route, waypoints);
+        graph.claim(rf, L0.ConsistsOf, L0.PartOf, route);
+        graph.claimLiteral(route, L0.HasName, UUID.randomUUID().toString(), Bindings.STRING);
+        graph.claimLiteral(route, L0.HasLabel, label, Bindings.STRING);
+
+        LOGGER.info("Persisted route {} with label {} and waypoints {}", route, label, waypoints);
+
+        return route;
+    }
+
+    public static void updateRoute(WriteGraph graph, Resource route, String label, List<Resource> waypoints) throws DatabaseException {
+        RouteResource RR = RouteResource.getInstance(graph);
+        Layer0 L0 = Layer0.getInstance(graph);
+
+        String existingLabel = graph.getPossibleRelatedValue(route, L0.HasLabel, Bindings.STRING);
+        if (ObjectUtils.objectEquals(existingLabel, label)) {
+            graph.claimLiteral(route, L0.HasLabel, label, Bindings.STRING);
+        }
+
+        List<Resource> existingWaypoints = ListUtils.toList(graph, route);
+        if (!existingWaypoints.equals(waypoints)) {
+            // TODO: optimize this
+            ListUtils.removeElements(graph, route, new HashSet<>(existingWaypoints));
+            ListUtils.createExisting(graph, RR.Route, waypoints);
+        }
+
+        LOGGER.info("Updated route {} with label {} and waypoints {}", route, label, waypoints);
+    }
+
+    public static void removeRoute(WriteGraph graph, Resource route) throws DatabaseException {
+        RemoverUtil.remove(graph, route);
+    }
+
+    public static Waypoint toWaypoint(ReadGraph graph, Resource waypoint) throws DatabaseException {
+        DistrictNetworkResource DN = DistrictNetworkResource.getInstance(graph);
+        Set<Resource> types = graph.getTypes(waypoint);
+        String name = NameUtils.getSafeName(graph, waypoint);
+        if (types.contains(DN.Vertex)) {
+            String address = graph.getPossibleRelatedValue(waypoint, DN.Vertex_HasAddress, Bindings.STRING);
+            String label = address != null
+                    ? NLS.bind("Node {0} - {1}", name, address).trim()
+                    : NLS.bind("Node {0}", name, address).trim();
+            return new WaypointImpl(waypoint, label);
+        } else if (types.contains(DN.Edge)) {
+            return new WaypointImpl(waypoint, NLS.bind("Edge {0}", name));
+        }
+        LOGGER.warn("Tried to convert unrecognized resource {} to a route waypoint", waypoint);
+        return null;
+    }
+
+    public static List<Waypoint> toWaypoints(ReadGraph graph, List<Resource> waypoints) throws DatabaseException {
+        List<Waypoint> result = new ArrayList<>();
+        for (Resource wpr : waypoints) {
+            Waypoint wp = toWaypoint(graph, wpr);
+            if (wp != null)
+                result.add(wp);
+        }
+        return result;
+    }
+
+    public static List<RouteImpl> findRoutes(ReadGraph graph, Resource model) throws DatabaseException {
+        Resource rf = getRouteFolder(graph, model);
+        if (rf == null)
+            return Collections.emptyList();
+
+        Layer0 L0 = Layer0.getInstance(graph);
+        RouteResource RR = RouteResource.getInstance(graph);
+
+        List<RouteImpl> routes = new ArrayList<>();
+
+        for (Resource route : graph.syncRequest(new ObjectsWithType(rf, L0.ConsistsOf, RR.Route))) {
+            RouteImpl ri = new RouteImpl(graph.getRelatedValue(route, L0.HasLabel, Bindings.STRING))
+                    .backend(route)
+                    .modelEntity(model)
+                    .waypoints(toWaypoints(graph, ListUtils.toList(graph, route)));
+            routes.add(ri);
+        }
+
+        return routes;
+    }
+
+    public static class ActiveModelRoutesRequest extends UniqueRead<List<RouteImpl>> {
+        @Override
+        public List<RouteImpl> perform(ReadGraph graph) throws DatabaseException {
+            return findRoutes(graph, graph.syncRequest(new PossibleActiveModel(Simantics.getProjectResource())));
+        }
+    }
+
+    public static List<Resource> toResources(List<Waypoint> waypoints) {
+        return waypoints.stream()
+                .<Resource>map(Waypoint::getObject)
+                .collect(Collectors.toList());
+    }
+
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/internal/RoutePersistenceJob.java b/org.simantics.district.route/src/org/simantics/district/route/internal/RoutePersistenceJob.java
new file mode 100644 (file)
index 0000000..037b590
--- /dev/null
@@ -0,0 +1,94 @@
+package org.simantics.district.route.internal;
+
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.simantics.DatabaseJob;
+import org.simantics.Simantics;
+import org.simantics.db.Resource;
+import org.simantics.db.WriteGraph;
+import org.simantics.db.exception.DatabaseException;
+import org.simantics.db.layer0.request.PossibleModel;
+import org.simantics.db.request.Write;
+import org.simantics.district.route.Route;
+import org.simantics.district.route.RouteEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Tuukka Lehtonen
+ * @since 6.09
+ */
+public class RoutePersistenceJob extends DatabaseJob {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(RoutePersistenceJob.class);
+
+    private RouteImpl route;
+    private int eventType;
+    private CompletableFuture<Route> future;
+
+    public RoutePersistenceJob(RouteImpl route, int eventType, CompletableFuture<Route> future) {
+        super("Route Persistence");
+        this.route = route;
+        this.eventType = eventType;
+        this.future = future;
+        setUser(false);
+        setSystem(true);
+    }
+
+    @Override
+    protected IStatus run(IProgressMonitor monitor) {
+        try {
+            Simantics.getSession().syncRequest((Write) graph -> persist(graph));
+            future.complete(route);
+        } catch (DatabaseException e) {
+            LOGGER.error("Failed to persist route {}", route, e);
+            future.completeExceptionally(e);
+        }
+        return Status.OK_STATUS;
+    }
+
+    private void persist(WriteGraph graph) throws DatabaseException {
+        switch (eventType) {
+            case RouteEvent.TYPE_ROUTE_DISCARDING:
+                remove(graph);
+                break;
+
+            case RouteEvent.TYPE_ROUTE_PERSISTING:
+                write(graph);
+                break;
+        }
+    }
+
+    private void write(WriteGraph graph) throws DatabaseException {
+        graph.markUndoPoint();
+        Resource r = route.backend();
+        Resource me = route.modelEntity();
+        if (r == null) {
+            Resource model = graph.sync(new PossibleModel(me));
+            route.backend(
+                    RoutePersistence.createRoute(graph,
+                            model,
+                            route.getName(),
+                            RoutePersistence.toResources(route.waypoints()))
+                    );
+        } else {
+            RoutePersistence.updateRoute(graph,
+                    r,
+                    route.getName(),
+                    RoutePersistence.toResources(route.waypoints())
+            );
+        }
+    }
+
+    private void remove(WriteGraph graph) throws DatabaseException {
+        graph.markUndoPoint();
+        Resource r = route.backend();
+        if (r != null) {
+            RoutePersistence.removeRoute(graph, r);
+        }
+    }
+
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/internal/RouteServiceImpl.java b/org.simantics.district.route/src/org/simantics/district/route/internal/RouteServiceImpl.java
new file mode 100644 (file)
index 0000000..e37a803
--- /dev/null
@@ -0,0 +1,195 @@
+package org.simantics.district.route.internal;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.core.runtime.ListenerList;
+import org.simantics.Simantics;
+import org.simantics.db.ReadGraph;
+import org.simantics.db.Resource;
+import org.simantics.db.Session;
+import org.simantics.db.exception.DatabaseException;
+import org.simantics.db.layer0.request.PossibleActiveModel;
+import org.simantics.db.management.ISessionContext;
+import org.simantics.db.management.ISessionContextChangedListener;
+import org.simantics.db.management.SessionContextChangedEvent;
+import org.simantics.db.procedure.SyncListener;
+import org.simantics.district.route.Route;
+import org.simantics.district.route.RouteEvent;
+import org.simantics.district.route.RouteService;
+import org.simantics.district.route.RouteServiceListener;
+import org.simantics.district.route.Router;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Tuukka Lehtonen
+ */
+public class RouteServiceImpl implements RouteService, ISessionContextChangedListener, Closeable {
+
+    private final Logger LOGGER = LoggerFactory.getLogger(RouteServiceImpl.class); 
+
+    private ListenerList<RouteServiceListener> listeners = new ListenerList<>();
+    private List<Route> routes = new ArrayList<>();
+    private List<Route> unmodifiableRoutes = Collections.unmodifiableList(routes);
+    private List<Router> routers = new ArrayList<>();
+    private List<Router> unmodifiableRouters = Collections.unmodifiableList(routers);
+
+    private class StoreListener implements SyncListener<Resource> {
+        private boolean disposed = false;
+
+        @Override
+        public void execute(ReadGraph graph, Resource activeModel) {
+            try {
+                if (activeModel != null) {
+                    resetRoutes(RoutePersistence.findRoutes(graph, activeModel));
+                } else {
+                    resetRoutes(Collections.emptyList());
+                }
+                fireEvent(RouteEvent.TYPE_ROUTE_SOURCE_CHANGED, this);
+            } catch (DatabaseException e) {
+                LOGGER.error("Failed to read routes from model {}", activeModel, e);
+            }
+        }
+
+        @Override
+        public void exception(ReadGraph graph, Throwable t) {
+            LOGGER.error("Failed to listen to current route service storage", t);
+        }
+
+        public void dispose() {
+            disposed = true;
+        }
+
+        @Override
+        public boolean isDisposed() {
+            return disposed;
+        }
+    }
+
+    private StoreListener storeListener;
+
+    private synchronized void listenToActiveModels(Session s) {
+        StoreListener sl =  storeListener;
+        if (sl != null)
+            sl.dispose();
+        if (s != null) {
+            s.asyncRequest(
+                    new PossibleActiveModel(Simantics.getProjectResource()),
+                    storeListener = new StoreListener());
+        } else {
+            resetRoutes(Collections.emptyList());
+        }
+    }
+
+    void resetRoutes(List<RouteImpl> newRoutes) {
+        routes.clear();
+        newRoutes.forEach(r -> r.routeService(this));
+        routes.addAll(newRoutes);
+    }
+
+    @Override
+    public void sessionContextChanged(SessionContextChangedEvent event) {
+        ISessionContext ctx = event.getNewValue();
+        Session s = ctx != null ? ctx.getSession() : null;
+        listenToActiveModels(s);
+    }
+
+    public RouteServiceImpl() {
+        Session s = Simantics.peekSession();
+        Simantics.getSessionContextProvider().addContextChangedListener(this);
+        if (s != null)
+            listenToActiveModels(s);
+    }
+
+    @Override
+    public void close() {
+        Simantics.getSessionContextProvider().removeContextChangedListener(this);
+    }
+
+    @Override
+    public void addListener(RouteServiceListener l) {
+        listeners.add(l);
+    }
+
+    @Override
+    public void removeListener(RouteServiceListener l) {
+        listeners.remove(l);
+    }
+
+    @Override
+    public Route createRoute(String name, Object backendModelEntity) {
+        Route r = new RouteImpl(name).modelEntity((Resource) backendModelEntity).routeService(this);
+        fireEvent(RouteEvent.TYPE_ROUTE_CREATED, r);
+        return r;
+    }
+
+    @Override
+    public void registerRoute(Route route) {
+        routes.add(route);
+        fireEvent(RouteEvent.TYPE_ROUTE_REGISTERED, route);
+    }
+
+    @Override
+    public CompletableFuture<Route> persistRoute(Route route) {
+        fireEvent(RouteEvent.TYPE_ROUTE_PERSISTING, route);
+        CompletableFuture<Route> future = new CompletableFuture<>();
+        new RoutePersistenceJob((RouteImpl) route, RouteEvent.TYPE_ROUTE_PERSISTING, future).schedule();
+        future.thenAccept(r -> fireEvent(RouteEvent.TYPE_ROUTE_PERSISTED, r));
+        return future;
+    }
+
+    @Override
+    public CompletableFuture<Route> discardRoute(Route route) {
+        fireEvent(RouteEvent.TYPE_ROUTE_DISCARDING, route);
+        CompletableFuture<Route> result = new CompletableFuture<>();
+        RouteImpl ri = (RouteImpl) route;
+        result.thenAccept(r -> {
+            routes.remove(route);
+            fireEvent(RouteEvent.TYPE_ROUTE_DISCARDED, route);
+        });
+        if (ri.backend() != null) {
+            new RoutePersistenceJob(ri, RouteEvent.TYPE_ROUTE_DISCARDING, result).schedule();
+        } else {
+            result.complete(route);
+        }
+        return result;
+    }
+
+    @Override
+    public List<Route> listRoutes() {
+        return unmodifiableRoutes;
+    }
+
+    @Override
+    public void registerRouter(Router router) {
+        routers.add(router);
+        fireEvent(RouteEvent.TYPE_ROUTER_REGISTERED, router);
+    }
+
+    @Override
+    public void unregisterRouter(Router router) {
+        routers.add(router);
+        fireEvent(RouteEvent.TYPE_ROUTER_UNREGISTERED, router);
+    }
+
+    public List<Router> routers() {
+        return unmodifiableRouters;
+    }
+
+    void fireEvent(int type, Object obj) {
+        RouteEvent e = new RouteEvent(this, type, obj);
+        LOGGER.info("firing route event {}", e);
+        listeners.forEach(l -> {
+            try {
+                l.handleEvent(e);
+            } catch (Exception | LinkageError | AssertionError ex) {
+                LOGGER.error("Failed to invoke RouteListener {}", l, ex);
+            }
+        });
+    }
+
+}
diff --git a/org.simantics.district.route/src/org/simantics/district/route/internal/WaypointImpl.java b/org.simantics.district.route/src/org/simantics/district/route/internal/WaypointImpl.java
new file mode 100644 (file)
index 0000000..d78f0c3
--- /dev/null
@@ -0,0 +1,67 @@
+package org.simantics.district.route.internal;
+
+import java.util.Objects;
+
+import org.simantics.db.Resource;
+import org.simantics.district.route.Waypoint;
+
+/**
+ * @author Tuukka Lehtonen
+ * @since 6.09
+ */
+public class WaypointImpl implements Waypoint {
+
+    private final Resource element;
+    private String label;
+
+    public WaypointImpl(Resource element, String label) {
+        Objects.requireNonNull(element, "Non-null element required");
+        this.element = element;
+        this.label = label;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T getObject() {
+        return (T) element;
+    }
+
+    @Override
+    public String getLabel() {
+        return label;
+    }
+
+    public void setLabel(String label) {
+        this.label = label;
+    }
+
+    @Override
+    public String toString() {
+        return label;
+    }
+
+    public Waypoint withLabel(String label) {
+        return new WaypointImpl(element, label);
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + element.hashCode();
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        WaypointImpl other = (WaypointImpl) obj;
+        return element.equals(other.element);
+    }
+
+}
diff --git a/pom.xml b/pom.xml
index 7834a65a0f048f86f7e4303fe5882d723ba573a7..65e92a3b92d4421a98f036f8dedd695e778440d7 100644 (file)
--- a/pom.xml
+++ b/pom.xml
                <module>org.simantics.district.region</module>
                <module>org.simantics.district.region.ontology</module>
                <module>org.simantics.district.region.ui</module>
+               <module>org.simantics.district.route</module>
+               <module>org.simantics.district.route.ontology</module>
+               <module>org.simantics.district.route.ui</module>
                <module>org.simantics.maps.server</module>
                <module>org.simantics.maps.server.ui</module>
 
                <module>org.simantics.district.feature</module>
                <module>org.simantics.district.regions.feature</module>
+               <module>org.simantics.district.route.feature</module>
                <module>org.simantics.district.ui.feature</module>
                <module>org.simantics.maps.server.feature</module>