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}