001package armyc2.c5isr.renderer.utilities; 002 003import android.graphics.Canvas; 004import android.graphics.Paint; 005import android.graphics.Paint.Style; 006import android.graphics.Point; 007import android.graphics.RectF; 008import android.util.SparseArray; 009 010import java.util.Map; 011import java.util.TreeSet; 012import java.util.logging.Level; 013import java.util.regex.Matcher; 014import java.util.regex.Pattern; 015 016public class RendererUtilities { 017 018 private static final float OUTLINE_SCALING_FACTOR = 2.5f; 019 private static SparseArray<Color> pastIdealOutlineColors = new SparseArray<Color>(); 020 /** 021 * 022 * @param color {String} color like "#FFFFFF" 023 * @return {String} 024 */ 025 public static Color getIdealOutlineColor(Color color){ 026 Color idealColor = Color.white; 027 028 if(color != null && pastIdealOutlineColors.indexOfKey(color.toInt())>=0) 029 { 030 return pastIdealOutlineColors.get(color.toInt()); 031 }//*/ 032 033 if(color != null) 034 { 035 036 int threshold = RendererSettings.getInstance().getTextBackgroundAutoColorThreshold(); 037 038 int r = color.getRed(); 039 int g = color.getGreen(); 040 int b = color.getBlue(); 041 042 float delta = ((r * 0.299f) + (g * 0.587f) + (b * 0.114f)); 043 044 if((255 - delta < threshold)) 045 { 046 idealColor = Color.black; 047 } 048 else 049 { 050 idealColor = Color.white; 051 } 052 } 053 054 if(color != null) 055 pastIdealOutlineColors.put(color.toInt(),idealColor); 056 057 return idealColor; 058 } 059 060 public static void renderSymbolCharacter(Canvas ctx, String symbol, int x, int y, Paint paint, Color color, int outlineWidth) 061 { 062 int tbm = RendererSettings.getInstance().getTextBackgroundMethod(); 063 064 Color outlineColor = RendererUtilities.getIdealOutlineColor(color); 065 066 //if(tbm == RendererSettings.TextBackgroundMethod_OUTLINE_QUICK) 067 //{ 068 //draw symbol outline 069 paint.setStyle(Style.FILL); 070 071 paint.setColor(outlineColor.toInt()); 072 if(outlineWidth > 0) 073 { 074 for(int i = 1; i <= outlineWidth; i++) 075 { 076 if(i % 2 == 1) 077 { 078 ctx.drawText(symbol, x - i, y, paint); 079 ctx.drawText(symbol, x + i, y, paint); 080 ctx.drawText(symbol, x, y + i, paint); 081 ctx.drawText(symbol, x, y - i, paint); 082 } 083 else 084 { 085 ctx.drawText(symbol, x - i, y - i, paint); 086 ctx.drawText(symbol, x + i, y - i, paint); 087 ctx.drawText(symbol, x - i, y + i, paint); 088 ctx.drawText(symbol, x + i, y + i, paint); 089 } 090 091 } 092 093 } 094 //draw symbol 095 paint.setColor(color.toInt()); 096 097 ctx.drawText(symbol, x, y, paint); 098 099 /*} 100 else 101 { 102 //draw text outline 103 paint.setStyle(Style.STROKE); 104 paint.setStrokeWidth(RendererSettings.getInstance().getTextOutlineWidth()); 105 paint.setColor(outlineColor.toInt()); 106 if(outlineWidth > 0) 107 { 108 109 ctx.drawText(symbol, x, y, paint); 110 111 } 112 //draw text 113 paint.setColor(color.toInt()); 114 paint.setStyle(Style.FILL); 115 116 ctx.drawText(symbol, x, y, paint); 117 }//*/ 118 } 119 120 /** 121 * Create a copy of the {@Color} object with the passed alpha value. 122 * @param color {@Color} object used for RGB values 123 * @param alpha {@float} value between 0 and 1 124 * @return 125 */ 126 public static Color setColorAlpha(Color color, float alpha) { 127 if (color != null) 128 { 129 if(alpha >= 0 && alpha <= 1) 130 return new Color(color.getRed(),color.getGreen(),color.getBlue(),(int)(alpha*255f)); 131 else 132 return color; 133 } 134 else 135 return null; 136 } 137 public static String colorToHexString(Color color, Boolean withAlpha) 138 { 139 if(color != null) 140 { 141 String hex = color.toHexString(); 142 hex = hex.toUpperCase(); 143 if(withAlpha) 144 return "#" + hex; 145 else 146 return "#" + hex.substring(2); 147 } 148 return null; 149 } 150 151 /** 152 * 153 * @param hexValue - String representing hex value (formatted "0xRRGGBB" 154 * i.e. "0xFFFFFF") OR formatted "0xAARRGGBB" i.e. "0x00FFFFFF" for a color 155 * with an alpha value I will also put up with "RRGGBB" and "AARRGGBB" 156 * without the starting "0x" 157 * @return 158 */ 159 public static Color getColorFromHexString(String hexValue) 160 { 161 try 162 { 163 if(hexValue==null || hexValue.isEmpty()) 164 return null; 165 String hexOriginal = hexValue; 166 167 String hexAlphabet = "0123456789ABCDEF"; 168 169 if (hexValue.charAt(0) == '#') 170 { 171 hexValue = hexValue.substring(1); 172 } 173 if (hexValue.substring(0, 2).equals("0x") || hexValue.substring(0, 2).equals("0X")) 174 { 175 hexValue = hexValue.substring(2); 176 } 177 178 hexValue = hexValue.toUpperCase(); 179 180 int count = hexValue.length(); 181 int[] value = null; 182 int k = 0; 183 int int1 = 0; 184 int int2 = 0; 185 186 if (count == 8 || count == 6) 187 { 188 value = new int[(count / 2)]; 189 for (int i = 0; i < count; i += 2) 190 { 191 int1 = hexAlphabet.indexOf(hexValue.charAt(i)); 192 int2 = hexAlphabet.indexOf(hexValue.charAt(i + 1)); 193 194 if(int1 == -1 || int2 == -1) 195 { 196 ErrorLogger.LogMessage("SymbolUtilities", "getColorFromHexString", "Bad hex value: " + hexOriginal, Level.WARNING); 197 return null; 198 } 199 200 value[k] = (int1 * 16) + int2; 201 k++; 202 } 203 204 if (count == 8) 205 { 206 return new Color(value[1], value[2], value[3], value[0]); 207 } 208 else if (count == 6) 209 { 210 return new Color(value[0], value[1], value[2]); 211 } 212 } 213 else 214 { 215 ErrorLogger.LogMessage("SymbolUtilities", "getColorFromHexString", "Bad hex value: " + hexOriginal, Level.WARNING); 216 } 217 return null; 218 } 219 catch (Exception exc) 220 { 221 ErrorLogger.LogException("SymbolUtilities", "getColorFromHexString", exc); 222 return null; 223 } 224 } 225 226 /** 227 * For Renderer Use Only 228 * Assumes a fresh SVG String from the SVGLookup with its default values 229 * @param symbolID 230 * @param svg 231 * @param strokeColor hex value like "#FF0000"; 232 * @param fillColor hex value like "#FF0000"; 233 * @return SVG String 234 */ 235 public static String setSVGFrameColors(String symbolID, String svg, Color strokeColor, Color fillColor) 236 { 237 String returnSVG = null; 238 String hexStrokeColor = null; 239 String hexFillColor = null; 240 float strokeAlpha = 1; 241 float fillAlpha = 1; 242 String strokeOpacity = ""; 243 String fillOpacity = ""; 244 245 int ss = SymbolID.getSymbolSet(symbolID); 246 int ver = SymbolID.getVersion(symbolID); 247 int affiliation = SymbolID.getAffiliation(symbolID); 248 String defaultFillColor = null; 249 returnSVG = svg; 250 if(strokeColor != null) 251 { 252 if(strokeColor.getAlpha() != 255) 253 { 254 strokeAlpha = strokeColor.getAlpha() / 255.0f; 255 strokeOpacity = " stroke-opacity=\"" + String.valueOf(strokeAlpha) + "\""; 256 fillOpacity = " fill-opacity=\"" + String.valueOf(strokeAlpha) + "\""; 257 } 258 259 hexStrokeColor = colorToHexString(strokeColor,false); 260 returnSVG = svg.replaceAll("stroke=\"#000000\"", "stroke=\"" + hexStrokeColor + "\"" + strokeOpacity); 261 returnSVG = returnSVG.replaceAll("fill=\"#000000\"", "fill=\"" + hexStrokeColor + "\"" + fillOpacity); 262 263 if(ss == SymbolID.SymbolSet_LandInstallation || 264 ss == SymbolID.SymbolSet_Space || 265 ss == SymbolID.SymbolSet_CyberSpace || 266 ss == SymbolID.SymbolSet_Activities) 267 {//add group fill so the extra shapes in these frames have the new frame color 268 String svgStart = "<g id=\"" + SVGLookup.getFrameID(symbolID) + "\">"; 269 String svgStartReplace = svgStart.substring(0,svgStart.length()-1) + " fill=\"" + hexStrokeColor + "\"" + fillOpacity + ">"; 270 returnSVG = returnSVG.replace(svgStart,svgStartReplace); 271 } 272 273 if((SymbolID.getSymbolSet(symbolID)==SymbolID.SymbolSet_LandInstallation && SymbolID.getFrameShape(symbolID)=='0') || 274 SymbolID.getFrameShape(symbolID)==SymbolID.FrameShape_LandInstallation) 275 { 276 int i1 = findInstIndIndex(returnSVG)+5; 277 //make sure installation indicator matches line color 278 returnSVG = returnSVG.substring(0,i1) + " fill=\"" + hexStrokeColor + "\"" + returnSVG.substring(i1); 279 } 280 281 } 282 else if((SymbolID.getSymbolSet(symbolID)==SymbolID.SymbolSet_LandInstallation && SymbolID.getFrameShape(symbolID)=='0') || 283 SymbolID.getFrameShape(symbolID)==SymbolID.FrameShape_LandInstallation) 284 { 285 int i1 = findInstIndIndex(returnSVG)+5; 286 //No line color change so make sure installation indicator stays black 287 returnSVG = returnSVG.substring(0,i1) + " fill=\"#000000\"" + returnSVG.substring(i1); 288 } 289 290 if(fillColor != null) 291 { 292 if(fillColor.getAlpha() != 255) 293 { 294 fillAlpha = fillColor.getAlpha() / 255.0f; 295 fillOpacity = " fill-opacity=\"" + String.valueOf(fillAlpha) + "\""; 296 } 297 298 hexFillColor = colorToHexString(fillColor,false); 299 switch(affiliation) 300 { 301 case SymbolID.StandardIdentity_Affiliation_Friend: 302 case SymbolID.StandardIdentity_Affiliation_AssumedFriend: 303 defaultFillColor = "fill=\"#80E0FF\"";//friendly frame fill 304 break; 305 case SymbolID.StandardIdentity_Affiliation_Hostile_Faker: 306 defaultFillColor = "fill=\"#FF8080\"";//hostile frame fill 307 break; 308 case SymbolID.StandardIdentity_Affiliation_Suspect_Joker: 309 if(SymbolID.getVersion(symbolID) >= SymbolID.Version_2525E) 310 defaultFillColor = "fill=\"#FFE599\"";//suspect frame fill 311 else 312 defaultFillColor = "fill=\"#FF8080\"";//hostile frame fill 313 break; 314 case SymbolID.StandardIdentity_Affiliation_Unknown: 315 case SymbolID.StandardIdentity_Affiliation_Pending: 316 defaultFillColor = "fill=\"#FFFF80\"";//unknown frame fill 317 break; 318 case SymbolID.StandardIdentity_Affiliation_Neutral: 319 defaultFillColor = "fill=\"#AAFFAA\"";//neutral frame fill 320 break; 321 default: 322 defaultFillColor = "fill=\"#80E0FF\"";//friendly frame fill 323 break; 324 } 325 326 int fillIndex = returnSVG.lastIndexOf(defaultFillColor); 327 if(fillIndex != -1) 328 returnSVG = returnSVG.substring(0,fillIndex) + "fill=\"" + hexFillColor + "\"" + fillOpacity + returnSVG.substring(fillIndex + defaultFillColor.length()); 329 330 //returnSVG = returnSVG.replaceFirst(defaultFillColor, "fill=\"" + hexFillColor + "\"" + fillOpacity); 331 } 332 333 if(returnSVG != null) 334 return returnSVG; 335 else 336 return svg; 337 } 338 339 /** 340 * For Renderer Use Only 341 * Changes colors for single point control measures 342 * @param symbolID 343 * @param svg 344 * @param strokeColor hex value like "#FF0000"; 345 * @param fillColor hex value like "#FF0000"; 346 * @param isOutline true if this represents a thicker outline to render first beneath the normal symbol (the function must be called twice) 347 * @return SVG String 348 */ 349 public static String setSVGSPCMColors(String symbolID, String svg, Color strokeColor, Color fillColor, boolean isOutline) 350 { 351 String returnSVG = svg; 352 String hexStrokeColor = null; 353 String hexFillColor = null; 354 float strokeAlpha = 1; 355 float fillAlpha = 1; 356 String strokeOpacity = ""; 357 String fillOpacity = ""; 358 String strokeCapSquare = " stroke-linecap=\"square\""; 359 String strokeCapButt = " stroke-linecap=\"butt\""; 360 String strokeCapRound = " stroke-linecap=\"round\""; 361 362 int affiliation = SymbolID.getAffiliation(symbolID); 363 String defaultFillColor = null; 364 if(strokeColor != null) 365 { 366 if(strokeColor.getAlpha() != 255) 367 { 368 strokeAlpha = strokeColor.getAlpha() / 255.0f; 369 strokeOpacity = " stroke-opacity=\"" + strokeAlpha + "\""; 370 fillOpacity = " fill-opacity=\"" + strokeAlpha + "\""; 371 } 372 373 hexStrokeColor = colorToHexString(strokeColor,false); 374 String defaultStrokeColor = "#000000"; 375 if(symbolID.length()==5) 376 { 377 int mod = Integer.valueOf(symbolID.substring(2,4)); 378 if(mod >= 13) 379 defaultStrokeColor = "#00A651"; 380 381 } 382 //key terrain 383 if(symbolID.length() >= 20 && 384 SymbolUtilities.getBasicSymbolID(symbolID).equals("25132100") && 385 SymbolID.getVersion(symbolID) >= SymbolID.Version_2525E) 386 { 387 defaultStrokeColor = "#800080"; 388 } 389 returnSVG = returnSVG.replaceAll("stroke=\"" + defaultStrokeColor + "\"", "stroke=\"" + hexStrokeColor + "\"" + strokeOpacity); 390 returnSVG = returnSVG.replaceAll("fill=\"" + defaultStrokeColor + "\"", "fill=\"" + hexStrokeColor + "\"" + fillOpacity); 391 } 392 else 393 { 394 strokeColor = Color.BLACK; 395 } 396 397 if (isOutline) { 398 // Capture and scale stroke-widths to create outlines. Note that some stroke-widths are not integral numbers. 399 Pattern pattern = Pattern.compile("(stroke-width=\")(\\d+\\.?\\d*)\""); 400 Matcher m = pattern.matcher(svg); 401 TreeSet<String> strokeWidthStrings = new TreeSet<>(); 402 while (m.find()) { 403 strokeWidthStrings.add(m.group(0)); 404 } 405 // replace stroke width values in SVG from greatest to least to avoid unintended replacements 406 // TODO This might not actually sort strings from greatest to least stroke-width values because they're alphabetical 407 for (String target : strokeWidthStrings.descendingSet()) { 408 Pattern numPattern = Pattern.compile("\\d+\\.?\\d*"); 409 Matcher numMatcher = numPattern.matcher(target); 410 numMatcher.find(); 411 float f = Float.parseFloat(numMatcher.group(0)); 412 String replacement = "stroke-width=\"" + (f * OUTLINE_SCALING_FACTOR) + "\""; 413 returnSVG = returnSVG.replace(target, replacement); 414 } 415 416 // add stroke-width and stroke (color) to all groups 417 pattern = Pattern.compile("(<g)"); 418 m = pattern.matcher(svg); 419 TreeSet<String> groupStrings = new TreeSet<>(); 420 while (m.find()) { 421 groupStrings.add(m.group(0)); 422 } 423 for (String target : groupStrings) { 424 String replacement = target + strokeCapSquare + " stroke-width=\"" + (2.5f * OUTLINE_SCALING_FACTOR) + "\" stroke=\"#" + strokeColor.toHexString().substring(2) + "\" "; 425 returnSVG = returnSVG.replace(target, replacement); 426 } 427 428 } 429 else 430 { 431 /* 432 Pattern pattern = Pattern.compile("(font-size=\"\\d+\\.?\\d*)\""); 433 Matcher m = pattern.matcher(svg); 434 TreeSet<String> fontStrings = new TreeSet<>(); 435 while (m.find()) { 436 fontStrings.add(m.group(0)); 437 } 438 for (String target : fontStrings) { 439 String replacement = target + " fill=\"#" + strokeColor.toHexString().substring(2) + "\" "; 440 returnSVG = returnSVG.replace(target, replacement); 441 }//*/ 442 443 String replacement = " fill=\"" + colorToHexString(strokeColor,false) + "\" "; 444 returnSVG = returnSVG.replace("fill=\"#000000\"",replacement);//only replace black fills, leave white fills alone. 445 446 //In case there are lines that don't have stroke defined, apply stroke color to the top level group. 447 String topGroupTag = "<g id=\"" + SymbolUtilities.getBasicSymbolID(symbolID) + "\">";//<g id="25212902"> 448 String newGroupTag = "<g id=\"" + SymbolUtilities.getBasicSymbolID(symbolID) + "\" stroke=\"" + hexStrokeColor + "\"" + strokeOpacity + " " + replacement + ">"; 449 returnSVG = returnSVG.replace(topGroupTag,newGroupTag); 450 } 451 452 if(fillColor != null) 453 { 454 if(fillColor.getAlpha() != 255) 455 { 456 fillAlpha = fillColor.getAlpha() / 255.0f; 457 fillOpacity = " fill-opacity=\"" + fillAlpha + "\""; 458 } 459 460 hexFillColor = colorToHexString(fillColor,false); 461 defaultFillColor = "fill=\"#000000\""; 462 463 returnSVG = returnSVG.replaceAll(defaultFillColor, "fill=\"" + hexFillColor + "\"" + fillOpacity); 464 } 465 466 return returnSVG; 467 } 468 469 public static float findWidestStrokeWidth(String svg) { 470 Pattern pattern = Pattern.compile("(stroke-width=\")(\\d+\\.?\\d*)\""); 471 Matcher m = pattern.matcher(svg); 472 TreeSet<Float> strokeWidths = new TreeSet<>(); 473 while (m.find()) { 474 // Log.d("found stroke width", m.group(0)); 475 strokeWidths.add(Float.valueOf(m.group(2))); 476 } 477 478 float largest = 4.0f; 479 if (!strokeWidths.isEmpty()) { 480 largest = strokeWidths.descendingSet().first(); 481 } 482 return largest * OUTLINE_SCALING_FACTOR; 483 } 484 485 public static int findInstIndIndex(String svg) 486 { 487 int start = -1; 488 int stop = -1; 489 490 start = svg.indexOf("<rect"); 491 stop = svg.indexOf(">",start); 492 493 String rect = svg.substring(start,stop+1); 494 if(!rect.contains("fill"))//no set fill so it's the indicator 495 { 496 return start; 497 } 498 else //it's the next rect 499 { 500 start = svg.indexOf("<rect",stop); 501 } 502 503 return start; 504 } 505 506 public static SVGInfo scaleIcon(String symbolID, SVGInfo icon) 507 { 508 SVGInfo retVal= icon; 509 //safe square inside octagon: <rect x="220" y="310" width="170" height="170"/> 510 double maxSize = 170; 511 RectF bbox = null; 512 if(icon != null) 513 bbox = icon.getBbox(); 514 double length = 0; 515 if(bbox != null) 516 length = Math.max(bbox.width(),bbox.height()); 517 if(length < 100 && length > 0 && 518 SymbolID.getCommonModifier1(symbolID)==0 && 519 SymbolID.getCommonModifier2(symbolID)==0 && 520 SymbolID.getModifier1(symbolID)==0 && 521 SymbolID.getModifier2(symbolID)==0)//if largest side smaller than 100 and there are no section mods, make it bigger 522 { 523 double ratio = maxSize / length; 524 double transx = ((bbox.left + (bbox.width()/2)) * ratio) - (bbox.left + (bbox.width()/2)); 525 double transy = ((bbox.top + (bbox.height()/2)) * ratio) - (bbox.top + (bbox.height()/2)); 526 String transform = " transform=\"translate(-" + transx + ",-" + transy + ") scale(" + ratio + " " + ratio + ")\">"; 527 String svg = icon.getSVG(); 528 svg = svg.replaceFirst(">",transform); 529 RectF newBbox = RectUtilities.makeRectF((float)(bbox.left - transx),(float)(bbox.top - transy),(float)(bbox.width() * ratio), (float) (bbox.height() * ratio)); 530 retVal = new SVGInfo(icon.getID(),newBbox,svg); 531 } 532 return retVal; 533 } 534 535 public static int getDistanceBetweenPoints(Point pt1, Point pt2) 536 { 537 int distance = (int)(Math.sqrt(Math.pow((pt2.x - pt1.x) ,2) + Math.pow((pt2.y - pt1.y) ,2))); 538 return distance; 539 } 540 541 /** 542 * A starting point for calculating map scale. 543 * The User may prefer a different calculation depending on how their maps works. 544 * @param mapPixelWidth Width of your map in pixels 545 * @param eastLon East Longitude of your map 546 * @param westLon West Longitude of your map 547 * @return Map scale value to use in the RenderSymbol function {@link armyc2.c5isr.web.render.WebRenderer#RenderSymbol(String, String, String, String, String, String, double, String, Map, Map, int)} 548 */ 549 public static double calculateMapScale(int mapPixelWidth, double eastLon, double westLon) 550 { 551 return calculateMapScale(mapPixelWidth,eastLon,westLon,RendererSettings.getInstance().getDeviceDPI()); 552 } 553 554 /** 555 * A starting point for calculating map scale. 556 * The User may prefer a different calculation depending on how their maps works. 557 * @param mapPixelWidth Width of your map in pixels 558 * @param eastLon East Longitude of your map 559 * @param westLon West Longitude of your map 560 * @param dpi Dots Per Inch of your device 561 * @return Map scale value to use in the RenderSymbol function {@link armyc2.c5isr.web.render.WebRenderer#RenderSymbol(String, String, String, String, String, String, double, String, Map, Map, int)} 562 */ 563 public static double calculateMapScale(int mapPixelWidth, double eastLon, double westLon, int dpi) 564 { 565 double INCHES_PER_METER = 39.3700787; 566 double METERS_PER_DEG = 40075017.0 / 360.0; // Earth's circumference in meters / 360 degrees 567 568 try 569 { 570 double sizeSquare = Math.abs(eastLon - westLon); 571 if (sizeSquare > 180) 572 sizeSquare = 360 - sizeSquare; 573 574 // physical screen length (in meters) = pixels in screen / pixels per inch / inch per meter 575 double screenLength = mapPixelWidth / dpi / INCHES_PER_METER; 576 // meters on screen = degrees on screen * meters per degree 577 double metersOnScreen = sizeSquare * METERS_PER_DEG; 578 579 double scale = metersOnScreen/screenLength; 580 return scale; 581 } 582 catch(Exception exc) 583 { 584 ErrorLogger.LogException("RendererUtilities","calculateMapScale",exc,Level.WARNING); 585 } 586 return 0; 587 } 588 589 // Overloaded method to return non-outline symbols as normal. 590 public static String setSVGSPCMColors(String symbolID, String svg, Color strokeColor, Color fillColor) { 591 return setSVGSPCMColors(symbolID, svg, strokeColor, fillColor, false); 592 } 593}