001package armyc2.c5isr.renderer;
002
003import android.graphics.Bitmap;
004import android.graphics.Bitmap.Config;
005import android.graphics.Canvas;
006import android.graphics.Paint;
007import android.graphics.Paint.FontMetrics;
008import android.graphics.Point;
009import android.graphics.PointF;
010import android.graphics.Rect;
011import android.graphics.RectF;
012import android.util.Log;
013import android.util.LruCache;
014
015import com.caverock.androidsvg.SVG;
016
017import java.util.HashMap;
018import java.util.Map;
019
020import armyc2.c5isr.renderer.utilities.Color;
021import armyc2.c5isr.renderer.utilities.DrawRules;
022import armyc2.c5isr.renderer.utilities.ErrorLogger;
023import armyc2.c5isr.renderer.utilities.ImageInfo;
024import armyc2.c5isr.renderer.utilities.MSInfo;
025import armyc2.c5isr.renderer.utilities.MSLookup;
026import armyc2.c5isr.renderer.utilities.MilStdAttributes;
027import armyc2.c5isr.renderer.utilities.Modifiers;
028import armyc2.c5isr.renderer.utilities.RectUtilities;
029import armyc2.c5isr.renderer.utilities.RendererSettings;
030import armyc2.c5isr.renderer.utilities.RendererUtilities;
031import armyc2.c5isr.renderer.utilities.SVGInfo;
032import armyc2.c5isr.renderer.utilities.SVGLookup;
033import armyc2.c5isr.renderer.utilities.SettingsChangedEvent;
034import armyc2.c5isr.renderer.utilities.SettingsChangedEventListener;
035import armyc2.c5isr.renderer.utilities.SymbolDimensionInfo;
036import armyc2.c5isr.renderer.utilities.SymbolID;
037import armyc2.c5isr.renderer.utilities.SymbolUtilities;
038
039public class SinglePointRenderer implements SettingsChangedEventListener
040{
041
042    private final String TAG = "SinglePointRenderer";
043    private static SinglePointRenderer _instance = null;
044
045    private final Object _SinglePointCacheMutex = new Object();
046    private final Object _UnitCacheMutex = new Object();
047
048    private final Object _modifierFontMutex = new Object();
049    private Paint _modifierFont = new Paint();
050    private Paint _modifierOutlineFont = new Paint();
051    private float _modifierDescent = 2;
052    private float _modifierFontHeight = 10;
053    private int _deviceDPI = 72;
054
055    //private LruCache<String, ImageInfo> _unitCache = new LruCache<String, ImageInfo>(15);
056    //private LruCache<String, ImageInfo> _tgCache = new LruCache<String, ImageInfo>(7);
057    private LruCache<String, ImageInfo> _unitCache = new LruCache<String, ImageInfo>(1024);
058    private LruCache<String, ImageInfo> _tgCache = new LruCache<String, ImageInfo>(1024);
059    private final int maxMemory = (int) (Runtime.getRuntime().maxMemory());// / 1024);
060    private int cacheSize = 5;//RendererSettings.getInstance().getCacheSize() / 2;
061    private int maxCachedEntrySize = cacheSize / 5;
062    private boolean cacheEnabled = RendererSettings.getInstance().getCacheEnabled();
063
064    private SinglePointRenderer()
065    {
066        RendererSettings.getInstance().addEventListener(this);
067        
068        //get modifier font values.
069        onSettingsChanged(new SettingsChangedEvent(SettingsChangedEvent.EventType_FontChanged));
070        //set cache
071        onSettingsChanged(new SettingsChangedEvent(SettingsChangedEvent.EventType_CacheSizeChanged));
072
073    }
074
075    public static synchronized SinglePointRenderer getInstance()
076    {
077        if (_instance == null)
078        {
079            _instance = new SinglePointRenderer();
080        }
081
082        return _instance;
083    }
084
085    /**
086     *
087     * @param symbolID
088     * @param modifiers
089     * @return
090     */
091    public ImageInfo RenderUnit(String symbolID, Map<String,String> modifiers, Map<String,String> attributes)
092    {
093        Color lineColor = SymbolUtilities.getLineColorOfAffiliation(symbolID);
094        Color fillColor = SymbolUtilities.getFillColorOfAffiliation(symbolID);
095        Color iconColor = null;
096
097        int alpha = -1;
098
099
100        //SVG values
101        String frameID = null;
102        String iconID = null;
103        String mod1ID = null;
104        String mod2ID = null;
105        SVGInfo siFrame = null;
106        SVGInfo siIcon = null;
107        SVGInfo siMod1 = null;
108        SVGInfo siMod2 = null;
109        SVG mySVG = null;
110        int top = 0;
111        int left = 0;
112        int width = 0;
113        int height = 0;
114        String svgStart = null;
115        String strSVG = null;
116        String strSVGFrame = null;
117
118
119        Rect symbolBounds = null;
120        Rect fullBounds = null;
121        Bitmap fullBMP = null;
122
123        boolean hasDisplayModifiers = false;
124        boolean hasTextModifiers = false;
125
126        int pixelSize = -1;
127        boolean keepUnitRatio = true;
128        boolean icon = false;
129        boolean noFrame = false;
130
131        int ver = SymbolID.getVersion(symbolID);
132
133        // <editor-fold defaultstate="collapsed" desc="Parse Attributes">
134        try
135        {
136
137            if (attributes.containsKey(MilStdAttributes.PixelSize))
138            {
139                pixelSize = Integer.parseInt(attributes.get(MilStdAttributes.PixelSize));
140            }
141            else
142            {
143                pixelSize = RendererSettings.getInstance().getDefaultPixelSize();
144            }
145
146            if (attributes.containsKey(MilStdAttributes.KeepUnitRatio))
147            {
148                keepUnitRatio = Boolean.parseBoolean(attributes.get(MilStdAttributes.KeepUnitRatio));
149            }
150
151            if (attributes.containsKey(MilStdAttributes.DrawAsIcon))
152            {
153                icon = Boolean.parseBoolean(attributes.get(MilStdAttributes.DrawAsIcon));
154            }
155
156            if (icon)//icon won't show modifiers or display icons
157            {
158                //TODO: symbolID modifications as necessary
159                keepUnitRatio = false;
160                hasDisplayModifiers = false;
161                hasTextModifiers = false;
162                //symbolID = symbolID.substring(0, 10) + "-----";
163            }
164            else
165            {
166                hasDisplayModifiers = ModifierRenderer.hasDisplayModifiers(symbolID, modifiers);
167                hasTextModifiers = ModifierRenderer.hasTextModifiers(symbolID, modifiers);
168            }
169
170            if (attributes.containsKey(MilStdAttributes.LineColor))
171            {
172                lineColor = new Color(attributes.get(MilStdAttributes.LineColor));
173            }
174            if (attributes.containsKey(MilStdAttributes.FillColor))
175            {
176                fillColor = new Color(attributes.get(MilStdAttributes.FillColor));
177            }
178            if (attributes.containsKey(MilStdAttributes.IconColor))
179            {
180                iconColor = new Color(attributes.get(MilStdAttributes.IconColor));
181            }//*/
182            if (attributes.containsKey(MilStdAttributes.Alpha))
183            {
184                alpha = Integer.parseInt(attributes.get(MilStdAttributes.Alpha));
185            }
186
187        }
188        catch (Exception excModifiers)
189        {
190            ErrorLogger.LogException("MilStdIconRenderer", "RenderUnit", excModifiers);
191        }
192        // </editor-fold>
193
194        try
195        {
196
197            ImageInfo ii = null;
198            String key = makeCacheKey(symbolID, lineColor.toInt(), fillColor.toInt(), String.valueOf(iconColor),pixelSize, keepUnitRatio, false);
199
200            //see if it's in the cache
201            if(_unitCache != null) {
202                ii = _unitCache.get(key);
203                //safety check in case bitmaps are getting recycled while still in the LRU cache
204                if (ii != null && ii.getImage() != null && ii.getImage().isRecycled()) {
205                    synchronized (_UnitCacheMutex) {
206                        _unitCache.remove(key);
207                        ii = null;
208                    }
209                }
210            }
211            //if not, generate symbol
212            if (ii == null)//*/
213            {
214                int version = SymbolID.getVersion(symbolID);
215                //Get SVG pieces of symbol
216                frameID = SVGLookup.getFrameID(symbolID);
217                iconID = SVGLookup.getMainIconID(symbolID);
218                mod1ID = SVGLookup.getMod1ID(symbolID);
219                mod2ID = SVGLookup.getMod2ID(symbolID);
220                siFrame = SVGLookup.getInstance().getSVGLInfo(frameID, version);
221                siIcon = SVGLookup.getInstance().getSVGLInfo(iconID, version);
222
223                if(siFrame == null)
224                {
225                    frameID = SVGLookup.getFrameID(SymbolUtilities.reconcileSymbolID(symbolID));
226                    siFrame = SVGLookup.getInstance().getSVGLInfo(frameID, version);
227                    if(siFrame == null)//still no match, get unknown frame
228                    {
229                        frameID = SVGLookup.getFrameID(SymbolID.setSymbolSet(symbolID,SymbolID.SymbolSet_Unknown));
230                        siFrame = SVGLookup.getInstance().getSVGLInfo(frameID, version);
231                    }
232                }
233
234                if(siIcon == null)
235                {
236                        if(iconID.substring(2,8).equals("000000")==false && MSLookup.getInstance().getMSLInfo(symbolID) == null)
237                            siIcon = SVGLookup.getInstance().getSVGLInfo("98100000", version);//inverted question mark
238                        else if(SymbolID.getSymbolSet(symbolID) == SymbolID.SymbolSet_Unknown)
239                            siIcon = SVGLookup.getInstance().getSVGLInfo("00000000", version);//question mark
240                }
241
242                if(RendererSettings.getInstance().getScaleMainIcon())
243                    siIcon = RendererUtilities.scaleIcon(symbolID,siIcon);
244
245                siMod1 = SVGLookup.getInstance().getSVGLInfo(mod1ID, version);
246                siMod2 = SVGLookup.getInstance().getSVGLInfo(mod2ID, version);
247                top = Math.round(siFrame.getBbox().top);
248                left = Math.round(siFrame.getBbox().left);
249                width = Math.round(siFrame.getBbox().width());
250                height = Math.round(siFrame.getBbox().height());
251                if(siFrame.getBbox().bottom > 400)
252                    svgStart = "<svg xmlns:svg=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 612 792\">";
253                else
254                    svgStart = "<svg xmlns:svg=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 400 400\">";
255
256                //update line and fill color of frame SVG
257                if(lineColor != null || fillColor != null)
258                    strSVGFrame = RendererUtilities.setSVGFrameColors(symbolID,siFrame.getSVG(),lineColor,fillColor);
259                else
260                    strSVGFrame = siFrame.getSVG();
261
262                if(frameID.equals("octagon"))//for the 1 unit symbol that doesn't have a frame: 30 + 15000
263                {
264                    noFrame = true;
265                    strSVGFrame = strSVGFrame.replaceFirst("<g id=\"octagon\">", "<g id=\"octagon\" display=\"none\">");
266                }
267
268
269                //get SVG dimensions and target dimensions
270                symbolBounds = RectUtilities.makeRect(left,top,width,height);
271                Rect rect = new Rect(symbolBounds);
272                float ratio = -1;
273
274                if (pixelSize > 0 && keepUnitRatio == true)
275                {
276                    float heightRatio = SymbolUtilities.getUnitRatioHeight(symbolID);
277                    float widthRatio = SymbolUtilities.getUnitRatioWidth(symbolID);
278
279                    if(noFrame == true)//using octagon with display="none" as frame for a 1x1 shape
280                    {
281                        heightRatio = 1.0f;
282                        widthRatio = 1.0f;
283                    }
284
285                    if (heightRatio > widthRatio)
286                    {
287                        pixelSize = (int) ((pixelSize / 1.5f) * heightRatio);
288                    }
289                    else
290                    {
291                        pixelSize = (int) ((pixelSize / 1.5f) * widthRatio);
292                    }
293                }
294                if (pixelSize > 0)
295                {
296                    float p = pixelSize;
297                    float h = rect.height();
298                    float w = rect.width();
299
300                    ratio = Math.min((p / h), (p / w));
301
302                    symbolBounds = RectUtilities.makeRect(0f, 0f, w * ratio, h * ratio);
303                }
304
305                //center of octagon is the center of all unit symbols
306                Point centerOctagon = new Point(306, 396);
307                centerOctagon.offset(-left,-top);//offset for the symbol bounds x,y
308                //scale center point by same ratio as the symbol
309                centerOctagon = new Point((int)(centerOctagon.x * ratio), (int)(centerOctagon.y * ratio));
310
311                //set centerpoint of the image
312                Point centerPoint = centerOctagon;
313                Point centerCache = new Point(centerOctagon.x, centerOctagon.y);
314
315                //y offset to get centerpoint so we set back to zero when done.
316                symbolBounds.top = 0;
317
318                //Create destination BMP
319                Bitmap bmp = Bitmap.createBitmap(symbolBounds.width(), symbolBounds.height(), Config.ARGB_8888);
320                Canvas canvas = new Canvas(bmp);
321
322                //draw unit from SVG
323                StringBuilder sb = new StringBuilder();
324                sb.append(svgStart);
325
326                if(strSVGFrame != null)
327                    sb.append(strSVGFrame);
328
329                String color = "";
330                String strokeFill = "";
331                if(iconColor != null)
332                {
333                    //make sure string is properly formatted.
334                    color = RendererUtilities.colorToHexString(iconColor,false);
335                    if(color != null && color != "#000000" && color != "")
336                        strokeFill = " fill=\"" + color + "\" ";
337                    else
338                        color = null;
339                }
340                String unit = "<g" + strokeFill + ">";
341                if (siIcon != null)
342                    unit += (siIcon.getSVG());
343                if (siMod1 != null)
344                    unit += (siMod1.getSVG());
345                if (siMod2 != null)
346                    unit += (siMod2.getSVG());
347                if(iconColor != null && color != null && color != "")
348                    unit = unit.replaceAll("#000000",color);
349                sb.append(unit + "</g>");
350
351                sb.append("</svg>");
352
353                strSVG = sb.toString();
354
355                mySVG = SVG.getFromString(strSVG);
356                mySVG.setDocumentViewBox(left,top,width,height);
357                mySVG.renderToCanvas(canvas);
358
359
360                //adjust centerpoint for HQStaff if present
361                if (SymbolUtilities.isHQ(symbolID))
362                {
363                    PointF point1 = new PointF();
364                    PointF point2 = new PointF();
365                    int affiliation = SymbolID.getAffiliation(symbolID);
366                    int ss = SymbolID.getStandardIdentity(symbolID);
367                    if (affiliation == SymbolID.StandardIdentity_Affiliation_Friend
368                            || affiliation == SymbolID.StandardIdentity_Affiliation_AssumedFriend
369                            || affiliation == SymbolID.StandardIdentity_Affiliation_Neutral
370                            || ss == 15 || ss == 16)//exercise joker or faker
371                    {
372                        point1.x = (symbolBounds.left);
373                        point1.y = symbolBounds.top + (symbolBounds.height());
374                        point2.x = point1.x;
375                        point2.y = point1.y + symbolBounds.height();
376                    }
377                    else
378                    {
379                        point1.x = (symbolBounds.left + 1);
380                        point1.y = symbolBounds.top + (symbolBounds.height() / 2);
381                        point2.x = point1.x;
382                        point2.y = point1.y + symbolBounds.height();
383                    }
384                    centerPoint = new Point((int) point2.x, (int) point2.y);
385                }
386
387                ii = new ImageInfo(bmp, centerPoint, symbolBounds);
388
389                if(cacheEnabled && icon == false && bmp.getAllocationByteCount() <= maxCachedEntrySize)
390                {
391                    synchronized (_UnitCacheMutex)
392                    {
393                        if(_unitCache != null && _unitCache.get(key) == null)
394                            _unitCache.put(key, new ImageInfo(bmp, new Point(centerCache), new Rect(symbolBounds)));
395                    }
396                }
397
398                /*if(icon == false && pixelSize <= 100)
399                {
400                    _unitCache.put(key, new ImageInfo(bmp, new Point(centerCache), new Rect(symbolBounds)));
401                }//*/
402            }
403
404            ImageInfo iiNew = null;
405            SymbolDimensionInfo sdiTemp = null;
406            ////////////////////////////////////////////////////////////////////
407            //process display modifiers
408            if (hasDisplayModifiers)
409            {
410                sdiTemp = ModifierRenderer.processUnitDisplayModifiers( ii, symbolID, modifiers, hasTextModifiers, attributes);
411                iiNew = (sdiTemp instanceof ImageInfo ? (ImageInfo)sdiTemp : null);
412                sdiTemp = null;
413            }
414
415            if (iiNew != null)
416            {
417                ii = iiNew;
418            }
419            iiNew = null;
420
421            //process text modifiers
422            if (hasTextModifiers)
423            {
424                sdiTemp = ModifierRenderer.processSPTextModifiers(ii, symbolID, modifiers, attributes);
425            }
426
427            iiNew = (sdiTemp instanceof ImageInfo ? (ImageInfo)sdiTemp : null);
428            if (iiNew != null)
429            {
430                ii = iiNew;
431            }
432            iiNew = null;
433
434            ii = (ImageInfo) ModifierRenderer.processSpeedLeader(ii,symbolID,modifiers,attributes);
435
436            //cleanup///////////////////////////////////////////////////////////
437            //bmp.recycle();
438            symbolBounds = null;
439            fullBMP = null;
440            fullBounds = null;
441            mySVG = null;
442            ////////////////////////////////////////////////////////////////////
443
444            if (icon == true)
445            {
446                return ii.getSquareImageInfo();
447            }
448            else
449            {
450                return ii;
451            }
452
453        }
454        catch (Exception exc)
455        {
456            ErrorLogger.LogException("MilStdIconRenderer", "RenderUnit", exc);
457        }
458        return null;
459    }
460
461    /**
462     *
463     * @param symbolID
464     * @param modifiers
465     * @return
466     */
467    @SuppressWarnings("unused")
468    public ImageInfo RenderSP(String symbolID, Map<String,String> modifiers, Map<String,String> attributes)
469    {
470        ImageInfo temp = null;
471        String basicSymbolID = null;
472
473        Color lineColor = SymbolUtilities.getDefaultLineColor(symbolID);
474        Color fillColor = null;//SymbolUtilities.getFillColorOfAffiliation(symbolID);
475
476        int alpha = -1;
477
478
479        //SVG rendering variables
480        MSInfo msi = null;
481        String iconID = null;
482        SVGInfo siIcon = null;
483        String mod1ID = null;
484        SVGInfo siMod1 = null;
485        int top = 0;
486        int left = 0;
487        int width = 0;
488        int height = 0;
489        String svgStart = null;
490        String strSVG = null;
491        SVG mySVG = null;
492
493        float ratio = 0;
494
495        Rect symbolBounds = null;
496        RectF fullBounds = null;
497        Bitmap fullBMP = null;
498
499        boolean drawAsIcon = false;
500        int pixelSize = -1;
501        boolean keepUnitRatio = true;
502        boolean hasDisplayModifiers = false;
503        boolean hasTextModifiers = false;
504        boolean drawCustomOutline = false;
505
506        msi = MSLookup.getInstance().getMSLInfo(symbolID);
507        int ss = SymbolID.getSymbolSet(symbolID);
508        int ec = SymbolID.getEntityCode(symbolID);
509        int mod1 = 0;
510        int drawRule = 0;
511        if(msi!=null){drawRule = msi.getDrawRule();}
512        boolean hasAPFill = false;
513        if(RendererSettings.getInstance().getActionPointDefaultFill()) {
514            if (SymbolUtilities.isActionPoint(symbolID) || //action points
515                    drawRule == DrawRules.POINT10 || //Sonobuoy
516                    ec == 180100 || ec == 180200 || ec == 180400) //ACP, CCP, PUP
517            {
518                if (SymbolID.getSymbolSet(symbolID) == SymbolID.SymbolSet_ControlMeasure) {
519                    lineColor = Color.BLACK;
520                    hasAPFill = true;
521                }
522            }
523        }
524
525        try
526        {
527            if (modifiers == null)
528            {
529                modifiers = new HashMap<>();
530            }
531
532
533            //get symbol info
534
535            msi = MSLookup.getInstance().getMSLInfo(symbolID);
536
537            if (msi == null)//if lookup fails, fix code/use unknown symbol code.
538            {
539                //TODO: change symbolID to Action Point with bad symbolID  in the T or H field
540            }
541
542            /* Fills built into SVG
543            if (SymbolUtilities.hasDefaultFill(symbolID))
544            {
545                fillColor = SymbolUtilities.getFillColorOfAffiliation(symbolID);
546            }
547            if (SymbolUtilities.isTGSPWithFill(symbolID))
548            {
549                fillID = SymbolUtilities.getTGFillSymbolCode(symbolID);
550                if (fillID != null)
551                {
552                    charFillIndex = SinglePointLookup.getInstance().getCharCodeFromSymbol(fillID, symStd);
553                }
554            }
555            else if (SymbolUtilities.isWeatherSPWithFill(symbolID))
556            {
557                charFillIndex = charFrameIndex + 1;
558                fillColor = SymbolUtilities.getFillColorOfWeather(symbolID);
559
560            }//*/
561
562            if (attributes != null)
563            {
564                if (attributes.containsKey(MilStdAttributes.KeepUnitRatio))
565                {
566                    keepUnitRatio = Boolean.parseBoolean(attributes.get(MilStdAttributes.KeepUnitRatio));
567                }
568
569                if (attributes.containsKey(MilStdAttributes.LineColor))
570                {
571                    lineColor = RendererUtilities.getColorFromHexString(attributes.get(MilStdAttributes.LineColor));
572                }
573
574                if (attributes.containsKey(MilStdAttributes.FillColor))
575                {
576                    fillColor = RendererUtilities.getColorFromHexString(attributes.get(MilStdAttributes.FillColor));
577                }
578
579                if (attributes.containsKey(MilStdAttributes.Alpha))
580                {
581                    alpha = Integer.parseInt(attributes.get(MilStdAttributes.Alpha));
582                }
583
584                if (attributes.containsKey(MilStdAttributes.DrawAsIcon))
585                {
586                    drawAsIcon = Boolean.parseBoolean(attributes.get(MilStdAttributes.DrawAsIcon));
587                }
588
589                if (attributes.containsKey(MilStdAttributes.PixelSize))
590                {
591                    pixelSize = Integer.parseInt(attributes.get(MilStdAttributes.PixelSize));
592                }
593                else
594                {
595                    pixelSize = RendererSettings.getInstance().getDefaultPixelSize();
596                }
597                if(keepUnitRatio == true && msi.getSymbolSet() == SymbolID.SymbolSet_ControlMeasure && msi.getGeometry().equalsIgnoreCase("point"))
598                {
599                    if(msi.getDrawRule() == DrawRules.POINT1)//Action Points
600                        pixelSize = (int)Math.ceil((pixelSize/1.5f) * 2.0f);
601                    else
602                        pixelSize = (int)Math.ceil((pixelSize/1.5f) * 1.2f);
603                }
604
605                if(attributes.containsKey(MilStdAttributes.OutlineSymbol))
606                    drawCustomOutline = Boolean.parseBoolean(attributes.get(MilStdAttributes.OutlineSymbol));
607                else
608                    drawCustomOutline = RendererSettings.getInstance().getOutlineSPControlMeasures();
609
610                if(SymbolUtilities.isMultiPoint(symbolID))
611                    drawCustomOutline=false;//icon previews for multipoints do not need outlines since they shouldn't be on the map
612            }
613
614            if (drawAsIcon)//icon won't show modifiers or display icons
615            {
616                keepUnitRatio = false;
617                hasDisplayModifiers = false;
618                hasTextModifiers = false;
619                drawCustomOutline = false;
620            }
621            else
622            {
623                hasDisplayModifiers = ModifierRenderer.hasDisplayModifiers(symbolID, modifiers);
624                hasTextModifiers = ModifierRenderer.hasTextModifiers(symbolID, modifiers);
625            }
626
627            //Check if we need to set 'N' to "ENY"
628            int aff = SymbolID.getAffiliation(symbolID);
629            //int ss = msi.getSymbolSet();
630            if (ss == SymbolID.SymbolSet_ControlMeasure &&
631                    (aff == SymbolID.StandardIdentity_Affiliation_Hostile_Faker ||
632                 aff == SymbolID.StandardIdentity_Affiliation_Suspect_Joker ) &&
633                    modifiers.containsKey(Modifiers.N_HOSTILE) &&
634                    drawAsIcon == false)
635            {
636                modifiers.put(Modifiers.N_HOSTILE, "ENY");
637            }
638
639        }
640        catch (Exception excModifiers)
641        {
642            ErrorLogger.LogException("MilStdIconRenderer", "RenderSP", excModifiers);
643        }
644
645        try
646        {
647            ImageInfo ii = null;
648            int intFill = -1;
649            if (fillColor != null)
650            {
651                intFill = fillColor.toInt();
652            }
653
654
655            if(msi.getSymbolSet() != SymbolID.SymbolSet_ControlMeasure)
656                lineColor = Color.BLACK;//color isn't black but should be fine for weather since colors can't be user defined.
657
658
659
660            if (SymbolID.getSymbolSet(symbolID)==SymbolID.SymbolSet_ControlMeasure && SymbolID.getEntityCode(symbolID) == 270701)//static depiction
661            {
662                //add mine fill to image
663                mod1 = SymbolID.getModifier1(symbolID);
664                if (!(mod1 >= 13 && mod1 <= 50))
665                    symbolID = SymbolID.setModifier1(symbolID, 13);
666            }
667
668            String key = makeCacheKey(symbolID, lineColor.toInt(), intFill, pixelSize, keepUnitRatio, drawCustomOutline);
669
670            //see if it's in the cache
671            if(_tgCache != null) {
672                ii = _tgCache.get(key);
673                //safety check in case bitmaps are getting recycled while still in the LRU cache
674                if (ii != null && ii.getImage() != null && ii.getImage().isRecycled()) {
675                    synchronized (_SinglePointCacheMutex) {
676                        _tgCache.remove(key);
677                        ii = null;
678                    }
679                }
680            }
681
682            //if not, generate symbol.
683            if (ii == null)//*/
684            {
685                int version = SymbolID.getVersion(symbolID);
686                //check symbol size////////////////////////////////////////////
687                Rect rect = null;
688                iconID = SVGLookup.getMainIconID(symbolID);
689                siIcon = SVGLookup.getInstance().getSVGLInfo(iconID, version);
690                mod1ID = SVGLookup.getMod1ID(symbolID);
691                siMod1 = SVGLookup.getInstance().getSVGLInfo(mod1ID, version);
692                float borderPadding = 0;
693                if (drawCustomOutline) {
694                    borderPadding = RendererUtilities.findWidestStrokeWidth(siIcon.getSVG());
695                }
696                top = (int)Math.floor(siIcon.getBbox().top);
697                left = (int)Math.floor(siIcon.getBbox().left);
698                width = (int)Math.ceil(siIcon.getBbox().width() + (siIcon.getBbox().left - left));
699                height = (int)Math.ceil(siIcon.getBbox().height() + (siIcon.getBbox().top - top));
700                if(siIcon.getBbox().bottom > 400)
701                    svgStart = "<svg xmlns:svg=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 612 792\">";
702                else
703                    svgStart = "<svg xmlns:svg=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 400 400\">";
704
705                String strSVGIcon = null;
706
707
708                if(hasAPFill) //action points and a few others //Sonobuoy //ACP, CCP, PUP
709                {
710                    String apFill;
711                    if(fillColor != null)
712                        apFill = RendererUtilities.colorToHexString(fillColor,false);
713                    else
714                        apFill = RendererUtilities.colorToHexString(SymbolUtilities.getFillColorOfAffiliation(symbolID),false);
715                    siIcon = new SVGInfo(siIcon.getID(),siIcon.getBbox(), siIcon.getSVG().replaceAll("fill=\"none\"","fill=\"" + apFill + "\""));
716                }
717
718                //update line and fill color of frame SVG
719                if(msi.getSymbolSet() == SymbolID.SymbolSet_ControlMeasure && (lineColor != null || fillColor != null)) {
720                    if (drawCustomOutline) {
721                        // create outline with larger stroke-width first (if selected)
722                        strSVGIcon = RendererUtilities.setSVGSPCMColors(symbolID, siIcon.getSVG(), RendererUtilities.getIdealOutlineColor(lineColor), fillColor, true);
723                    }
724
725                    // append normal symbol SVG to be layered on top of outline
726                    strSVGIcon += RendererUtilities.setSVGSPCMColors(symbolID, siIcon.getSVG(), lineColor, fillColor, false);
727                }
728                else//weather symbol (don't change color of weather graphics)
729                    strSVGIcon = siIcon.getSVG();
730
731                //If symbol is Static Depiction, add internal mine graphic based on sector modifier 1
732                if(SymbolID.getEntityCode(symbolID) == 270701 && siMod1 != null)
733                {
734                    if (drawCustomOutline) {
735                        // create outline with larger stroke-width first (if selected)
736                        strSVGIcon += RendererUtilities.setSVGSPCMColors(mod1ID, siMod1.getSVG(), RendererUtilities.getIdealOutlineColor(RendererUtilities.getColorFromHexString("#00A651")), RendererUtilities.getColorFromHexString("#00A651"), true);
737                    }
738                    //strSVGIcon += siMod1.getSVG();
739                    strSVGIcon += RendererUtilities.setSVGSPCMColors(mod1ID, siMod1.getSVG(), lineColor, fillColor, false);
740                }
741
742                if (pixelSize > 0)
743                {
744                    symbolBounds = RectUtilities.makeRect(left,top,width,height);
745                    rect = new Rect(symbolBounds);
746
747                    //adjust size
748                    float p = pixelSize;
749                    float h = rect.height();
750                    float w = rect.width();
751
752                    ratio = Math.min((p / h), (p / w));
753
754                    symbolBounds = RectUtilities.makeRect(0f, 0f, w * ratio, h * ratio);
755
756                    //make sure border padding isn't excessive.
757                    w = symbolBounds.width();
758                    h = symbolBounds.height();
759
760                    if(h/(h+borderPadding) > 0.10)
761                    {
762                        borderPadding = (float)(h * 0.1);
763                    }
764                    else if(w/(w+borderPadding) > 0.10)
765                    {
766                        borderPadding = (float)(w * 0.1);
767                    }
768
769                }
770
771                //Draw glyphs to bitmap
772                Bitmap bmp = Bitmap.createBitmap((symbolBounds.width() + Math.round(borderPadding)), (symbolBounds.height() + Math.round(borderPadding)), Config.ARGB_8888);
773                Canvas canvas = new Canvas(bmp);
774
775                symbolBounds = new Rect(0, 0, bmp.getWidth(), bmp.getHeight());
776
777                //grow size SVG to accommodate the outline we added
778                int offset = 0;
779                if(drawCustomOutline)
780                {
781                    //TODO: maybe come up with a calculation vs just the #2, although it seems to work well.
782                    RectUtilities.grow(rect, 2);
783                    offset = 4;
784                }//*/
785
786
787                if(msi.getSymbolSet()==SymbolID.SymbolSet_ControlMeasure && msi.getDrawRule()==DrawRules.POINT1)//smooth out action points
788                    strSVGIcon = "/n<g stroke-linejoin=\"round\" >/n" + strSVGIcon + "/n</g>";
789
790                strSVG = svgStart + strSVGIcon + "</svg>";
791                mySVG = SVG.getFromString(strSVG);
792                //mySVG.setDocumentViewBox(left,top,width,height);
793                mySVG.setDocumentViewBox(rect.left,rect.top,rect.width(),rect.height());
794                mySVG.renderToCanvas(canvas);
795
796                Point centerPoint = SymbolUtilities.getCMSymbolAnchorPoint(symbolID,new RectF(offset, offset, symbolBounds.right, symbolBounds.bottom));
797
798                ii = new ImageInfo(bmp, centerPoint, symbolBounds);
799
800                if(cacheEnabled && drawAsIcon == false && bmp.getAllocationByteCount() <= maxCachedEntrySize)
801                {
802                    synchronized (_SinglePointCacheMutex)
803                    {
804                        if(_tgCache.get(key) == null)
805                            _tgCache.put(key, ii);
806                    }
807                }
808                /*if (drawAsIcon == false && pixelSize <= 100)
809
810                    _tgCache.put(key, ii);
811                }//*/
812            }
813
814            //Process Modifiers
815            ImageInfo iiNew = null;
816            if (drawAsIcon == false && (hasTextModifiers || hasDisplayModifiers))
817            {
818                SymbolDimensionInfo sdiTemp = null;
819                if (SymbolUtilities.isSPWithSpecialModifierLayout(symbolID))//(SymbolUtilitiesD.isTGSPWithSpecialModifierLayout(symbolID))
820                {
821                    sdiTemp = ModifierRenderer.ProcessTGSPWithSpecialModifierLayout(ii, symbolID, modifiers, attributes, lineColor);
822                }
823                else
824                {
825                    sdiTemp = ModifierRenderer.ProcessTGSPModifiers(ii, symbolID, modifiers, attributes, lineColor);
826                }
827                iiNew = (sdiTemp instanceof ImageInfo ? (ImageInfo)sdiTemp : null);
828            }
829
830            if (iiNew != null)
831            {
832                ii = iiNew;
833            }
834
835            //cleanup
836            //bmp.recycle();
837            symbolBounds = null;
838            fullBMP = null;
839            fullBounds = null;
840            mySVG = null;
841
842
843            if (drawAsIcon)
844            {
845                return ii.getSquareImageInfo();
846            }
847            else
848            {
849                return ii;
850            }
851
852        }
853        catch (Exception exc)
854        {
855            ErrorLogger.LogException("MilStdIconRenderer", "RenderSP", exc);
856        }
857        return null;
858    }
859
860
861    /**
862     *
863     * @param symbolID
864     * @return
865     */
866    @SuppressWarnings("unused")
867    public ImageInfo RenderModifier(String symbolID, Map<String,String> attributes)
868    {
869        ImageInfo temp = null;
870        String basicSymbolID = null;
871
872        Color lineColor = null;
873        Color fillColor = null;//SymbolUtilities.getFillColorOfAffiliation(symbolID);
874
875        int alpha = -1;
876
877
878        //SVG rendering variables
879        MSInfo msi = null;
880        String iconID = null;
881        SVGInfo siIcon = null;
882        int top = 0;
883        int left = 0;
884        int width = 0;
885        int height = 0;
886        String svgStart = null;
887        String strSVG = null;
888        SVG mySVG = null;
889
890        float ratio = 0;
891
892        Rect symbolBounds = null;
893        RectF fullBounds = null;
894        Bitmap fullBMP = null;
895
896        boolean drawAsIcon = false;
897        int pixelSize = -1;
898        boolean keepUnitRatio = true;
899        boolean hasDisplayModifiers = false;
900        boolean hasTextModifiers = false;
901        int symbolOutlineWidth = RendererSettings.getInstance().getSinglePointSymbolOutlineWidth();
902        boolean drawCustomOutline = false;
903
904        try
905        {
906
907            msi = MSLookup.getInstance().getMSLInfo(symbolID);
908            if (attributes != null)
909            {
910                if (attributes.containsKey(MilStdAttributes.KeepUnitRatio))
911                {
912                    keepUnitRatio = Boolean.parseBoolean(attributes.get(MilStdAttributes.KeepUnitRatio));
913                }
914
915                if (attributes.containsKey(MilStdAttributes.LineColor))
916                {
917                    lineColor = RendererUtilities.getColorFromHexString(attributes.get(MilStdAttributes.LineColor));
918                }
919
920                if (attributes.containsKey(MilStdAttributes.FillColor))
921                {
922                    fillColor = RendererUtilities.getColorFromHexString(attributes.get(MilStdAttributes.FillColor));
923                }
924
925                if (attributes.containsKey(MilStdAttributes.Alpha))
926                {
927                    alpha = Integer.parseInt(attributes.get(MilStdAttributes.Alpha));
928                }
929
930                if (attributes.containsKey(MilStdAttributes.DrawAsIcon))
931                {
932                    drawAsIcon = Boolean.parseBoolean(attributes.get(MilStdAttributes.DrawAsIcon));
933                }
934
935                if (attributes.containsKey(MilStdAttributes.PixelSize))
936                {
937                    pixelSize = Integer.parseInt(attributes.get(MilStdAttributes.PixelSize));
938                    if(msi.getSymbolSet() == SymbolID.SymbolSet_ControlMeasure)
939                    {
940                        if(SymbolID.getEntityCode(symbolID)==270701)//static depiction
941                            pixelSize = (int)(pixelSize * 0.9);//try to scale to be somewhat in line with units
942                    }
943                }
944
945                if(drawAsIcon==false)//don't outline icons because they're not going on the map
946                {
947                    if(attributes.containsKey(MilStdAttributes.OutlineSymbol))
948                        drawCustomOutline = Boolean.parseBoolean(attributes.get(MilStdAttributes.OutlineSymbol));
949                    else
950                        drawCustomOutline = RendererSettings.getInstance().getOutlineSPControlMeasures();
951                }
952
953                if(SymbolUtilities.isMultiPoint(symbolID))
954                    drawCustomOutline=false;//icon previews for multipoints do not need outlines since they shouldn't be on the map
955
956                /*if (attributes.containsKey(MilStdAttributes.OutlineWidth)>=0)
957                 symbolOutlineWidth = Integer.parseInt(attributes.get(MilStdAttributes.OutlineWidth));//*/
958            }
959
960            int outlineOffset = symbolOutlineWidth;
961            if (drawCustomOutline && outlineOffset > 2)
962            {
963                outlineOffset = (outlineOffset - 1) / 2;
964            }
965            else
966            {
967                outlineOffset = 0;
968            }
969
970        }
971        catch (Exception excModifiers)
972        {
973            ErrorLogger.LogException("MilStdIconRenderer", "RenderSP", excModifiers);
974        }
975
976        try
977        {
978            ImageInfo ii = null;
979            int intFill = -1;
980            if (fillColor != null)
981            {
982                intFill = fillColor.toInt();
983            }
984
985
986            if(msi.getSymbolSet() != SymbolID.SymbolSet_ControlMeasure)
987                lineColor = Color.BLACK;//color isn't black but should be fine for weather since colors can't be user defined.
988
989
990            //if not, generate symbol
991            if (ii == null)//*/
992            {
993                int version = SymbolID.getVersion(symbolID);
994                //check symbol size////////////////////////////////////////////
995                Rect rect = null;
996
997                iconID = SVGLookup.getMod1ID(symbolID);
998                siIcon = SVGLookup.getInstance().getSVGLInfo(iconID, version);
999                top = Math.round(siIcon.getBbox().top);
1000                left = Math.round(siIcon.getBbox().left);
1001                width = Math.round(siIcon.getBbox().width());
1002                height = Math.round(siIcon.getBbox().height());
1003                if(siIcon.getBbox().bottom > 400)
1004                    svgStart = "<svg xmlns:svg=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 612 792\">";
1005                else
1006                    svgStart = "<svg xmlns:svg=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 400 400\">";
1007
1008                String strSVGIcon = null;
1009                String strSVGOutline = null;
1010
1011                //update line and fill color of frame SVG
1012                if(msi.getSymbolSet() == SymbolID.SymbolSet_ControlMeasure && (lineColor != null || fillColor != null))
1013                    strSVGIcon = RendererUtilities.setSVGFrameColors(symbolID,siIcon.getSVG(),lineColor,fillColor);
1014                else
1015                    strSVGIcon = siIcon.getSVG();
1016
1017                if (pixelSize > 0)
1018                {
1019                    symbolBounds = RectUtilities.makeRect(left,top,width,height);
1020                    rect = new Rect(symbolBounds);
1021
1022                    //adjust size
1023                    float p = pixelSize;
1024                    float h = rect.height();
1025                    float w = rect.width();
1026
1027                    ratio = Math.min((p / h), (p / w));
1028
1029                    symbolBounds = RectUtilities.makeRect(0f, 0f, w * ratio, h * ratio);
1030
1031                }
1032
1033
1034                //TODO: figure out how to draw an outline and adjust the symbol bounds accordingly
1035
1036                //Draw glyphs to bitmap
1037                Bitmap bmp = Bitmap.createBitmap((symbolBounds.width()), (symbolBounds.height()), Config.ARGB_8888);
1038                Canvas canvas = new Canvas(bmp);
1039
1040                symbolBounds = new Rect(0, 0, bmp.getWidth(), bmp.getHeight());
1041
1042                strSVG = svgStart + strSVGIcon + "</svg>";
1043                mySVG = SVG.getFromString(strSVG);
1044                mySVG.setDocumentViewBox(left,top,width,height);
1045                mySVG.renderToCanvas(canvas);
1046
1047                Point centerPoint = SymbolUtilities.getCMSymbolAnchorPoint(symbolID,new RectF(0, 0, symbolBounds.right, symbolBounds.bottom));
1048
1049                ii = new ImageInfo(bmp, centerPoint, symbolBounds);
1050
1051
1052                /*if (drawAsIcon == false && pixelSize <= 100)
1053                {
1054                    _tgCache.put(key, ii);
1055                }//*/
1056            }
1057
1058
1059            //cleanup
1060            //bmp.recycle();
1061            symbolBounds = null;
1062            fullBMP = null;
1063            fullBounds = null;
1064            mySVG = null;
1065
1066
1067            if (drawAsIcon)
1068            {
1069                return ii.getSquareImageInfo();
1070            }
1071            else
1072            {
1073                return ii;
1074            }
1075
1076        }
1077        catch (Exception exc)
1078        {
1079            ErrorLogger.LogException("MilStdIconRenderer", "RenderSP", exc);
1080        }
1081        return null;
1082    }
1083
1084
1085
1086    /**
1087     *
1088     * @param symbolID
1089     * @param lineColor
1090     * @param fillColor
1091     * @param size
1092     * @param keepUnitRatio
1093     * @param drawOutline (only for single-point Control Measures)
1094     * @return
1095     */
1096    private static String makeCacheKey(String symbolID, int lineColor, int fillColor, int size, boolean keepUnitRatio, boolean drawOutline)
1097    {
1098        return makeCacheKey(symbolID, lineColor, fillColor, "null",size, keepUnitRatio, false);
1099    }
1100
1101    private static String makeCacheKey(String symbolID, int lineColor, int fillColor, String iconColor, int size, boolean keepUnitRatio, boolean drawOutline)
1102    {
1103        //String key = symbolID.substring(0, 20) + String.valueOf(lineColor) + String.valueOf(fillColor) + String.valueOf(size) + String.valueOf(keepUnitRatio);
1104        String key = symbolID.substring(0, 7) + symbolID.substring(10, 20) + SymbolID.getFrameShape(symbolID) + lineColor + fillColor + iconColor + size + keepUnitRatio + drawOutline;
1105        return key;
1106    }
1107
1108    public void logError(String tag, Throwable thrown)
1109    {
1110        if (tag == null || tag.equals(""))
1111        {
1112            tag = "singlePointRenderer";
1113        }
1114
1115        String message = thrown.getMessage();
1116        String stack = getStackTrace(thrown);
1117        if (message != null)
1118        {
1119            Log.e(tag, message);
1120        }
1121        if (stack != null)
1122        {
1123            Log.e(tag, stack);
1124        }
1125    }
1126
1127    public String getStackTrace(Throwable thrown)
1128    {
1129        try
1130        {
1131            if (thrown != null)
1132            {
1133                if (thrown.getStackTrace() != null)
1134                {
1135                    String eol = System.getProperty("line.separator");
1136                    StringBuilder sb = new StringBuilder();
1137                    sb.append(thrown.toString());
1138                    sb.append(eol);
1139                    for (StackTraceElement element : thrown.getStackTrace())
1140                    {
1141                        sb.append("        at ");
1142                        sb.append(element);
1143                        sb.append(eol);
1144                    }
1145                    return sb.toString();
1146                }
1147                else
1148                {
1149                    return thrown.getMessage() + "- no stack trace";
1150                }
1151            }
1152            else
1153            {
1154                return "no stack trace";
1155            }
1156        }
1157        catch (Exception exc)
1158        {
1159            Log.e("getStackTrace", exc.getMessage());
1160        }
1161        return thrown.getMessage();
1162    }//
1163
1164    /*
1165     private static String PrintList(ArrayList list)
1166     {
1167     String message = "";
1168     for(Object item : list)
1169     {
1170
1171     message += item.toString() + "\n";
1172     }
1173     return message;
1174     }//*/
1175    /*
1176     private static String PrintObjectMap(Map<String, Object> map)
1177     {
1178     Iterator<Object> itr = map.values().iterator();
1179     String message = "";
1180     String temp = null;
1181     while(itr.hasNext())
1182     {
1183     temp = String.valueOf(itr.next());
1184     if(temp != null)
1185     message += temp + "\n";
1186     }
1187     //ErrorLogger.LogMessage(message);
1188     return message;
1189     }//*/
1190    @Override
1191    public void onSettingsChanged(SettingsChangedEvent sce)
1192    {
1193
1194        if(sce != null && sce.getEventType().equals(SettingsChangedEvent.EventType_FontChanged))
1195        {
1196            synchronized (_modifierFontMutex)
1197            {
1198                _modifierFont = RendererSettings.getInstance().getModiferFont();
1199                _modifierOutlineFont = RendererSettings.getInstance().getModiferFont();
1200                FontMetrics fm = new FontMetrics();
1201                fm = _modifierFont.getFontMetrics();
1202                _modifierDescent = fm.descent;
1203                //_modifierFontHeight = fm.top + fm.bottom;
1204                _modifierFontHeight = fm.bottom - fm.top;
1205
1206                _modifierFont.setStrokeWidth(RendererSettings.getInstance().getTextOutlineWidth());
1207                _modifierOutlineFont.setColor(Color.white.toInt());
1208                _deviceDPI = RendererSettings.getInstance().getDeviceDPI();
1209
1210                ModifierRenderer.setModifierFont(_modifierFont, _modifierFontHeight, _modifierDescent);
1211
1212            }
1213        }
1214
1215        if(sce != null && sce.getEventType().equals(SettingsChangedEvent.EventType_CacheSizeChanged))
1216        {
1217
1218            int cSize = RendererSettings.getInstance().getCacheSize()/2;
1219            //adjust unit cache
1220            if(cSize != cacheSize) {
1221                cacheSize = cSize;
1222                if (cacheSize >= 5)
1223                    maxCachedEntrySize = cacheSize / 5;
1224                else
1225                    maxCachedEntrySize = 1;
1226
1227                if(cacheEnabled) //if cache enabled, update cache
1228                {
1229
1230                    synchronized (_UnitCacheMutex) {
1231                        if(_unitCache != null)
1232                            _unitCache.evictAll();
1233                        _unitCache = new LruCache<String, ImageInfo>(cSize) {
1234                            @Override
1235                            protected int sizeOf(String key, ImageInfo ii) {
1236                                return ii.getByteCount();// / 1024;
1237                            }
1238                        };
1239                    }
1240                    //adjust tg cache
1241                    synchronized (_SinglePointCacheMutex) {
1242                        if(_tgCache != null)
1243                            _tgCache.evictAll();
1244                        _tgCache = new LruCache<String, ImageInfo>(cSize) {
1245                            @Override
1246                            protected int sizeOf(String key, ImageInfo ii) {
1247                                return ii.getByteCount();// / 1024;
1248                            }
1249                        };
1250                    }
1251                }
1252            }
1253        }
1254        if(sce != null && sce.getEventType().equals(SettingsChangedEvent.EventType_CacheToggled))
1255        {
1256            if(cacheEnabled != RendererSettings.getInstance().getCacheEnabled())
1257            {
1258                cacheEnabled = RendererSettings.getInstance().getCacheEnabled();
1259
1260                if (cacheEnabled == false)
1261                {
1262                    synchronized (_SinglePointCacheMutex)
1263                    {
1264                        if (_tgCache != null)
1265                            _tgCache.evictAll();
1266                        _tgCache = null;
1267                    }
1268                    synchronized (_UnitCacheMutex)
1269                    {
1270                        if (_unitCache != null)
1271                            _unitCache.evictAll();
1272                        _unitCache = null;
1273                    }
1274                }
1275                else
1276                {
1277                    int cSize = RendererSettings.getInstance().getCacheSize() / 2;
1278                    synchronized (_SinglePointCacheMutex)
1279                    {
1280                        if(_tgCache != null)
1281                            _tgCache.evictAll();
1282                        _tgCache = new LruCache<String, ImageInfo>(cSize) {
1283                            @Override
1284                            protected int sizeOf(String key, ImageInfo ii) {
1285                                return ii.getByteCount();// / 1024;
1286                            }
1287                        };
1288                    }
1289                    synchronized (_UnitCacheMutex)
1290                    {
1291                        if(_unitCache != null)
1292                            _unitCache.evictAll();
1293                        _unitCache = new LruCache<String, ImageInfo>(cSize) {
1294                            @Override
1295                            protected int sizeOf(String key, ImageInfo ii) {
1296                                return ii.getByteCount();// / 1024;
1297                            }
1298                        };
1299                    }
1300                }
1301            }
1302        }
1303    }
1304}