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("RendererUtilities", "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("RendererUtilities", "getColorFromHexString", "Bad hex value: " + hexOriginal, Level.WARNING);
216            }
217            return null;
218        }
219        catch (Exception exc)
220        {
221            ErrorLogger.LogException("RendererUtilities", "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        int outlineSize = 15;
362
363        int affiliation = SymbolID.getAffiliation(symbolID);
364        String defaultFillColor = null;
365        if(strokeColor != null)
366        {
367            if(strokeColor.getAlpha() != 255)
368            {
369                strokeAlpha = strokeColor.getAlpha() / 255.0f;
370                strokeOpacity =  " stroke-opacity=\"" + strokeAlpha + "\"";
371                fillOpacity =  " fill-opacity=\"" + strokeAlpha + "\"";
372            }
373
374            hexStrokeColor = colorToHexString(strokeColor,false);
375            String defaultStrokeColor = "#000000";
376            if(symbolID.length()==5)
377            {
378                int mod = Integer.valueOf(symbolID.substring(2,4));
379                if(mod >= 13)
380                    defaultStrokeColor = "#00A651";
381
382            }
383
384            if(symbolID.length() >= 20)
385            {
386                if(SymbolUtilities.getBasicSymbolID(symbolID).equals("25132100") && //key terrain
387                        SymbolID.getVersion(symbolID) >= SymbolID.Version_2525E)
388                    defaultStrokeColor = "#800080";
389                else if(isOutline && SymbolUtilities.getBasicSymbolID(symbolID).startsWith("2535"))//space debris doesn't change color
390                    defaultStrokeColor = "black";
391            }
392            returnSVG = returnSVG.replaceAll("stroke=\"" + defaultStrokeColor + "\"", "stroke=\"" + hexStrokeColor + "\"" + strokeOpacity);
393            returnSVG = returnSVG.replaceAll("fill=\"" + defaultStrokeColor + "\"", "fill=\"" + hexStrokeColor + "\"" + fillOpacity);
394        }
395        else
396        {
397            strokeColor = Color.BLACK;
398        }
399
400        if (isOutline) {
401            //increase stroke-width so the white outline shows around the symbol
402            returnSVG = increaseStrokeWidth(returnSVG,(outlineSize));
403            //set the stroke color for the group so filled shapes without stokes get outlined as well.
404            returnSVG = returnSVG.replaceFirst("<g", "<g stroke=\"" + hexStrokeColor + "\" " + strokeOpacity + " stroke-linecap=\"square\"");
405
406        }
407        else
408        {
409            /*
410            Pattern pattern = Pattern.compile("(font-size=\"\\d+\\.?\\d*)\"");
411            Matcher m = pattern.matcher(svg);
412            TreeSet<String> fontStrings = new TreeSet<>();
413            while (m.find()) {
414                fontStrings.add(m.group(0));
415            }
416            for (String target : fontStrings) {
417                String replacement = target + " fill=\"#" + strokeColor.toHexString().substring(2) + "\" ";
418                returnSVG = returnSVG.replace(target, replacement);
419            }//*/
420
421            String replacement = " fill=\"" + colorToHexString(strokeColor,false) + "\" ";
422            returnSVG = returnSVG.replace("fill=\"#000000\"",replacement);//only replace black fills, leave white fills alone.
423
424            //In case there are lines that don't have stroke defined, apply stroke color to the top level group.
425            String topGroupTag = "<g id=\"" + SymbolUtilities.getBasicSymbolID(symbolID) + "\">";//<g id="25212902">
426            String newGroupTag = "<g id=\"" + SymbolUtilities.getBasicSymbolID(symbolID) + "\" stroke=\"" + hexStrokeColor + "\"" + strokeOpacity + " " + replacement + ">";
427            returnSVG = returnSVG.replace(topGroupTag,newGroupTag);
428        }
429
430        if(fillColor != null)
431        {
432            if(fillColor.getAlpha() != 255)
433            {
434                fillAlpha = fillColor.getAlpha() / 255.0f;
435                fillOpacity =  " fill-opacity=\"" + fillAlpha + "\"";
436            }
437
438            hexFillColor = colorToHexString(fillColor,false);
439            defaultFillColor = "fill=\"#000000\"";
440
441            returnSVG = returnSVG.replaceAll(defaultFillColor, "fill=\"" + hexFillColor + "\"" + fillOpacity);
442        }
443
444        return returnSVG;
445    }
446
447    /**
448     * Sets SVG stroke-dasharray when action points are in planned status
449     * @param symbolID
450     * @param siIcon
451     * @return
452     */
453    public static SVGInfo setAffiliationDashArray(String symbolID, SVGInfo siIcon)
454    {
455        String svg = siIcon.getSVG();
456        int status = SymbolID.getStatus(symbolID);
457        int aff = SymbolID.getAffiliation(symbolID);
458        SVGInfo returnVal = siIcon;
459        if(status == SymbolID.Status_Planned_Anticipated_Suspect)
460        {
461            if(SymbolUtilities.isActionPoint(symbolID))
462            {
463                svg = svg.replaceFirst("<rect ","<rect stroke-dasharray=\"20 19\" ");
464                svg = svg.replaceFirst("<polygon ","<polygon stroke-dasharray=\"20 20\" ");
465                returnVal = new SVGInfo(siIcon.getID(),siIcon.getBbox(), svg);
466            }
467        }
468        /*else if(aff == SymbolID.StandardIdentity_Affiliation_Pending ||
469                aff == SymbolID.StandardIdentity_Affiliation_AssumedFriend ||
470                aff == SymbolID.StandardIdentity_Affiliation_Suspect_Joker)
471        {
472            //Dot pattern if Control Measures use it?
473        }//*/
474
475        return returnVal;
476    }
477    public static float findWidestStrokeWidth(String svg) {
478        Pattern pattern = Pattern.compile("(stroke-width=\")(\\d+\\.?\\d*)\"");
479        Matcher m = pattern.matcher(svg);
480        TreeSet<Float> strokeWidths = new TreeSet<>();
481        while (m.find()) {
482            // Log.d("found stroke width", m.group(0));
483            strokeWidths.add(Float.valueOf(m.group(2)));
484        }
485
486        float largest = 4.0f;
487        if (!strokeWidths.isEmpty()) {
488            largest = strokeWidths.descendingSet().first();
489        }
490        return largest * OUTLINE_SCALING_FACTOR;
491    }
492
493    public static int findInstIndIndex(String svg)
494    {
495        int start = -1;
496        int stop = -1;
497
498        start = svg.indexOf("<rect");
499        stop = svg.indexOf(">",start);
500
501        String rect = svg.substring(start,stop+1);
502        if(!rect.contains("fill"))//no set fill so it's the indicator
503        {
504            return start;
505        }
506        else //it's the next rect
507        {
508            start = svg.indexOf("<rect",stop);
509        }
510
511        return start;
512    }
513
514    public static SVGInfo scaleIcon(String symbolID, SVGInfo icon)
515    {
516        SVGInfo retVal= icon;
517        //safe square inside octagon:  <rect x="220" y="310" width="170" height="170"/>
518        double maxSize = 170;
519        RectF bbox = null;
520        if(icon != null)
521            bbox = icon.getBbox();
522        double length = 0;
523        if(bbox != null)
524        {
525            length = Math.max(bbox.width(), bbox.height());
526            //adjust max size for narrow, tall icons
527            if(bbox.width() < 60 && bbox.height() > 90)
528                maxSize = 200;
529
530            if(SVGLookup.getMainIconID(symbolID).length() == 8 && length < 145 && length > 0 &&
531                    bbox.height() < 105 &&
532                    SymbolID.getCommonModifier1(symbolID)==0 &&
533                    SymbolID.getCommonModifier2(symbolID)==0 &&
534                    SymbolID.getModifier1(symbolID)==0 &&
535                    SymbolID.getModifier2(symbolID)==0)//if largest side smaller than 145 and there are no section mods, make it bigger
536            {
537                double ratio = maxSize / length;
538                double transx = ((bbox.left + (bbox.width()/2)) * ratio) - (bbox.left + (bbox.width()/2));
539                double transy = ((bbox.top + (bbox.height()/2)) * ratio) - (bbox.top + (bbox.height()/2));
540                String transform = " transform=\"translate(-" + transx + ",-" + transy + ") scale(" + ratio + " " + ratio + ")\">";
541                String svg = icon.getSVG();
542                svg = svg.replaceFirst(">",transform);
543                RectF newBbox = RectUtilities.makeRectF((float)(bbox.left - transx),(float)(bbox.top - transy),(float)(bbox.width() * ratio), (float) (bbox.height() * ratio));
544                retVal = new SVGInfo(icon.getID(),newBbox,svg);
545            }
546        }
547
548        return retVal;
549    }
550
551    /**
552     * Takes an SVG string and increases all stroke-width values by the increaseBy value.
553     * @param svgString The raw SVG content.
554     * @param increaseBy the number to add to the current stroke value
555     * @return The modified SVG content.
556     */
557    public static String increaseStrokeWidth(String svgString, int increaseBy) {
558        Pattern pattern = Pattern.compile("stroke-width=\"([^\"]+)\"");
559        Matcher matcher = pattern.matcher(svgString);
560        StringBuilder sb = new StringBuilder();
561        int lastEnd = 0;
562
563        while (matcher.find()) {
564            // 1. Append everything from the last match up to the current match
565            sb.append(svgString.substring(lastEnd, matcher.start()));
566
567            String replacement;
568            try {
569                // 2. Calculate the new value
570                double currentValue = Double.parseDouble(matcher.group(1));
571                double newValue = currentValue + increaseBy;
572                String formattedValue = (newValue == (long) newValue)
573                        ? String.valueOf((long) newValue)
574                        : String.valueOf(newValue);
575
576                replacement = "stroke-width=\"" + formattedValue + "\"";
577            } catch (NumberFormatException e) {
578                // Fallback to original text if not a number
579                replacement = matcher.group(0);
580            }
581
582            // 3. Append the replacement and update our position
583            sb.append(replacement);
584            lastEnd = matcher.end();
585        }
586
587        // 4. Append any remaining text after the last match
588        sb.append(svgString.substring(lastEnd));
589        int firstGroup = sb.indexOf("<g");
590        sb.replace(firstGroup, firstGroup+2,"<g stroke-width=\"" + increaseBy + "\" ");
591        return sb.toString();
592    }
593
594    public static int getDistanceBetweenPoints(Point pt1, Point pt2)
595    {
596        int distance = (int)(Math.sqrt(Math.pow((pt2.x - pt1.x) ,2) + Math.pow((pt2.y - pt1.y) ,2)));
597        return distance;
598    }
599
600    /**
601     * A starting point for calculating map scale.
602     * The User may prefer a different calculation depending on how their maps works.
603     * @param mapPixelWidth Width of your map in pixels
604     * @param eastLon East Longitude of your map
605     * @param westLon West Longitude of your map
606     * @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)}
607     */
608    public static double calculateMapScale(int mapPixelWidth, double eastLon, double westLon)
609    {
610        return calculateMapScale(mapPixelWidth,eastLon,westLon,RendererSettings.getInstance().getDeviceDPI());
611    }
612
613    /**
614     * A starting point for calculating map scale.
615     * The User may prefer a different calculation depending on how their maps works.
616     * @param mapPixelWidth Width of your map in pixels
617     * @param eastLon East Longitude of your map
618     * @param westLon West Longitude of your map
619     * @param dpi Dots Per Inch of your device
620     * @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)}
621     */
622    public static double calculateMapScale(int mapPixelWidth, double eastLon, double westLon, int dpi)
623    {
624        double INCHES_PER_METER = 39.3700787;
625        double METERS_PER_DEG = 40075017.0 / 360.0; // Earth's circumference in meters / 360 degrees
626
627        try
628        {
629            double sizeSquare = Math.abs(eastLon - westLon);
630            if (sizeSquare > 180)
631                sizeSquare = 360 - sizeSquare;
632
633            // physical screen length (in meters) = pixels in screen / pixels per inch / inch per meter
634            double screenLength = mapPixelWidth / dpi / INCHES_PER_METER;
635            // meters on screen = degrees on screen * meters per degree
636            double metersOnScreen = sizeSquare * METERS_PER_DEG;
637
638            double scale = metersOnScreen/screenLength;
639            return scale;
640        }
641        catch(Exception exc)
642        {
643            ErrorLogger.LogException("RendererUtilities","calculateMapScale",exc,Level.WARNING);
644        }
645        return 0;
646    }
647
648    // Overloaded method to return non-outline symbols as normal.
649    public static String setSVGSPCMColors(String symbolID, String svg, Color strokeColor, Color fillColor) {
650        return setSVGSPCMColors(symbolID, svg, strokeColor, fillColor, false);
651    }
652}