001package com.hfg.svg;
002
003import com.hfg.graphics.Graphics2DState;
004import com.hfg.svg.path.SvgPathCmd;
005import com.hfg.svg.path.SvgPathCurveToCmd;
006import com.hfg.svg.path.SvgPathQuadCurveToCmd;
007import com.hfg.svg.path.SvgPathSmoothCurveToCmd;
008import com.hfg.svg.path.SvgPathSmoothQuadCurveToCmd;
009import com.hfg.graphics.ColorUtil;
010import com.hfg.util.StringUtil;
011import com.hfg.util.collection.CollectionUtil;
012import com.hfg.xml.XMLTag;
013
014import java.awt.*;
015import java.awt.geom.GeneralPath;
016import java.awt.geom.Point2D;
017import java.util.ArrayList;
018import java.util.List;
019
020//------------------------------------------------------------------------------
021/**
022 * Object representation of an SVG (Scalable Vector Graphics) path tag.
023 *
024 * @author J. Alex Taylor, hairyfatguy.com
025 */
026//------------------------------------------------------------------------------
027// com.hfg XML/HTML Coding Library
028//
029// This library is free software; you can redistribute it and/or
030// modify it under the terms of the GNU Lesser General Public
031// License as published by the Free Software Foundation; either
032// version 2.1 of the License, or (at your option) any later version.
033//
034// This library is distributed in the hope that it will be useful,
035// but WITHOUT ANY WARRANTY; without even the implied warranty of
036// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
037// Lesser General Public License for more details.
038//
039// You should have received a copy of the GNU Lesser General Public
040// License along with this library; if not, write to the Free Software
041// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
042//
043// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
044// jataylor@hairyfatguy.com
045//------------------------------------------------------------------------------
046// From http://www.w3.org/TR/SVG/paths.html
047//
048// Superfluous white space and separators such as commas can be eliminated
049// (e.g., "M 100 100 L 200 200" contains unnecessary spaces and could be expressed more compactly as "M100 100L200 200").
050// The command letter can be eliminated on subsequent commands if the same command is used multiple times in a row
051// (e.g., you can drop the second "L" in "M 100 200 L 200 100 L -100 -200" and use "M 100 200 L 200 100 -100 -200" instead).
052
053public class SvgPath extends AbstractSvgNode implements SvgNode
054{
055//   private static Color  DEFAULT_FILL = HTMLColor.BLACK;
056
057   private List<SvgPathCmd> mPathCmds;
058
059   //###########################################################################
060   // CONSTRUCTORS
061   //###########################################################################
062
063   //---------------------------------------------------------------------------
064   public SvgPath()
065   {
066      super(SVG.path);
067//      setFill(DEFAULT_FILL);
068   }
069
070   //---------------------------------------------------------------------------
071   public SvgPath(String inPathData)
072   {
073      this();
074      setData(inPathData);
075   }
076
077   //---------------------------------------------------------------------------
078   public SvgPath(XMLTag inXMLTag)
079   {
080      super(SVG.path);  // Don't call this() or we will pick up the default fill color
081      initFromXMLTag(inXMLTag);
082   }
083
084   //###########################################################################
085   // PUBLIC METHODS
086   //###########################################################################
087
088   //---------------------------------------------------------------------------
089   public SvgPath setFill(Color inColor)
090   {
091      setAttribute(SvgAttr.fill, inColor != null ? "#" + ColorUtil.colorToHex(inColor) : SvgAttr.Value.none);
092      return this;
093   }
094
095   //---------------------------------------------------------------------------
096   public SvgPath setStroke(Color inColor)
097   {
098      setAttribute(SvgAttr.stroke, inColor != null ? "#" + ColorUtil.colorToHex(inColor) : SvgAttr.Value.none);
099      return this;
100   }
101
102   //---------------------------------------------------------------------------
103   public SvgPath setStrokeWidth(int inValue)
104   {
105      setAttribute(SvgAttr.strokeWidth, inValue);
106      return this;
107   }
108
109   //--------------------------------------------------------------------------
110   @Override
111   public SvgPath addStyle(String inValue)
112   {
113      return (SvgPath) super.addStyle(inValue);
114   }
115
116   //---------------------------------------------------------------------------
117   @Override
118   public SvgPath setStyle(String inValue)
119   {
120      return (SvgPath) super.setStyle(inValue);
121   }
122
123   //---------------------------------------------------------------------------
124   @Override
125   public SvgPath setTransform(String inValue)
126   {
127      return (SvgPath) super.setTransform(inValue);
128   }
129
130   //---------------------------------------------------------------------------
131   public SvgPath setOnMouseOver(String inValue)
132   {
133      setAttribute(SvgAttr.onmouseover, inValue);
134      return this;
135   }
136
137   //---------------------------------------------------------------------------
138   public SvgPath setOnMouseOut(String inValue)
139   {
140      setAttribute(SvgAttr.onmouseout, inValue);
141      return this;
142   }
143
144   //---------------------------------------------------------------------------
145   public SvgPath setOnMouseDown(String inValue)
146   {
147      setAttribute(SvgAttr.onmousedown, inValue);
148      return this;
149   }
150
151
152
153   //---------------------------------------------------------------------------
154   public SvgPath setData(String inValue)
155   {
156      setAttribute(SvgAttr.d, inValue);
157      return this;
158   }
159
160   //---------------------------------------------------------------------------
161   public String getData()
162   {
163      return getAttributeValue(SvgAttr.d);
164   }
165
166   //--------------------------------------------------------------------------
167   @Override
168   public String toString()
169   {
170      return toXML();
171   }
172
173   //---------------------------------------------------------------------------
174   public SvgPath addPathCommand(SvgPathCmd inValue)
175   {
176      if (inValue != null)
177      {
178         if (null == mPathCmds)
179         {
180            mPathCmds = new ArrayList<>(20);
181         }
182
183         mPathCmds.add(inValue);
184
185         setData(StringUtil.join(mPathCmds, " "));
186      }
187
188      return this;
189   }
190
191   //---------------------------------------------------------------------------
192   public List<SvgPathCmd> getPathCommands()
193   {
194      if (null == mPathCmds
195            && StringUtil.isSet(getData()))
196      {
197         mPathCmds = parsePathData();
198      }
199
200      return mPathCmds;
201   }
202
203   //---------------------------------------------------------------------------
204   // Path data could look like 'M 100 100 L 300 100 L 200 300 z'
205   // or like
206   @Override
207   public Rectangle getBoundsBox()
208   {
209      Rectangle boundsBox = null;
210
211      List<SvgPathCmd> pathCmds = getPathCommands();
212      if (CollectionUtil.hasValues(pathCmds))
213      {
214         boundsBox = generateGeneralPath().getBounds();
215      }
216
217      adjustBoundsForTransform(boundsBox);
218
219      return boundsBox;
220   }
221
222   //--------------------------------------------------------------------------
223   @Override
224   public void draw(Graphics2D g2)
225   {
226      // Save settings
227      Graphics2DState origState = new Graphics2DState(g2);
228
229      GeneralPath path = generateGeneralPath();
230
231      // Fill the path
232      Paint paint = getG2Paint();
233      if (paint != null)
234      {
235         g2.setPaint(paint);
236         g2.fill(path);
237      }
238
239      // Set the stroke
240      Stroke stroke = getG2Stroke();
241      if (stroke != null)
242      {
243         g2.setStroke(stroke);
244      }
245
246      // Set the stroke color
247      paint = getG2StrokeColor();
248      if (null == paint) paint = Color.BLACK;
249      g2.setPaint(paint);
250
251      applyTransforms(g2);
252
253      g2.draw(path);
254
255      // Restore settings
256      origState.applyTo(g2);
257   }
258
259   //--------------------------------------------------------------------------
260   /**
261    Generates the path for a curly brace between two specified points.
262    * @param inPoint1 "from" point
263    * @param inPoint2 "to" point
264    * @param inWidth  width of the curly brace
265    * @return the SVG path for the curly brace
266    */
267   public static SvgPath generateCurlyBracket(Point2D inPoint1, Point2D inPoint2, int inWidth)
268   {
269      // Calculate unit vector
270      double dx = inPoint1.getX() - inPoint2.getX();
271      double dy = inPoint1.getY() - inPoint2.getY();
272      double len = Math.sqrt(dx * dx + dy * dy);
273      dx = dx / len;
274      dy = dy / len;
275
276      double q = 0.5;
277
278      // Calculate path control points
279      double qx1 = inPoint1.getX() + q * inWidth * dy;
280      double qy1 = inPoint1.getY() - q * inWidth * dx;
281      double qx2 = (inPoint1.getX() - .25*len * dx) + (1-q) * inWidth * dy;
282      double qy2 = (inPoint1.getY() - .25*len * dy) - (1-q) * inWidth * dx;
283      double tx1 = (inPoint1.getX() -  .5*len * dx) + inWidth * dy;
284      double ty1 = (inPoint1.getY() -  .5*len * dy) - inWidth * dx;
285      double qx3 = inPoint2.getX() + q * inWidth * dy;
286      double qy3 = inPoint2.getY() - q * inWidth * dx;
287      double qx4 = (inPoint1.getX() - .75 * len * dx) + (1-q) * inWidth * dy;
288      double qy4 = (inPoint1.getY() - .75 * len * dy) - (1-q) * inWidth * dx;
289
290
291      return new SvgPath("M " + (int) inPoint1.getX() + " " + (int) inPoint1.getY() +
292                            " Q " + (int) qx1 + " " + (int) qy1 + " " + (int) qx2 + " " + (int) qy2 +
293                                    " T " + (int) tx1 + " " + (int) ty1 +
294                               " M " + (int) inPoint2.getX() + " " + (int) inPoint2.getY() +
295                                    " Q " + (int) qx3 + " " + (int) qy3 + " " + (int) qx4 + " " + (int) qy4 +
296                                    " T " + (int) tx1 + " " + (int) ty1 ).setFill(null).setStroke(Color.BLACK);
297   }
298
299   //###########################################################################
300   // PRIVATE METHODS
301   //###########################################################################
302
303   //---------------------------------------------------------------------------
304   private GeneralPath generateGeneralPath()
305   {
306      int numSteps = 0;
307      for (SvgPathCmd cmd : getPathCommands())
308      {
309         numSteps += cmd.getNumSteps();
310      }
311
312      GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD, numSteps);
313
314      Point2D.Float lastPoint = null;
315      SvgPathCmd prevCmd = null;
316      for (SvgPathCmd cmd : getPathCommands())
317      {
318         cmd.setStartingPoint(lastPoint);
319         // The smooth (shorthand) curve cmds need extra info from the previous cmd
320         if (cmd instanceof SvgPathSmoothCurveToCmd
321               && prevCmd != null
322               && prevCmd instanceof SvgPathCurveToCmd)
323         {
324            ((SvgPathSmoothCurveToCmd)cmd).setPrevCmdSecondControlPoint(((SvgPathCurveToCmd)prevCmd).getSecondControlPoint());
325         }
326         else if (cmd instanceof SvgPathSmoothQuadCurveToCmd
327                  && prevCmd != null
328                  && prevCmd instanceof SvgPathQuadCurveToCmd)
329         {
330            ((SvgPathSmoothQuadCurveToCmd)cmd).setPrevCmdControlPoint(((SvgPathQuadCurveToCmd) prevCmd).getControlPoint());
331         }
332
333         lastPoint = cmd.draw(path);
334         prevCmd = cmd;
335      }
336
337      return path;
338   }
339
340   //---------------------------------------------------------------------------
341   private List<SvgPathCmd> parsePathData()
342   {
343      List<SvgPathCmd> commands = new ArrayList<>();
344
345      SvgPathCmd currentCmd = null;
346      StringBuilder numBuffer = new StringBuilder();
347      List<Float> rawNumbers = new ArrayList<>();
348      String pathData = getData();
349      for (int i = 0; i < pathData.length(); i++)
350      {
351         char theChar = pathData.charAt(i);
352
353         if (Character.isLetter(theChar))
354         {
355            if (currentCmd != null)
356            {
357               currentCmd.setRawNumbers(rawNumbers);
358               rawNumbers.clear();
359            }
360            currentCmd = SvgPathCmd.allocate(theChar);
361            if (null == currentCmd)
362            {
363               throw new RuntimeException(StringUtil.singleQuote(theChar) + " is not a recognized SVG path command!");
364            }
365
366            commands.add(currentCmd);
367         }
368         else if (theChar == ','
369                  || Character.isWhitespace(theChar))
370         {
371            if (numBuffer.length() > 0)
372            {
373               rawNumbers.add(Float.parseFloat(numBuffer.toString()));
374               numBuffer.setLength(0);
375            }
376         }
377         else
378         {
379            numBuffer.append(theChar);
380         }
381      }
382
383      if (numBuffer.length() > 0)
384      {
385         rawNumbers.add(Float.parseFloat(numBuffer.toString()));
386      }
387
388      if (CollectionUtil.hasValues(rawNumbers))
389      {
390         currentCmd.setRawNumbers(rawNumbers);
391      }
392
393      return commands;
394   }
395}