Base RXJava POC

This commit is contained in:
Thibaud Gasser 2019-03-19 20:41:21 +01:00
parent 95cb82d419
commit e8840cfd8b
19 changed files with 628 additions and 0 deletions

View File

@ -15,6 +15,13 @@
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<dependencies> <dependencies>
<!-- https://mvnrepository.com/artifact/io.reactivex.rxjava2/rxjava -->
<dependency>
<groupId>io.reactivex.rxjava2</groupId>
<artifactId>rxjava</artifactId>
<version>2.2.7</version>
</dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>

View File

@ -0,0 +1,43 @@
package ovh.gasser.rxshapes;
import ovh.gasser.rxshapes.shapes.SCollection;
import ovh.gasser.rxshapes.shapes.SRectangle;
import ovh.gasser.rxshapes.shapes.Shape;
import ovh.gasser.rxshapes.ui.ShapesView;
import javax.swing.*;
import java.awt.*;
public class App {
public static final Dimension WIN_SIZE = new Dimension(800, 600);
private Shape model;
private App() throws HeadlessException {
final JFrame frame = new JFrame("Reactive shapes");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
buildModel();
final ShapesView view = new ShapesView(this.model);
view.setPreferredSize(WIN_SIZE);
frame.getContentPane().add(view, BorderLayout.CENTER);
frame.setContentPane(view);
frame.pack();
frame.setVisible(true);
}
private void buildModel() {
model = SCollection.of(
SRectangle.create(10, 10, 40, 60, Color.RED),
SRectangle.create(70, 10, 40, 60),
SCollection.of(
SRectangle.create(100, 200, 40, 60, Color.MAGENTA),
SRectangle.create(150, 200, 40, 60, Color.MAGENTA)
)
);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(App::new);
}
}

View File

@ -0,0 +1,39 @@
package ovh.gasser.rxshapes;
import ovh.gasser.rxshapes.shapes.SCollection;
import ovh.gasser.rxshapes.shapes.SRectangle;
import ovh.gasser.rxshapes.ui.html.*;
import java.awt.*;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
public class HTMLExporter {
private final SCollection model;
private HTMLExporter() throws FileNotFoundException {
model = SCollection.of(
SRectangle.create(10, 10, 40, 60, Color.RED),
SRectangle.create(70, 10, 40, 60),
SCollection.of(
SRectangle.create(100, 200, 40, 60, Color.MAGENTA),
SRectangle.create(150, 200, 40, 60, Color.MAGENTA)
)
);
export();
}
private void export() throws FileNotFoundException {
try (final PrintWriter html = new PrintWriter("out.html")) {
try (final PrintWriter css = new PrintWriter("style.css")) {
HTMLDraftman draftman = new HTMLDraftman(html, css);
draftman.generateHTML(this.model);
}
}
}
public static void main(String[] args) throws FileNotFoundException {
new HTMLExporter().export();
}
}

View File

@ -0,0 +1,24 @@
package ovh.gasser.rxshapes;
import ovh.gasser.rxshapes.attributes.SelectionAttributes;
import ovh.gasser.rxshapes.shapes.Shape;
public class Selection {
private final SelectionAttributes attributes;
public final Shape shape;
public Selection(final Shape shape, boolean selected) {
this(shape);
attributes.selected = selected;
}
private Selection(final Shape shape) {
attributes = (SelectionAttributes) shape.getAttributes(SelectionAttributes.ID);
this.shape = shape;
}
public void unselect() {
attributes.selected = false;
}
}

View File

@ -0,0 +1,10 @@
package ovh.gasser.rxshapes;
import ovh.gasser.rxshapes.shapes.SCollection;
import ovh.gasser.rxshapes.shapes.SRectangle;
public interface ShapeVisitor {
void visitRectangle(SRectangle sRectangle);
void visitCollection(SCollection collection);
}

View File

@ -0,0 +1,5 @@
package ovh.gasser.rxshapes.attributes;
public interface Attributes {
String getID();
}

View File

@ -0,0 +1,33 @@
package ovh.gasser.rxshapes.attributes;
import java.awt.*;
public class ColorAttributes implements Attributes {
public static final String ID = "COLOR_ATTRS";
public final boolean filled;
public final boolean stroked;
public final Color filledColor;
public final Color strokedColor;
public ColorAttributes(boolean filled, boolean stroked, Color filledColor, Color strokedColor) {
this.filled = filled;
this.stroked = stroked;
this.filledColor = filledColor;
this.strokedColor = strokedColor;
}
@Override
public String toString() {
return "ColorAttributes{" +
"filled=" + filled +
", stroked=" + stroked +
", filledColor=" + filledColor +
", strokedColor=" + strokedColor +
'}';
}
@Override
public String getID() {
return ID;
}
}

View File

@ -0,0 +1,11 @@
package ovh.gasser.rxshapes.attributes;
public class SelectionAttributes implements Attributes {
public static final String ID = "SELECTION_ATTRS";
public boolean selected;
@Override
public String getID() {
return ID;
}
}

View File

@ -0,0 +1,51 @@
package ovh.gasser.rxshapes.shapes;
import ovh.gasser.rxshapes.attributes.Attributes;
import java.awt.*;
import java.util.Map;
import java.util.TreeMap;
public abstract class AbstractShape implements Shape {
private Map<String, Attributes> attributes = new TreeMap<>();
private Rectangle bounds;
AbstractShape() {
this(null);
}
AbstractShape(Rectangle bounds) {
this.bounds = bounds;
}
@Override
public Attributes getAttributes(String key) {
return attributes.get(key);
}
@Override
public void addAttributes(Attributes attrs) {
attributes.put(attrs.getID(), attrs);
}
@Override
public void setLoc(Point newLoc) {
getBounds().setLocation(newLoc);
}
@Override
public void translate(int dx, int dy) {
getBounds().translate(dx, dy);
}
@Override
public Rectangle getBounds() {
return this.bounds;
}
@Override
public String toString() {
return String.format("x=%d, y=%d, width=%d, height=%d", bounds.x, bounds.y, bounds.width, bounds.height);
}
}

View File

@ -0,0 +1,65 @@
package ovh.gasser.rxshapes.shapes;
import ovh.gasser.rxshapes.App;
import ovh.gasser.rxshapes.attributes.SelectionAttributes;
import ovh.gasser.rxshapes.ShapeVisitor;
import ovh.gasser.rxshapes.util.Streamable;
import java.awt.*;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
public class SCollection extends AbstractShape implements Streamable<Shape> {
private final List<Shape> children;
private SCollection(Shape... shapes) {
this.children = List.of(shapes);
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visitCollection(this);
}
@Override
public Rectangle getBounds() {
try {
Rectangle bounds = children.get(0).getBounds();
for (Shape s : children) bounds = bounds.union(s.getBounds());
return bounds;
} catch (IndexOutOfBoundsException e){
// If the SCollection is empty, set the bounds to fill the window
return new Rectangle(App.WIN_SIZE);
}
}
@Override
public void setLoc(Point newLoc) {
final Point loc = getBounds().getLocation();
children.forEach(s -> s.translate(newLoc.x - loc.x, newLoc.y - loc.y));
}
@Override
public Iterator<Shape> iterator() {
return children.iterator();
}
@Override
public Spliterator<Shape> spliterator() {
return children.spliterator();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("SCollection{");
children.forEach(obj -> sb.append(obj).append(", "));
return sb.append("}").toString();
}
public static SCollection of(Shape ...shapes) {
SCollection collection = new SCollection(shapes);
collection.addAttributes(new SelectionAttributes());
return collection;
}
}

View File

@ -0,0 +1,38 @@
package ovh.gasser.rxshapes.shapes;
import ovh.gasser.rxshapes.attributes.ColorAttributes;
import ovh.gasser.rxshapes.attributes.SelectionAttributes;
import ovh.gasser.rxshapes.ShapeVisitor;
import java.awt.*;
public class SRectangle extends AbstractShape {
private SRectangle(Rectangle rect) {
super(rect);
}
@Override
public void accept(ShapeVisitor visitor) {
visitor.visitRectangle(this);
}
@Override
public String toString() {
return "SRectangle{" +
super.toString() +
'}';
}
public static SRectangle create(int x, int y, int width, int height) {
SRectangle rect = new SRectangle(new Rectangle(x, y, width, height));
rect.addAttributes(new SelectionAttributes());
return rect;
}
public static SRectangle create(int x, int y, int width, int height, Color color) {
final SRectangle rect = create(x, y, width, height);
rect.addAttributes(new ColorAttributes(false, true, Color.BLACK, color));
return rect;
}
}

View File

@ -0,0 +1,15 @@
package ovh.gasser.rxshapes.shapes;
import ovh.gasser.rxshapes.ShapeVisitor;
import ovh.gasser.rxshapes.attributes.Attributes;
import java.awt.*;
public interface Shape {
void accept(ShapeVisitor visitor);
void setLoc(Point newLoc);
void translate(int dx, int dy);
Attributes getAttributes(String key);
void addAttributes(Attributes attr);
Rectangle getBounds();
}

View File

@ -0,0 +1,62 @@
package ovh.gasser.rxshapes.ui;
import io.reactivex.Observable;
import io.reactivex.disposables.Disposable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ovh.gasser.rxshapes.Selection;
import ovh.gasser.rxshapes.util.EventSource;
import ovh.gasser.rxshapes.shapes.Shape;
import ovh.gasser.rxshapes.shapes.SCollection;
import java.awt.event.MouseEvent;
import java.util.Optional;
public class Controller {
private final Logger logger = LoggerFactory.getLogger(ShapesView.class);
private final Disposable mouseSub;
private Selection selection;
Controller(ShapesView view, Shape model) {
mouseSub = EventSource
.fromMouseEventsOf(view)
.subscribe(evt -> {
assert model instanceof SCollection;
SCollection sc = (SCollection) model;
switch (evt.getID()) {
case MouseEvent.MOUSE_PRESSED:
getTarget(evt, sc)
.ifPresentOrElse(
s -> selection = new Selection(s, true),
() -> {
if (selection != null) {
selection.unselect();
selection = null;
}
}
);
break;
case MouseEvent.MOUSE_DRAGGED:
handleMouseMoved(evt);
break;
}
view.repaint();
}, err -> logger.error("{}", err)
);
}
private Optional<Shape> getTarget(MouseEvent evt, SCollection sc) {
return sc.stream()
.filter(s -> s.getBounds().contains(evt.getPoint()))
.findFirst();
}
private void handleMouseMoved(MouseEvent evt) {
if (selection != null) selection.shape.setLoc(evt.getPoint());
}
public void dispose() {
logger.info("Cleaning subscriptions...");
mouseSub.dispose();
}
}

View File

@ -0,0 +1,59 @@
package ovh.gasser.rxshapes.ui;
import ovh.gasser.rxshapes.ShapeVisitor;
import ovh.gasser.rxshapes.shapes.Shape;
import ovh.gasser.rxshapes.attributes.ColorAttributes;
import ovh.gasser.rxshapes.attributes.SelectionAttributes;
import ovh.gasser.rxshapes.shapes.SCollection;
import ovh.gasser.rxshapes.shapes.SRectangle;
import java.awt.*;
public class ShapeDraftman implements ShapeVisitor {
private static final ColorAttributes DEFAULT_COLOR_ATTRIBUTES =
new ColorAttributes(false, true, Color.BLACK, Color.BLACK);
private Graphics2D g2d;
public ShapeDraftman(Graphics graph) {
this.g2d = (Graphics2D) graph;
}
@Override
public void visitRectangle(SRectangle rect) {
Rectangle r = rect.getBounds();
ColorAttributes colAttrs = (ColorAttributes) rect.getAttributes(ColorAttributes.ID);
if (colAttrs == null){
colAttrs = DEFAULT_COLOR_ATTRIBUTES;
}
if (colAttrs.filled) {
this.g2d.setColor(colAttrs.filledColor);
this.g2d.fillRect(r.x, r.y, r.width, r.height);
}
if (colAttrs.stroked) {
this.g2d.setColor(colAttrs.strokedColor);
this.g2d.drawRect(r.x, r.y, r.width, r.height);
}
drawHandlerIfSelected(rect);
}
@Override
public void visitCollection(SCollection collection) {
for (Shape s: collection) {
s.accept(this);
}
drawHandlerIfSelected(collection);
}
private void drawHandlerIfSelected(Shape s) {
SelectionAttributes selAttrs = (SelectionAttributes) s.getAttributes(SelectionAttributes.ID);
if ((selAttrs != null) && (selAttrs.selected)){
Rectangle bounds = s.getBounds();
this.g2d.setColor(Color.RED);
this.g2d.drawRect(bounds.x - 5, bounds.y - 5, 5, 5);
this.g2d.drawRect(bounds.x + bounds.width, bounds.y + bounds.height, 5, 5);
}
}
}

View File

@ -0,0 +1,29 @@
package ovh.gasser.rxshapes.ui;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ovh.gasser.rxshapes.ShapeVisitor;
import ovh.gasser.rxshapes.shapes.Shape;
import javax.swing.*;
import java.awt.*;
public class ShapesView extends JPanel {
final Logger logger = LoggerFactory.getLogger(ShapesView.class);
private final Shape model;
private final Controller controller;
private ShapeVisitor draftman;
public ShapesView(Shape model) {
this.model = model;
this.controller = new Controller(this, model);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
this.draftman = new ShapeDraftman(g);
model.accept(draftman);
}
}

View File

@ -0,0 +1,85 @@
package ovh.gasser.rxshapes.ui.html;
import ovh.gasser.rxshapes.ShapeVisitor;
import ovh.gasser.rxshapes.attributes.ColorAttributes;
import ovh.gasser.rxshapes.shapes.SCollection;
import ovh.gasser.rxshapes.shapes.SRectangle;
import java.awt.*;
import java.io.PrintWriter;
public class HTMLDraftman implements ShapeVisitor {
private static final String HEADER_TEMPLATE = "<!DOCTYPE html>" +
"<html lang=\"fr\">" +
"<head>" +
" <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />" +
" <link rel=\"stylesheet\" href=\"style.css\" />" +
" <title>Reactive shapes HTML</title>" +
"</head>" +
"<body>";
private static final String FOOTER_TEMPLATE = "</body>";
private PrintWriter htmlOutput;
private PrintWriter cssOutput;
public HTMLDraftman(PrintWriter htmlOutput, PrintWriter cssOutput) {
this.htmlOutput = htmlOutput;
this.cssOutput = cssOutput;
}
@Override
public void visitRectangle(SRectangle rect) {
htmlOutput.println("<div id=\"rec" + rect.hashCode() + "\"></div>");
cssOutput.println("#rec" + rect.hashCode() + "{ ");
cssOutput.println("position:absolute;");
cssOutput.println("top:" + rect.getBounds().y + "px;");
cssOutput.println("left:" + rect.getBounds().x + "px;");
cssOutput.println("width:" + rect.getBounds().width + "px;");
cssOutput.println("height:" + rect.getBounds().height + "px;");
cssOutput.println(this.attributesToCss(rect) + " }");
}
private String attributesToCss(SRectangle rect) {
ColorAttributes attrs = (ColorAttributes) rect.getAttributes(ColorAttributes.ID);
String strokedColor = "#ffffff";
String filledColor = "#ffffff";
if (attrs != null && attrs.filledColor != null){
filledColor = formatCSSColor(attrs.filledColor);
}
if (attrs != null && attrs.strokedColor != null){
strokedColor = formatCSSColor(attrs.strokedColor);
}
if(attrs != null && attrs.stroked && attrs.filled){
return "background: " + filledColor + ";border:1px solid " + strokedColor + ";";
}
if (attrs != null && attrs.stroked){
return "background:#ffffff;border:1px solid "+strokedColor+";";
}
if (attrs != null && attrs.filled) {
return "background: "+filledColor+";";
}
return "";
}
private String formatCSSColor(Color col) {
final int r = col.getRed();
final int g = col.getGreen();
final int b = col.getBlue();
return String.format("#%02x%02x%02x", r, g, b);
}
@Override
public void visitCollection(SCollection collection) {
collection.stream().forEach(shape -> shape.accept(this));
}
public void generateHTML(SCollection model) {
htmlOutput.println(HEADER_TEMPLATE);
visitCollection(model);
htmlOutput.println(FOOTER_TEMPLATE);
}
}

View File

@ -0,0 +1,29 @@
package ovh.gasser.rxshapes.util;
import io.reactivex.Observable;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.concurrent.Future;
public class EventSource {
public static Observable<MouseEvent> fromMouseEventsOf(final Component component) {
return Observable.create(emitter -> {
final MouseAdapter adapter = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
emitter.onNext(e);
}
@Override
public void mouseDragged(MouseEvent e) {
emitter.onNext(e);
}
};
component.addMouseMotionListener(adapter);
component.addMouseListener(adapter);
emitter.setCancellable(() -> component.removeMouseListener(adapter));
});
}
}

View File

@ -0,0 +1,10 @@
package ovh.gasser.rxshapes.util;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public interface Streamable<T> extends Iterable<T> {
default Stream<T> stream() {
return StreamSupport.stream(this.spliterator(), false);
}
}

View File

@ -0,0 +1,13 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %highlight(%-5level) [%10thread] %cyan(%-40logger{36}) - %msg%n</pattern>
</encoder>
</appender>
<root level="trace">
<appender-ref ref="STDOUT" />
</root>
</configuration>