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}