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