0

I'm trying to build a User Interface for the RGBike POV: http://www.instructables.com/id/RGBike-POV-Open-project/ The program will display a bike wheel in form of a grid. The user can click onto the single squares and changes the colour of these. I want to build this applet in java. I'm stuck at drawing the wheel in the right way. I need to have a sort of array of every rectangle, to export the colour later. The best thing would be to draw a sort of circular table. Drawing each shape With graphics2D to have each as a single object would be an idea, too. But that would be around 860 single shapes, little bit too much to update them every time by paint().

Spoke POV has done such a user Interface for their project already: http://www.ladyada.net/make/spokepov/software.html But only their old python script is open source.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
PhoGGy
  • 23
  • 6
  • I wonder why I'm not logged in... – PhoGGy Jan 31 '13 at 03:15
  • Render the grid to a backing buffer (like a `BufferedImage`) and simply paint the image. Update it ONLY when you need to change it. What's your grid requirements?? – MadProgrammer Jan 31 '13 at 03:16
  • I wonder what your question is. What is your question? – Andrew Thompson Jan 31 '13 at 03:16
  • 1
    See also [How to draw a circle in java with a radius and points around the edge](http://stackoverflow.com/questions/2508704/how-to-draw-a-circle-in-java-with-a-radius-and-points-around-the-edge) – trashgod Jan 31 '13 at 03:17
  • Thanks for your answer! My question is: How do I manage to draw this wheel in the way that I can react via mouseclick event to each button? How can I determine which rectangle is pressed if I just draw it as a picture? – PhoGGy Jan 31 '13 at 03:40
  • @MadProgrammer The grid requirements are 16 circles, 80 lines, so 860 rectangles. If I understand you right, you would draw each shape into one backing buffer? I don't know how I can determine which square is clicked, if I only have the coordinates. If each rectangle is a single object, it would be possible by asking each rectangle if it contains the Point. – PhoGGy Jan 31 '13 at 03:57
  • @user1644310 I'd draw the whole thing onto a `BufferedImage`. You could maintain each segment as `Shape` object and using the `contains` method, determine if mouse was clicked within a given area – MadProgrammer Jan 31 '13 at 03:58
  • Are you able to draw the grid by drawing the circles and the lines, and then "extract" the different shape object by determine the crossing points? Otherwise you would have to write code for all the 860 shapes! Using shape and contains method was my idea, too. Can you imagine any sort of formula to draw the shapes? You would save them in one big shape array then? Thanks! – PhoGGy Jan 31 '13 at 04:07
  • I would be able to draw the rectangles, if they wouldn't have a curved shape. Maybe I draw them without a curved top and bottom. Then I could create a formula on my own with the help of my geometry book, I hope. Thanks for your help again! – PhoGGy Jan 31 '13 at 04:36
  • @user1644310 Sorry. Was dragging out source code. I did a quick example, hope it works... – MadProgrammer Jan 31 '13 at 05:00

1 Answers1

3

Be VERY grateful that I have previously generate a "segment" shape in the past ;)

enter image description here

This basically generates each segment individually (does some funky translation into real space) and maintains a cache of shapes which can be checked to see if the mouse falls within there bounds.

This is rather inefficient, but I think you get the idea.

I should also be noted, that I didn't bother with a backing buffer. Not to say it could use one, I just got away without it...

public class TestSpoke {

    public static void main(String[] args) {
        new TestSpoke();
    }

    public TestSpoke() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (Exception ex) {
                }

                JFrame frame = new JFrame("Test");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public static class TestPane extends JPanel {

        public static final int CIRCLE_COUNT = 16;
        public static final int SEGMENT_COUNT = 80;
        private Map<Integer, List<Shape>> mapWheel;
        private Map<Point, Color> mapColors;

        public TestPane() {
            mapColors = new HashMap<>(CIRCLE_COUNT * SEGMENT_COUNT);
            addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {

                    Map<Integer, List<Shape>> mapWheel = getWheel();

                    for (Integer circle : mapWheel.keySet()) {
                        List<Shape> segments = mapWheel.get(circle);
                        for (int index = 0; index < segments.size(); index++) {
                            Shape segment = segments.get(index);
                            if (segment.contains(e.getPoint())) {
                                mapColors.put(new Point(circle, index), Color.RED);
                                repaint();
                                break;
                            }
                        }
                    }
                }
            });
        }

        @Override
        public void invalidate() {
            mapWheel = null;
            super.invalidate();
        }

        protected float getRadius() {
            return Math.min(getWidth(), getHeight());
        }

        /**
         * This builds a wheel (if required) made of segments.
         * @return 
         */
        protected Map<Integer, List<Shape>> getWheel() {
            if (mapWheel == null) {
                mapWheel = new HashMap<>(CIRCLE_COUNT);

                // The current radius
                float radius = getRadius();
                // The radius of each individual circle...
                float circleRadius = radius / CIRCLE_COUNT;
                // The range of each segment
                float extent = 360f / SEGMENT_COUNT;
                for (int circle = 0; circle < CIRCLE_COUNT; circle++) {
                    float startAngle = 0;
                    List<Shape> segments = new ArrayList<>(SEGMENT_COUNT);
                    mapWheel.put(circle, segments);

                    // Calculate the "translation" to place each segement in the
                    // center of the screen
                    float innerRadius = circleRadius * circle;
                    float x = (getWidth() - innerRadius) / 2;
                    float y = (getHeight() - innerRadius) / 2;
                    for (int seg = 0; seg < SEGMENT_COUNT; seg++) {
                        // Generate a Segment shape
                        Segment segment = new Segment(circleRadius * circle, circleRadius, startAngle, extent);
                        startAngle += extent;

                        // We translate the segment to the screen space
                        // This will make it faster to paint and check for mouse clicks
                        PathIterator pi = segment.getPathIterator(AffineTransform.getTranslateInstance(x, y));
                        Path2D path = new Path2D.Float();
                        path.append(pi, true);
                        segments.add(path);
                    }
                }
            }

            return mapWheel;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            Graphics2D g2d = (Graphics2D) g.create();

            Map<Integer, List<Shape>> mapWheel = getWheel();
            for (Integer circle : mapWheel.keySet()) {

                List<Shape> segments = mapWheel.get(circle);
                for (int index = 0; index < segments.size(); index++) {
                    Shape segment = segments.get(index);

                    Color color = mapColors.get(new Point(circle, index));
                    if (color != null) {
                        g2d.setColor(color);
                        g2d.fill(segment);
                    }
                    g2d.setColor(Color.BLACK);
                    g2d.draw(segment);
                }
            }

            g2d.dispose();

        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }
    }

    public static class Segment extends Path2D.Float {

        public Segment(float radius, float thickness, float extent) {
            this(radius, thickness, 0f, extent);
        }

        public Segment(float radius, float thickness, float startAngle, float extent) {
            // Basically, we want to draw the outter edge from a to b angle,
            // draw the connecting line from the outter to the inner,
            // draw the inner from b to a angel and
            // draw the connecting line from the inner to out the outter

            // We want to span about 30 degrees, with a small gap...
            // I want the gap to be a factor of the radius

            Arc2D.Float outter = new Arc2D.Float(0, 0, radius, radius, startAngle, extent, Arc2D.OPEN);
            Arc2D.Float inner = new Arc2D.Float(thickness / 2f, thickness / 2f, radius - thickness, radius - thickness, startAngle + extent, -extent, Arc2D.OPEN);

            append(outter, true);

            float angel = startAngle + extent;
            Point2D p1 = getPointOnEdge(angel, radius);
            Point2D p2 = getPointOnEdge(angel, radius - thickness);
            // We need to adjust in for the change in the radius
            p2.setLocation(p2.getX() + (thickness / 2f), p2.getY() + (thickness / 2f));
            lineTo(p2.getX(), p2.getY());

            append(inner, true);

            angel = startAngle;
            p1 = getPointOnEdge(angel, radius);
            p2 = getPointOnEdge(angel, radius - thickness);
            p2.setLocation(p2.getX() + (thickness / 2f), p2.getY() + (thickness / 2f));
            lineTo(p1.getX(), p1.getY());

            closePath();
        }

        public Point2D getPointOnEdge(float angel, float radius) {
            angel -= 90;

            float x = radius / 2f;
            float y = radius / 2f;

            double rads = Math.toRadians((angel + 90));

            // This determins the length of tick as calculate from the center of
            // the circle.  The original code from which this derived allowed
            // for a varible length line from the center of the cirlce, we
            // actually want the opposite, so we calculate the outter limit first
            float fullLength = (radius / 2f);

            // Calculate the outter point of the line
            float xPosy = (float) (x + Math.cos(rads) * fullLength);
            float yPosy = (float) (y - Math.sin(rads) * fullLength);

            return new Point2D.Float(xPosy, yPosy);
        }
    }
}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Wow, you're unbelievable! Thanks so much! I'm in Melbourne in 10 days, I would like to spend you a beer! ;-) – PhoGGy Jan 31 '13 at 05:14
  • Hey MadProgrammer. I really have to thank you again for this nice piece of code! I needed around 5 hours to understand it fully, but nice commented! I hope you gonna help other people as good as me on the future! Keep Going! You're doing great work! Greetings Phil – PhoGGy Feb 02 '13 at 08:05