001package armyc2.c5isr.renderer.symbolpicker;
002
003import android.app.Activity;
004import android.app.AlertDialog;
005import android.app.Dialog;
006import android.content.Context;
007import android.content.Intent;
008import android.graphics.Bitmap;
009import android.graphics.Color;
010import android.os.Build;
011import android.os.Bundle;
012import androidx.core.widget.TextViewCompat;
013import android.text.Editable;
014import android.text.InputType;
015import android.text.TextWatcher;
016import android.util.SparseArray;
017import android.util.TypedValue;
018import android.view.LayoutInflater;
019import android.view.View;
020import android.view.ViewGroup;
021import android.view.Window;
022import android.view.WindowManager;
023import android.widget.AdapterView;
024import android.widget.ArrayAdapter;
025import android.widget.Button;
026import android.widget.CheckBox;
027import android.widget.CompoundButton;
028import android.widget.DatePicker;
029import android.widget.EditText;
030import android.widget.GridView;
031import android.widget.ImageView;
032import android.widget.LinearLayout;
033import android.widget.ListView;
034import android.widget.RadioGroup;
035import android.widget.SearchView;
036import android.widget.Spinner;
037import android.widget.TextView;
038import android.widget.TimePicker;
039import android.widget.Toast;
040import android.widget.ToggleButton;
041
042import androidx.core.widget.TextViewCompat;
043
044import java.io.BufferedReader;
045import java.io.IOException;
046import java.io.InputStream;
047import java.io.InputStreamReader;
048import java.text.DecimalFormat;
049import java.util.ArrayList;
050import java.util.Calendar;
051import java.util.Collections;
052import java.util.Comparator;
053import java.util.Date;
054import java.util.GregorianCalendar;
055import java.util.HashMap;
056import java.util.List;
057import java.util.Locale;
058import java.util.Map;
059import java.util.Objects;
060import java.util.Stack;
061import java.util.TreeMap;
062import java.util.stream.Collectors;
063
064import armyc2.c5isr.renderer.MilStdIconRenderer;
065import armyc2.c5isr.renderer.R;
066import armyc2.c5isr.renderer.utilities.DrawRules;
067import armyc2.c5isr.renderer.utilities.ImageInfo;
068import armyc2.c5isr.renderer.utilities.MSInfo;
069import armyc2.c5isr.renderer.utilities.MSLookup;
070import armyc2.c5isr.renderer.utilities.MilStdAttributes;
071import armyc2.c5isr.renderer.utilities.Modifiers;
072import armyc2.c5isr.renderer.utilities.RendererSettings;
073import armyc2.c5isr.renderer.utilities.SVGInfo;
074import armyc2.c5isr.renderer.utilities.SVGLookup;
075import armyc2.c5isr.renderer.utilities.SymbolID;
076import armyc2.c5isr.renderer.utilities.SymbolUtilities;
077import armyc2.c5isr.web.render.MultiPointHandler;
078
079/**
080 * Symbol picker activity. Sends selected symbol ID back to activity initialized from
081 */
082public class SymbolPickerActivity extends Activity {
083    public static final String selectedSymbolIdKey = "selectedSymbolIdKey";
084    public static final String modifiersKey = "modifiersKey";
085    public static final String attributesKey = "attributesKey";
086    public static final String supportedVersionsKey = "supportedVersionsKey";
087    private static final String searchNodeName = "searchResults";
088    private boolean activeSearch = false;
089    private final int cellSize = 85 * RendererSettings.getInstance().getDeviceDPI() / 96; // in px
090    private Stack<Node> pageTrail; // top of stack is current page
091    private SymbolGVAdapter symbolTableAdapter;
092    private Button configureButton;
093    private ToggleButton flattenTreeToggle;
094    private Node selectedSymbolNode;
095    private MilStdIconRenderer mir = null;
096    private final Bitmap emptyBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
097
098    // Main Modifiers Dialog
099    private Dialog mainModifiersDialog;
100    private RadioGroup contextRadioGroup;
101    private Spinner stdIdSpinner;
102    private Spinner statusSpinner;
103    private Spinner hqSpinner;
104    private Spinner ampCategorySpinner;
105    private Spinner ampTypeSpinner;
106    private LinearLayout ampTypeLayout;
107    private boolean hasSectorModifiers;
108    private Spinner sector1Spinner;
109    private final int SECTOR_1_MINE_INDEX = 13; // position of Mine in ss_ControlMeasure_sector1_array
110    private final int ANTIPERSONNEL_MINE = 0b000001;
111    private final int ANTIPERSONNEL_MINE_DIRECTIONAL = 0b000010;
112    private final int ANTITANK_MINE = 0b000100;
113    private final int ANTITANK_MINE_ANTIHANDLING = 0b001000;
114    private final int WIDE_AREA_ANTITANK_MINE = 0b010000;
115    private final int MINE_CLUSTER = 0b100000;
116    private SparseArray<String> mineSectorLookup;
117    private ArrayList<Integer> invalidMineTrios;
118    private CheckBox AntipersonnelMineBox;
119    private CheckBox AntipersonnelDirectionalMineBox;
120    private CheckBox AntitankMineBox;
121    private CheckBox AntitankAntihandlingMineBox;
122    private CheckBox WideAreaAntitankMineBox;
123    private CheckBox MineClusterBox;
124    private ArrayList<CheckBox> mineCheckBoxList;
125    private int numMinesChecked;
126    private Spinner sector2Spinner;
127    private boolean hasCountryModifier;
128    private TextView countryTextView;
129    private TreeMap<String, String> countryMap;
130    private ArrayList<String> countryNames;
131    private Dialog countrySearchDialog;
132
133    // Attributes + Extra Modifiers Dialog
134    private Dialog extraModifiersDialog;
135    private static final int M = 0;
136    private static final int KM = 1;
137    private static final int FT = 2;
138    private static final int SM = 3;
139    private static final int FL = 4;
140    enum AltitudeUnits {
141        M(SymbolPickerActivity.M, "(meters)"),
142        KM(SymbolPickerActivity.KM, "(kilometers)"),
143        FT(SymbolPickerActivity.FT, "(feet)"),
144        SM(SymbolPickerActivity.SM, "(statute miles)"),
145        FL(SymbolPickerActivity.FL, "(flight level)");
146
147        private final int index;
148        private final String desc;
149
150        AltitudeUnits(int index, String description) {
151            this.index = index;
152            this.desc = description;
153        }
154    }
155    enum AltitudeModes {
156        AMSL(0, "(above mean sea level)"),
157        BMSL(1, "(below mean sea level)"),
158        HAE(2, "(height above ellipsoid)"),
159        AGL(3, "(above ground level)");
160
161        private final int index;
162        private final String desc;
163
164        AltitudeModes(int index, String description) {
165            this.index = index;
166            this.desc = description;
167        }
168    }
169
170    Spinner altitudeUnitSpinner;
171    Spinner altitudeModeSpinner;
172    private ArrayList<String> modifiersToGet;
173    private HashMap<String, String> modifiersToSend;
174    private HashMap<String, String> attributesToSend;
175    private TreeManager treeManager;
176
177    @Override
178    protected void onCreate(Bundle savedInstanceState) {
179        super.onCreate(savedInstanceState);
180        setContentView(R.layout.activity_symbol_picker);
181
182        pageTrail = new Stack<>();
183
184        Button backButton = findViewById(R.id.symbol_picker_back_button);
185        backButton.setOnClickListener(view -> onBackPressed());
186
187        configureButton = findViewById(R.id.symbol_picker_configure_button);
188        configureButton.setOnClickListener(view -> onConfigureSymbol());
189
190        symbolTableAdapter = new SymbolGVAdapter(this, new ArrayList<>());
191        GridView symbolTable = findViewById(R.id.symbol_picker_table);
192        symbolTable.setAdapter(symbolTableAdapter);
193        symbolTable.setColumnWidth(cellSize);
194
195        flattenTreeToggle = findViewById(R.id.symbol_picker_flatten_toggle);
196        flattenTreeToggle.setOnClickListener(view -> updateSymbolTable());
197
198        // Do not reinitialize render or change settings in child activity
199        mir = MilStdIconRenderer.getInstance();
200
201        treeManager = new TreeManager();
202        try {
203            int[] versions = getIntent().getIntArrayExtra(supportedVersionsKey);
204            if (versions == null)
205                versions = new int[]{SymbolID.Version_2525Dch1};
206            treeManager.buildTree(getApplicationContext(), versions);
207        } catch (IOException e) {
208            throw new RuntimeException(e);
209        }
210
211        updateSelectedSymbol(treeManager.mil2525Tree);
212
213        // read in country names and codes
214        String line;
215        String[] segments;
216        countryMap = new TreeMap<>();
217        try (InputStream in = this.getResources().openRawResource(R.raw.genc);
218             BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
219            while ((line = reader.readLine()) != null) {
220                segments = line.split("\t+");
221                StringBuilder countryCode = new StringBuilder(segments[1]);
222                while (countryCode.length() < 3) {
223                    countryCode.insert(0, "0");
224                }
225                countryMap.put(segments[2], countryCode.toString());
226            }
227        } catch (IOException e) {
228            throw new RuntimeException(e);
229        }
230        countryNames = new ArrayList<>(countryMap.keySet());
231        Collections.sort(countryNames);
232
233        SearchView symbolSearchView = findViewById(R.id.symbol_picker_search);
234        symbolSearchView.setOnQueryTextListener(new SearchBoxListener());
235
236        // these four combinations of 3 mines do not have a code in 2525D Change 1
237        invalidMineTrios = new ArrayList<>();
238        invalidMineTrios.add(ANTITANK_MINE | ANTITANK_MINE_ANTIHANDLING | WIDE_AREA_ANTITANK_MINE);
239        invalidMineTrios.add(ANTITANK_MINE | ANTITANK_MINE_ANTIHANDLING | MINE_CLUSTER);
240        invalidMineTrios.add(ANTITANK_MINE | WIDE_AREA_ANTITANK_MINE | MINE_CLUSTER);
241        invalidMineTrios.add(ANTITANK_MINE_ANTIHANDLING | WIDE_AREA_ANTITANK_MINE | MINE_CLUSTER);
242    }
243
244    @Override
245    public void onBackPressed() {
246        if (pageTrail.peek().getName().equals(searchNodeName)) {
247            // Clearing the query will remove the search page
248            SearchView symbolSearchView = findViewById(R.id.symbol_picker_search);
249            symbolSearchView.setQuery("", false);
250            symbolSearchView.clearFocus();
251        } else if (pageTrail.size() > 1) {
252            // Go to next higher level
253            pageTrail.pop();
254            updateSelectedSymbol(pageTrail.pop());
255        } else {
256            // Ask if the user wishes to exit
257            new AlertDialog.Builder(this)
258                    .setTitle("Confirm Exit")
259                    .setMessage("Do you want to exit the symbol picker?")
260                    .setPositiveButton(android.R.string.cancel, null)
261                    .setNegativeButton(android.R.string.ok, (dialog, whichButton) -> {
262                        // Exit symbol picker return blank symbol code
263                        Intent resultIntent = new Intent();
264                        resultIntent.putExtra(selectedSymbolIdKey, "");
265                        setResult(Activity.RESULT_OK, resultIntent);
266                        finish();
267                    })
268                    .show();
269        }
270    }
271
272    // Updates symbol preview on modifier change
273    private class ModifierSpinnerListener implements AdapterView.OnItemSelectedListener {
274        @Override
275        public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
276            updateSymbolPreview();
277
278            // Disables altitudeModeSpinner if user selects "FL" (flight level) because it doesn't use an altitude mode
279            // (though the selected Mode would be ignored anyway)
280            if (altitudeUnitSpinner != null && altitudeModeSpinner != null) {
281                altitudeModeSpinner.setEnabled(altitudeUnitSpinner.getSelectedItemPosition() != AltitudeUnits.valueOf("FL").index);
282            }
283        }
284
285        @Override
286        public void onNothingSelected(AdapterView<?> adapterView) {
287            updateSymbolPreview();
288        }
289    }
290
291    // Called when "configure symbol" is clicked
292    private void onConfigureSymbol() {
293        // There's no good way to send a SparseArray<String> in an Intent, so use HashMaps here and
294        // the calling application can build SparseArrays from them.
295        modifiersToSend = new HashMap<>();
296        attributesToSend = new HashMap<>();
297
298        final int selectedSymbolVersion = Integer.parseInt(selectedSymbolNode.getVersion());
299        final String selectedSymbolSet = selectedSymbolNode.getSymbolSetCode();
300        final String selectedSymbolEntityCode = selectedSymbolNode.getCode();
301        final String selectedSymbolName = selectedSymbolNode.getName();
302
303        // TODO temporarily bypasses weather all modifiers because the extra ones that are applicable are not implemented yet (and thus sends empty modifiers HashMap back up)
304        switch (Integer.parseInt(selectedSymbolSet)) {
305            case SymbolID.SymbolSet_Atmospheric:
306            case SymbolID.SymbolSet_Oceanographic:
307            case SymbolID.SymbolSet_MeteorologicalSpace:
308                // for debugging modifiers:
309                /*MSInfo msi = MSLookup.getInstance().getMSLInfo(symbolSetCode + selectedSymbolEntityCode,0);
310                ArrayList<Integer> initialModifiers = msi.getModifiers();
311                ArrayList<Integer> ignoreModifiers = Modifiers.GetPredeterminedModifiersList();
312                modifiersToGet = new ArrayList<>();
313                for (int i : initialModifiers) {
314                    if (!ignoreModifiers.contains(i)) {
315                        modifiersToGet.add(i);
316                        Log.d("onSelectPressed", "added modifier: " + i);
317                    }
318                }*/
319
320                // construct neutral present (---4--0) code for all weather symbols
321                String weatherSymbolID = selectedSymbolVersion + "04" + selectedSymbolSet + "0000" + selectedSymbolEntityCode + "00000000000000";
322                Intent resultIntent = new Intent();
323                resultIntent.putExtra(selectedSymbolIdKey, weatherSymbolID);
324                resultIntent.putExtra(modifiersKey, modifiersToSend);
325                resultIntent.putExtra(attributesKey, attributesToSend);
326                setResult(Activity.RESULT_OK, resultIntent);
327                finish();
328                return;
329        }
330
331        mainModifiersDialog = new Dialog(this);
332        mainModifiersDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
333        mainModifiersDialog.setContentView(R.layout.modifiers_page1);
334        mainModifiersDialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
335        mainModifiersDialog.show();
336
337        contextRadioGroup = mainModifiersDialog.findViewById(R.id.context_radio_group);
338        contextRadioGroup.setOnCheckedChangeListener((radioGroup, i) -> updateSymbolPreview());
339
340        stdIdSpinner = mainModifiersDialog.findViewById(R.id.std_id_spinner);
341        ArrayAdapter<CharSequence> stdIdAdapter = ArrayAdapter.createFromResource(this,
342                R.array.std_id_array, android.R.layout.simple_spinner_item);
343        stdIdAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
344        stdIdSpinner.setAdapter(stdIdAdapter);
345        stdIdSpinner.setSelection(3); // Default to friendly identity
346        stdIdSpinner.setOnItemSelectedListener(new ModifierSpinnerListener());
347
348        statusSpinner = mainModifiersDialog.findViewById(R.id.status_spinner);
349        ArrayAdapter<CharSequence> statusAdapter = ArrayAdapter.createFromResource(this,
350                R.array.status_array, android.R.layout.simple_spinner_item);
351        statusAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
352        statusSpinner.setAdapter(statusAdapter);
353        statusSpinner.setOnItemSelectedListener(new ModifierSpinnerListener());
354
355        hqSpinner = mainModifiersDialog.findViewById(R.id.hq_spinner);
356        ArrayAdapter<CharSequence> hqAdapter = ArrayAdapter.createFromResource(this,
357                R.array.hq_array, android.R.layout.simple_spinner_item);
358        hqAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
359        hqSpinner.setAdapter(hqAdapter);
360        hqSpinner.setOnItemSelectedListener(new ModifierSpinnerListener());
361
362        ampCategorySpinner = mainModifiersDialog.findViewById(R.id.amplifier_category_spinner);
363        ArrayAdapter<CharSequence> ampCategoryAdapter = ArrayAdapter.createFromResource(this,
364                R.array.amplifier_category_array, android.R.layout.simple_spinner_item);
365        ampCategoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
366        ampCategorySpinner.setAdapter(ampCategoryAdapter);
367        ampCategorySpinner.setOnItemSelectedListener(new AmplifierSelectedItemListener());
368
369        ampTypeLayout = mainModifiersDialog.findViewById(R.id.amplifier_type_layout);
370        ampTypeLayout.setVisibility(View.GONE);
371
372        ampTypeSpinner = mainModifiersDialog.findViewById(R.id.amplifier_type_spinner);
373        ArrayAdapter<CharSequence> ampTypeAdapter = ArrayAdapter.createFromResource(this,
374                R.array.amp0_unknown_array, android.R.layout.simple_spinner_item);
375        ampTypeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
376        ampTypeSpinner.setAdapter(ampTypeAdapter);
377        ampTypeSpinner.setOnItemSelectedListener(new ModifierSpinnerListener());
378
379        int sector1ArrayID;
380        int sector2ArrayID;
381
382        switch (Integer.parseInt(selectedSymbolSet)) {
383            case SymbolID.SymbolSet_Air:
384                sector1ArrayID = R.array.ss_Air_sector1_array;
385                sector2ArrayID = R.array.ss_Air_sector2_array;
386                break;
387            case SymbolID.SymbolSet_AirMissile:
388                sector1ArrayID = R.array.ss_AirMissile_sector1_array;
389                sector2ArrayID = R.array.ss_AirMissile_sector2_array;
390                break;
391            case SymbolID.SymbolSet_Space:
392                sector1ArrayID = R.array.ss_Space_sector1_array;
393                sector2ArrayID = R.array.ss_Space_sector2_array;
394                break;
395            case SymbolID.SymbolSet_SpaceMissile:
396                sector1ArrayID = R.array.ss_SpaceMissile_sector1_array;
397                sector2ArrayID = R.array.ss_SpaceMissile_sector2_array;
398                break;
399            case SymbolID.SymbolSet_LandUnit:
400                sector1ArrayID = R.array.ss_LandUnit_sector1_array;
401                sector2ArrayID = R.array.ss_LandUnit_sector2_array;
402                break;
403            case SymbolID.SymbolSet_LandCivilianUnit_Organization:
404                sector1ArrayID = R.array.ss_LandCivilianUnitOrganization_sector1_array;
405                sector2ArrayID = R.array.ss_LandCivilianUnitOrganization_sector2_array;
406                break;
407            case SymbolID.SymbolSet_LandEquipment:
408                sector1ArrayID = R.array.ss_LandEquipment_sector1_array;
409                sector2ArrayID = R.array.ss_LandEquipment_sector2_array;
410                break;
411            case SymbolID.SymbolSet_LandInstallation:
412                sector1ArrayID = R.array.ss_LandInstallation_sector1_array;
413                sector2ArrayID = R.array.ss_LandInstallation_sector2_array;
414                break;
415            case SymbolID.SymbolSet_ControlMeasure:
416                // Control Measures only use Sector 1
417                sector1ArrayID = R.array.ss_ControlMeasure_sector1_array;
418                sector2ArrayID = R.array.ss_NotApplicable_array;
419                break;
420            case SymbolID.SymbolSet_SeaSurface:
421                sector1ArrayID = R.array.ss_SeaSurface_sector1_array;
422                sector2ArrayID = R.array.ss_SeaSurface_sector2_array;
423                break;
424            case SymbolID.SymbolSet_SeaSubsurface:
425                sector1ArrayID = R.array.ss_SeaSubsurface_sector1_array;
426                sector2ArrayID = R.array.ss_SeaSubsurface_sector2_array;
427                break;
428            case SymbolID.SymbolSet_Activities:
429                sector1ArrayID = R.array.ss_Activities_sector1_array;
430                sector2ArrayID = R.array.ss_Activities_sector2_array;
431                break;
432            case SymbolID.SymbolSet_SignalsIntelligence_Space:
433            case SymbolID.SymbolSet_SignalsIntelligence_Air:
434            case SymbolID.SymbolSet_SignalsIntelligence_Land:
435            case SymbolID.SymbolSet_SignalsIntelligence_SeaSurface:
436            case SymbolID.SymbolSet_SignalsIntelligence_SeaSubsurface:
437                sector1ArrayID = R.array.ss_SignalsIntelligence_sector1_array;
438                sector2ArrayID = R.array.ss_SignalsIntelligence_sector2_array;
439                break;
440            default:
441                // Unknown, MineWarfare, Cyberspace
442                // (also Atmospheric, Oceanographic, and MeteorologicalSpace if we don't skip their modifier dialogs)
443                sector1ArrayID = R.array.ss_NotApplicable_array;
444                sector2ArrayID = R.array.ss_NotApplicable_array;
445                break;
446        }
447
448        View sectorsView = mainModifiersDialog.findViewById(R.id.sectors_layout);
449        if (sector1ArrayID == R.array.ss_NotApplicable_array) {
450            sectorsView.setVisibility(View.GONE);
451            hasSectorModifiers = false;
452        } else {
453            sectorsView.setVisibility(View.VISIBLE);
454            hasSectorModifiers = true;
455
456            sector1Spinner = mainModifiersDialog.findViewById(R.id.sector_1_spinner);
457            ArrayAdapter<CharSequence> sector1Adapter;
458            String[] landUnitSector1Mods = getResources().getStringArray(R.array.ss_LandUnit_sector1_array);
459            if (sector1ArrayID == R.array.ss_LandUnit_sector1_array) {
460                // create custom adapter to skip two "{Reserved for future use}" codes without altering the positions of the rest
461                sector1Adapter = new ArrayAdapter<CharSequence>(this, android.R.layout.simple_spinner_item, landUnitSector1Mods) {
462                    @Override
463                    public View getDropDownView(int position, View convertView, ViewGroup parent) {
464                        View v;
465
466                        if ("{Reserved for future use}".equals(landUnitSector1Mods[position])) {
467                            TextView tv = new TextView(getContext());
468                            tv.setHeight(0);
469                            tv.setVisibility(View.GONE);
470                            v = tv;
471                        } else {
472                            // Pass convertView as null to prevent reuse of special case views
473                            v = super.getDropDownView(position, null, parent);
474                        }
475                        return v;
476                    }
477                };
478            } else {
479                sector1Adapter = ArrayAdapter.createFromResource(this,
480                        sector1ArrayID, android.R.layout.simple_spinner_item);
481            }
482
483            sector1Adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
484            sector1Spinner.setAdapter(sector1Adapter);
485            sector1Spinner.setOnItemSelectedListener(new ModifierSpinnerListener());
486
487            sector2Spinner = mainModifiersDialog.findViewById(R.id.sector_2_spinner);
488            ArrayAdapter<CharSequence> sector2Adapter = ArrayAdapter.createFromResource(this,
489                    sector2ArrayID, android.R.layout.simple_spinner_item);
490            sector2Adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
491            sector2Spinner.setAdapter(sector2Adapter);
492            sector2Spinner.setOnItemSelectedListener(new ModifierSpinnerListener());
493        }
494
495        boolean isControlMeasure = (Integer.parseInt(selectedSymbolSet) == SymbolID.SymbolSet_ControlMeasure);
496        if (isControlMeasure) {
497            // set up Mines checkboxes for Control Measures
498            View mineContainer = mainModifiersDialog.findViewById(R.id.mine_type_layout);
499            sector1Spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
500                @Override
501                public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
502                    if (position == SECTOR_1_MINE_INDEX) {
503                        mineContainer.setVisibility(View.VISIBLE);
504                    } else {
505                        mineContainer.setVisibility(View.GONE);
506                    }
507                    updateSymbolPreview(); // necessary since this ItemSelectedListener overwrites the previously set ModifierSpinnerListener
508                }
509
510                @Override
511                public void onNothingSelected(AdapterView<?> parent) {
512                    // Auto-generated method stub
513                }
514            });
515
516            String line;
517            String[] segments;
518            mineSectorLookup = new SparseArray<>(51); // needs space for indices up to 50
519            try (InputStream in = this.getResources().openRawResource(R.raw.mine_bitmasks_to_codes);
520                 BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
521                while ((line = reader.readLine()) != null) {
522                    segments = line.split("\t+");
523                    // converts segments[0] binary string to int index, and reads segments[1] as string for symbol code
524                    mineSectorLookup.put(Integer.parseInt(segments[0], 2), segments[1]);
525                }
526            } catch (IOException e) {
527                throw new RuntimeException(e);
528            }
529
530            AntipersonnelMineBox = mainModifiersDialog.findViewById(R.id.antipersonnel_mine_checkbox);
531            AntipersonnelDirectionalMineBox = mainModifiersDialog.findViewById(R.id.antipersonnel_mine_directional_checkbox);
532            AntitankMineBox = mainModifiersDialog.findViewById(R.id.antitank_mine_checkbox);
533            AntitankAntihandlingMineBox = mainModifiersDialog.findViewById(R.id.antitank_mine_antihandling_checkbox);
534            WideAreaAntitankMineBox = mainModifiersDialog.findViewById(R.id.wide_area_antitank_mine_checkbox);
535            MineClusterBox = mainModifiersDialog.findViewById(R.id.mine_cluster_checkbox);
536
537            mineCheckBoxList = new ArrayList<>();
538            mineCheckBoxList.add(AntipersonnelMineBox);
539            mineCheckBoxList.add(AntipersonnelDirectionalMineBox);
540            mineCheckBoxList.add(AntitankMineBox);
541            mineCheckBoxList.add(AntitankAntihandlingMineBox);
542            mineCheckBoxList.add(WideAreaAntitankMineBox);
543            mineCheckBoxList.add(MineClusterBox);
544
545            CompoundButton.OnCheckedChangeListener mineSelectionLimiter = (cb, isChecked) -> {
546                if (isChecked) {
547                    numMinesChecked++;
548                    if (numMinesChecked == 3) {
549                        for (CheckBox mine : mineCheckBoxList) {
550                            if (!mine.isChecked()) {
551                                mine.setEnabled(false);
552                            }
553                        }
554                    }
555                } else {
556                    numMinesChecked--;
557                    for (CheckBox mine : mineCheckBoxList) {
558                        mine.setEnabled(true);
559                    }
560                }
561                updateSymbolPreview();
562            };
563
564            for (CheckBox mine : mineCheckBoxList) {
565                mine.setOnCheckedChangeListener(mineSelectionLimiter);
566            }
567        }
568
569        View countryView = mainModifiersDialog.findViewById(R.id.country_layout);
570        MSInfo msi = MSLookup.getInstance().getMSLInfo(selectedSymbolSet + selectedSymbolEntityCode, selectedSymbolVersion);
571        if (!msi.getModifiers().contains(Modifiers.AS_COUNTRY)) {
572            countryView.setVisibility(View.GONE);
573            hasCountryModifier = false;
574        } else {
575            countryView.setVisibility(View.VISIBLE);
576            hasCountryModifier = true;
577            countryTextView = mainModifiersDialog.findViewById(R.id.country_textview);
578            countryTextView.setBackgroundColor(Color.TRANSPARENT);
579
580            countryTextView.setOnClickListener(v -> {
581                countrySearchDialog = new Dialog(this);
582                countrySearchDialog.setContentView(R.layout.country_selector);
583                countrySearchDialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
584                countrySearchDialog.show();
585
586                EditText editText = countrySearchDialog.findViewById(R.id.country_edittext);
587                editText.setBackgroundColor(Color.TRANSPARENT);
588                ListView listView = countrySearchDialog.findViewById(R.id.country_listview);
589
590                ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, countryNames);
591                listView.setAdapter(adapter);
592                editText.addTextChangedListener(new TextWatcher() {
593                    @Override
594                    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
595                        // Auto-generated method stub
596                    }
597
598                    @Override
599                    public void onTextChanged(CharSequence s, int start, int before, int count) {
600                        adapter.getFilter().filter(s);
601                    }
602
603                    @Override
604                    public void afterTextChanged(Editable s) {
605                        // Auto-generated method stub
606                    }
607                });
608
609                listView.setOnItemClickListener((parent, view, position, id) -> {
610                    countryTextView.setText(adapter.getItem(position));
611                    countrySearchDialog.dismiss();
612                    updateSymbolPreview();
613                });
614            });
615        }
616
617        Button modifiersBackButton = mainModifiersDialog.findViewById(R.id.modifiers_page1_back_button);
618        modifiersBackButton.setOnClickListener(w -> mainModifiersDialog.dismiss());
619
620        ViewGroup symbolPreviewView = mainModifiersDialog.findViewById(R.id.symbol_picker_modifiers_preview);
621        TextView symbolPreviewTV = symbolPreviewView.findViewById(R.id.symbol_picker_cell_TV);
622        symbolPreviewTV.setText(selectedSymbolName);
623        TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(symbolPreviewTV, 8,
624                12, 1, TypedValue.COMPLEX_UNIT_SP);
625        ViewGroup.LayoutParams params = symbolPreviewView.getLayoutParams();
626        params.height = cellSize;
627        params.width = cellSize;
628        symbolPreviewView.setLayoutParams(params);
629
630        Button sendButton = mainModifiersDialog.findViewById(R.id.send_button);
631        sendButton.setOnClickListener(view -> onPickSymbol());
632        sendButton.setText(getString(R.string.send_btn_label, "'" + selectedSymbolName + "'"));
633
634        // build list of modifiers that aren't already determined by the symbol code
635        ArrayList<String> initialModifiers = msi.getModifiers();
636        ArrayList<String> ignoreModifiers = Modifiers.GetSymbolCodeModifiersList();
637        modifiersToGet = new ArrayList<>();
638        for (String i : initialModifiers) {
639            if (!ignoreModifiers.contains(i)) {
640                modifiersToGet.add(i);
641            }
642        }
643
644        Button extraModifiersButton = mainModifiersDialog.findViewById(R.id.extra_modifiers_button);
645        if (!modifiersToGet.isEmpty() || isControlMeasure) {
646            extraModifiersButton.setEnabled(true);
647            extraModifiersButton.setOnClickListener(v -> {
648                extraModifiersDialog = new Dialog(this);
649                extraModifiersDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
650                extraModifiersDialog.setContentView(R.layout.modifiers_page2);
651                extraModifiersDialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
652                extraModifiersDialog.show();
653
654                altitudeUnitSpinner = null;
655                altitudeModeSpinner = null;
656
657                // Attributes
658                LinearLayout attributesLayout = extraModifiersDialog.findViewById(R.id.attributes_layout);
659                CheckBox outlineCheckbox = extraModifiersDialog.findViewById(R.id.outline_checkbox);
660                // show outline checkbox just for single-point Control Measures
661                String tempCode = selectedSymbolVersion + "03" + selectedSymbolSet + "0000" + selectedSymbolEntityCode + "0000";
662                if (isControlMeasure && SymbolUtilities.isMultiPoint(tempCode) == false) {
663                    outlineCheckbox.setVisibility(View.VISIBLE);
664                }
665
666                EditText lineColorField = extraModifiersDialog.findViewById(R.id.edit_LineColor);
667                EditText fillColorField = extraModifiersDialog.findViewById(R.id.edit_FillColor);
668                EditText lineWidthField = extraModifiersDialog.findViewById(R.id.edit_LineWidth);
669                EditText textColorField = extraModifiersDialog.findViewById(R.id.edit_TextColor);
670                if (isControlMeasure) {
671                    attributesLayout.setVisibility(View.VISIBLE);
672                    // restores previous values if dialog was reopened
673                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
674                        outlineCheckbox.setChecked(Boolean.parseBoolean(attributesToSend.get(MilStdAttributes.OutlineSymbol)));
675                        lineColorField.setText(attributesToSend.getOrDefault(MilStdAttributes.LineColor, ""));
676                        fillColorField.setText(attributesToSend.getOrDefault(MilStdAttributes.FillColor, ""));
677                        lineWidthField.setText(attributesToSend.getOrDefault(MilStdAttributes.LineWidth, ""));
678                        textColorField.setText(attributesToSend.getOrDefault(MilStdAttributes.TextColor, ""));
679                    }
680                }
681
682                // Extra modifiers
683                LinearLayout ll = extraModifiersDialog.findViewById(R.id.modifiers_layout);
684                LinearLayout unitLayout = null;
685
686                for (String i : modifiersToGet) {
687                    EditText et = new EditText(this);
688                    LinearLayout.LayoutParams p = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
689                    et.setLayoutParams(p);
690
691                    // set maximum char length for the field?
692                    /*
693                    et.setFilters(new InputFilter[] { new InputFilter.LengthFilter(3) });
694                    //Disabling suggestions seems to be the only way to stop the buffer from
695                    //filling past the length limit if you keep typing.
696                    et.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
697                    */
698
699                    et.setHint(Modifiers.getModifierLetterCode(i) + ": " + Modifiers.getModifierName(i));
700                    //et.setId(i);
701                    et.setId(convertStringIDtoInt(i));
702                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
703                        et.setText(modifiersToSend.getOrDefault(i, "")); // restores previous values if dialog was reopened
704                    }
705
706                    // DateTime fields
707                    if (i == Modifiers.W_DTG_1 || i == Modifiers.W1_DTG_2) {
708                        et.setInputType(InputType.TYPE_NULL); // prevents users from typing directly in datetime field
709                        // ensures the EditText only has to be clicked once to show dialog
710                        et.setOnFocusChangeListener((d, hasFocus) -> {
711                            if (hasFocus)
712                                callDatetimeDialog(et);
713                        });
714                        et.setOnClickListener(l -> callDatetimeDialog(et));
715                    }
716
717                    // Dynamically add spinners for multi-point graphics' X (altitude/depth) Units and Mode.
718                    // (For single-point graphics, the X field is free-type.)
719                    if (i == Modifiers.X_ALTITUDE_DEPTH) {
720                        if (isControlMeasure) {
721                            LinearLayout.LayoutParams altParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
722
723                            // build unit array, label, and spinner
724                            List<String> unitArray = new ArrayList<>();
725                            for (AltitudeUnits u : AltitudeUnits.values()) {
726                                unitArray.add(u.name() + " " + u.desc);
727                            }
728
729                            unitLayout = new LinearLayout(this);
730                            unitLayout.setLayoutParams(altParams);
731                            TextView unitLabel = new TextView(this);
732                            unitLabel.setText(R.string.altitude_unit_label);
733
734                            altitudeUnitSpinner = new Spinner(this);
735                            ArrayAdapter<String> unitAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, unitArray) {
736                                @Override
737                                public View getView(int position, View convertView, ViewGroup parent) {
738                                    View v = super.getView(position, convertView, parent);
739                                    v.setMinimumHeight((int) (48*this.getContext().getResources().getDisplayMetrics().density));
740                                    return v;
741                                }
742                            };
743                            unitAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
744                            altitudeUnitSpinner.setAdapter(unitAdapter);
745                            altitudeUnitSpinner.setOnItemSelectedListener(new ModifierSpinnerListener());
746
747                            unitLayout.addView(unitLabel);
748                            unitLayout.addView(altitudeUnitSpinner);
749
750                            // build mode array, label, and spinner
751                            List<String> modeArray = new ArrayList<>();
752                            for (AltitudeModes m : AltitudeModes.values()) {
753                                modeArray.add(m.name() + " " + m.desc);
754                            }
755
756                            LinearLayout modeLayout = new LinearLayout(this);
757                            modeLayout.setLayoutParams(altParams);
758                            TextView modeLabel = new TextView(this);
759                            modeLabel.setText(R.string.altitude_mode_label);
760
761                            altitudeModeSpinner = new Spinner(this);
762                            ArrayAdapter<String> modeAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, modeArray) {
763                                @Override
764                                public View getView(int position, View convertView, ViewGroup parent) {
765                                    View v = super.getView(position, convertView, parent);
766                                    v.setMinimumHeight((int) (48*this.getContext().getResources().getDisplayMetrics().density));
767                                    return v;
768                                }
769                            };
770                            modeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
771                            altitudeModeSpinner.setAdapter(modeAdapter);
772                            altitudeModeSpinner.setOnItemSelectedListener(new ModifierSpinnerListener());
773
774                            modeLayout.addView(modeLabel);
775                            modeLayout.addView(altitudeModeSpinner);
776
777                            // change multi-point X hint and add X, Units, and Mode fields to the main view
778                            et.setHint(Modifiers.getModifierLetterCode(i) + ": " + Modifiers.getModifierName(i) + "\n(Type one or more comma-separated numbers)");
779                            ll.addView(et);
780                            ll.addView(unitLayout);
781                            ll.addView(modeLayout);
782
783                            // if the calling application passes any invalid values for altitude units or mode, this sets both to the defaults
784                            try {
785                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
786                                    altitudeUnitSpinner.setSelection(AltitudeUnits.valueOf(attributesToSend.getOrDefault(MilStdAttributes.AltitudeUnits, "1,M").split(",")[1]).index);
787                                    altitudeModeSpinner.setSelection(AltitudeModes.valueOf(attributesToSend.getOrDefault(MilStdAttributes.AltitudeMode, "AMSL")).index);
788                                }
789                            } catch (Exception e) {
790                                altitudeUnitSpinner.setSelection(AltitudeUnits.valueOf("M").index);
791                                altitudeModeSpinner.setSelection(AltitudeModes.valueOf("AMSL").index);
792                            }
793                            continue;
794                        } else {
795                            // change single-point X hint
796                            et.setHint(Modifiers.getModifierLetterCode(i) + ": " + Modifiers.getModifierName(i) + "\n(Type anything, e.g. 500 M AMSL)");
797                        }
798                    }
799                    ll.addView(et);
800                }
801
802                Button saveExtraModsButton = extraModifiersDialog.findViewById(R.id.modifiers_dialog_save_button);
803                saveExtraModsButton.setOnClickListener(w -> {
804                    // save attributes
805                    if (isControlMeasure) {
806                        attributesToSend.put(MilStdAttributes.OutlineSymbol, String.valueOf(outlineCheckbox.isChecked()));
807
808                        String value = String.valueOf(lineColorField.getText());
809                        if (value.isEmpty()) {
810                            attributesToSend.remove(MilStdAttributes.LineColor);
811                        } else {
812                            attributesToSend.put(MilStdAttributes.LineColor, value);
813                        }
814
815                        value = String.valueOf(fillColorField.getText());
816                        if (value.isEmpty()) {
817                            attributesToSend.remove(MilStdAttributes.FillColor);
818                        } else {
819                            attributesToSend.put(MilStdAttributes.FillColor, value);
820                        }
821
822                        value = String.valueOf(lineWidthField.getText());
823                        if (value.isEmpty()) {
824                            attributesToSend.remove(MilStdAttributes.LineWidth);
825                        } else {
826                            attributesToSend.put(MilStdAttributes.LineWidth, value);
827                        }
828
829                        value = String.valueOf(textColorField.getText());
830                        if (value.isEmpty()) {
831                            attributesToSend.remove(MilStdAttributes.TextColor);
832                        } else {
833                            attributesToSend.put(MilStdAttributes.TextColor, value);
834                        }
835                    }
836
837                    if (altitudeUnitSpinner != null && altitudeUnitSpinner.getVisibility() == View.VISIBLE) {
838                        // We are assuming the user will not do any unit conversion--they will always type a value in (e.g.) meters if they want to display meters.
839                        // Therefore the AltitudeUnits string should always be "1,<x>" where <x> is one of M/KM/FT/SM/FL because the conversion factor will always be 1.
840                        attributesToSend.put(MilStdAttributes.AltitudeUnits, "1," + ((String) altitudeUnitSpinner.getSelectedItem()).split(" ")[0]);
841                        attributesToSend.put(MilStdAttributes.AltitudeMode, ((String) altitudeModeSpinner.getSelectedItem()).split(" ")[0]);
842                    }
843
844                    // save extra modifiers
845                    for (String j : modifiersToGet) {
846                        EditText et = extraModifiersDialog.findViewById(convertStringIDtoInt(j));
847                        String value = String.valueOf(et.getText());
848                        if (value.isEmpty()) {
849                            modifiersToSend.remove(j);
850                        } else {
851                            modifiersToSend.put(j, value);
852                        }
853                    }
854                    extraModifiersDialog.dismiss();
855                    updateSymbolPreview();
856                });
857            });
858            updateSymbolPreview();
859        } else {
860            extraModifiersButton.setEnabled(false);
861        }
862    }
863
864    /**
865     * Used to call Datetime selector from Extra Modifiers page (should only be called from that page)
866     * @param et Datetime EditText field that called this dialog
867     */
868    private void callDatetimeDialog(EditText et) {
869        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
870            Dialog datetimeDialog = new Dialog(this);
871            datetimeDialog.setContentView(R.layout.datetime_selector);
872            datetimeDialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT);
873            datetimeDialog.show();
874
875            DatePicker datePicker = datetimeDialog.findViewById(R.id.datePicker);
876            TimePicker timePicker = datetimeDialog.findViewById(R.id.timePicker);
877            timePicker.setIs24HourView(true);
878
879            Button nowButton = datetimeDialog.findViewById(R.id.datetime_now_button);
880            nowButton.setOnClickListener(n -> {
881                Calendar c = Calendar.getInstance();
882                int year = c.get(Calendar.YEAR);
883                int month = c.get(Calendar.MONTH);
884                int day = c.get(Calendar.DAY_OF_MONTH);
885                datePicker.init(year, month, day, null);
886                timePicker.setHour(c.get(Calendar.HOUR_OF_DAY));
887                timePicker.setMinute(c.get(Calendar.MINUTE));
888
889            });
890            nowButton.callOnClick();
891
892            Button onOrderButton = datetimeDialog.findViewById(R.id.datetime_on_order_button);
893            onOrderButton.setOnClickListener(o -> {
894                et.setText("O/O");
895                datetimeDialog.dismiss();
896                updateSymbolPreview();
897            });
898
899            Button confirmTimeButton = datetimeDialog.findViewById(R.id.datetime_confirm_button);
900            confirmTimeButton.setOnClickListener(c -> {
901                Calendar myCalendar = new GregorianCalendar(datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth(), timePicker.getHour(), timePicker.getMinute());
902                Date selectedDate = myCalendar.getTime();
903                et.setText(SymbolUtilities.getDateLabel(selectedDate));
904                datetimeDialog.dismiss();
905                updateSymbolPreview();
906            });
907        }
908    }
909
910    private int getMineBitmask() {
911        int mineBitmask = 0;
912
913        if (Integer.parseInt(selectedSymbolNode.getSymbolSetCode()) == SymbolID.SymbolSet_ControlMeasure) {
914            if (AntipersonnelMineBox.isChecked())
915                mineBitmask |= ANTIPERSONNEL_MINE;
916            if (AntipersonnelDirectionalMineBox.isChecked())
917                mineBitmask |= ANTIPERSONNEL_MINE_DIRECTIONAL;
918            if (AntitankMineBox.isChecked())
919                mineBitmask |= ANTITANK_MINE;
920            if (AntitankAntihandlingMineBox.isChecked())
921                mineBitmask |= ANTITANK_MINE_ANTIHANDLING;
922            if (WideAreaAntitankMineBox.isChecked())
923                mineBitmask |= WIDE_AREA_ANTITANK_MINE;
924            if (MineClusterBox.isChecked())
925                mineBitmask |= MINE_CLUSTER;
926        }
927        return mineBitmask;
928    }
929
930    // Updates preview in configure symbol page
931    // Uses modifiers - different than update selected symbol
932    private void updateSymbolPreview() {
933        // update Control Measures modifier N ("ENY" if hostile)
934        boolean isHostile = (SymbolID.getAffiliation(getSymbolID()) == SymbolID.StandardIdentity_Affiliation_Hostile_Faker);
935        modifiersToSend.put(Modifiers.N_HOSTILE, isHostile ? "ENY" : null);
936
937        Map<String,String> modifiers = new HashMap<>();
938        for (String i : modifiersToSend.keySet()) {
939            modifiers.put(i, modifiersToSend.get(i));
940        }
941        Map<String,String> attributes = new HashMap<>();
942        for (String i : attributesToSend.keySet()) {
943            attributes.put(i, attributesToSend.get(i));
944        }
945        attributes.put(MilStdAttributes.PixelSize, "240");
946
947        ImageInfo ii = mir.RenderIcon(getSymbolID(), modifiers, attributes);
948
949        ViewGroup symbolPreviewView = mainModifiersDialog.findViewById(R.id.symbol_picker_modifiers_preview);
950        ImageView symbolPreviewIV = symbolPreviewView.findViewById(R.id.symbol_picker_cell_IV);
951        symbolPreviewIV.setBackgroundColor(Color.LTGRAY);
952
953        if (ii != null && ii.getImage() != null)
954            symbolPreviewIV.setImageBitmap(ii.getImage());
955        else
956            // If can't render show empty preview
957            symbolPreviewIV.setImageBitmap(emptyBitmap);
958    }
959
960    // Returns symbol id with no modifiers just symbol set and entity code in correct positions
961    private String getGenericSymbolID(Node symbolNode) {
962        return symbolNode.getVersion() + "03" + symbolNode.getSymbolSetCode() + "0011" + symbolNode.getCode() + "00000000000000";
963    }
964
965    // Includes modifiers in code
966    private String getSymbolID() {
967        final String selectedSymbolSet = selectedSymbolNode.getSymbolSetCode();
968        final String selectedSymbolEntityCode = selectedSymbolNode.getCode();
969
970        // Resource IDs will be non-final by default in Android Gradle Plugin version 8.0,
971        // so Google recommends using if/else for them instead of switch statements.
972        int checkedRadioButtonId = contextRadioGroup.getCheckedRadioButtonId();
973        String contextCode;
974        if (checkedRadioButtonId == R.id.context_radio_exercise) {
975            contextCode = String.valueOf(SymbolID.StandardIdentity_Context_Exercise);
976        } else if (checkedRadioButtonId == R.id.context_radio_simulation) {
977            contextCode = String.valueOf(SymbolID.StandardIdentity_Context_Simulation);
978        } else {
979            contextCode = String.valueOf(SymbolID.StandardIdentity_Context_Reality);
980        }
981
982        // For spinners, the selected item position equals the digit to use in the symbol code
983        // (except for ampTypeSpinner, which is position+1 when not category Unknown).
984        String stdIdCode = String.valueOf(stdIdSpinner.getSelectedItemPosition());
985        String statusCode = String.valueOf(statusSpinner.getSelectedItemPosition());
986        String hqCode = String.valueOf(hqSpinner.getSelectedItemPosition());
987
988        String amplifierCode = "00";
989        if (ampCategorySpinner.getSelectedItemPosition() != 0) {
990            amplifierCode = ampCategorySpinner.getSelectedItemPosition() +
991                    String.valueOf(ampTypeSpinner.getSelectedItemPosition() + 1);
992        }
993
994        String sector1Code = "00";
995        String sector2Code = "00";
996        if (hasSectorModifiers) {
997            DecimalFormat twoDigitFormatter = new DecimalFormat("00");
998            sector1Code = twoDigitFormatter.format(sector1Spinner.getSelectedItemPosition());
999            sector2Code = twoDigitFormatter.format(sector2Spinner.getSelectedItemPosition());
1000
1001            // (Control Measures) Gets symbol code section matching mineBitmask, or returns value for "Unspecified Mine" if combination is not found.
1002            if (Integer.parseInt(selectedSymbolSet) == SymbolID.SymbolSet_ControlMeasure && Integer.parseInt(sector1Code) == SECTOR_1_MINE_INDEX) {
1003                sector1Code = mineSectorLookup.get(getMineBitmask(), String.valueOf(SECTOR_1_MINE_INDEX));
1004            }
1005        }
1006
1007        String countryCode = "000";
1008        if (hasCountryModifier) {
1009            String key = (String) countryTextView.getText();
1010            if (!Objects.equals(key, "")) {
1011                countryCode = countryMap.get(key);
1012            }
1013        }
1014
1015        return selectedSymbolNode.getVersion() + contextCode + stdIdCode + selectedSymbolSet + statusCode + hqCode + amplifierCode +
1016                selectedSymbolEntityCode + sector1Code + sector2Code + "0000000" + countryCode;
1017    }
1018
1019    // Called when pick button is selected from the configure window
1020    private void onPickSymbol() {
1021        String canRender;
1022        int mineBitmask = getMineBitmask();
1023        final String symbolID = getSymbolID();
1024        if (SymbolUtilities.isMultiPoint(symbolID)) {
1025            Map<String,String> modifiers = new HashMap<>();
1026            for (String i : modifiersToSend.keySet()) {
1027                modifiers.put(i, modifiersToSend.get(i));
1028            }
1029            canRender = MultiPointHandler.canRenderMultiPoint(symbolID, modifiers, Integer.MAX_VALUE);
1030            if (!canRender.equals("true")) {
1031                // Clean up error message for user
1032                canRender = selectedSymbolNode.getName() + canRender.substring(30);
1033                canRender = canRender.replace("a modifiers object that has ", "");
1034            }
1035        } else {
1036            Map<String,String> attributes = new HashMap<>();
1037            for (String i : attributesToSend.keySet()) {
1038                attributes.put(i, attributesToSend.get(i));
1039            }
1040            if (MilStdIconRenderer.getInstance().CanRender(symbolID, attributes)) {
1041                canRender = "true";
1042            } else {
1043                // Shouldn't be able to get here with a single point that can't be rendered
1044                canRender = "Unable to render " + selectedSymbolNode.getName();
1045            }
1046        }
1047
1048        if (invalidMineTrios.contains(mineBitmask)) {
1049            Toast.makeText(mainModifiersDialog.getContext(), "Invalid combination of mines. Please make a different selection.", Toast.LENGTH_LONG).show();
1050        } else if (canRender.equals("true")) {
1051            mainModifiersDialog.dismiss();
1052            Intent resultIntent = new Intent();
1053            resultIntent.putExtra(selectedSymbolIdKey, getSymbolID());
1054            resultIntent.putExtra(modifiersKey, modifiersToSend);
1055            resultIntent.putExtra(attributesKey, attributesToSend);
1056            setResult(Activity.RESULT_OK, resultIntent);
1057            finish();
1058        } else {
1059            Toast.makeText(mainModifiersDialog.getContext(), canRender, Toast.LENGTH_LONG).show();
1060        }
1061    }
1062
1063    // Updates selected symbol variable, selected symbol preview and symbols on page if necessary
1064    private void updateSelectedSymbol(Node newSelectedSymbol) {
1065        // Update the selected symbol set code
1066        selectedSymbolNode = newSelectedSymbol;
1067        ImageView selectedSymbolIV = findViewById(R.id.selected_symbol_iv);
1068        Bitmap render = getRender(selectedSymbolNode);
1069
1070        String selectedSymbolName;
1071        if (selectedSymbolNode.getName().equalsIgnoreCase("root")) {
1072            selectedSymbolName = "Symbol";
1073        } else {
1074            selectedSymbolName = selectedSymbolNode.getName();
1075        }
1076        configureButton.setText(getString(R.string.configure_btn_label, "'" + selectedSymbolName + "'"));
1077
1078        if (render != null) {
1079            selectedSymbolIV.setImageBitmap(render);
1080            configureButton.setEnabled(canRender(getGenericSymbolID(selectedSymbolNode)));
1081
1082            // change button text for weather symbols because the modifier dialogs are skipped
1083            switch (Integer.parseInt(selectedSymbolNode.getSymbolSetCode())) {
1084                case SymbolID.SymbolSet_Atmospheric:
1085                case SymbolID.SymbolSet_Oceanographic:
1086                case SymbolID.SymbolSet_MeteorologicalSpace:
1087                    configureButton.setText(getString(R.string.send_btn_label, "'" + selectedSymbolName + "'"));
1088            }
1089        } else {
1090            selectedSymbolIV.setImageResource(R.drawable.baseline_folder_24);
1091            configureButton.setEnabled(false);
1092        }
1093
1094        if (!newSelectedSymbol.getChildren().isEmpty()) {
1095            pageTrail.add(selectedSymbolNode);
1096            updateSymbolTable();
1097        }
1098    }
1099
1100    // Updates symbols in page based on top of pageTrail
1101    private void updateSymbolTable() {
1102        Node symbolTree = pageTrail.peek();
1103        ArrayList<Node> symbols; // Symbols to be in new page
1104
1105        // Scroll to top
1106        GridView symbolTable = findViewById(R.id.symbol_picker_table);
1107        symbolTable.smoothScrollToPositionFromTop(0, 0, 0);
1108
1109        if (!flattenTreeToggle.isChecked()) {
1110            symbols = new ArrayList<>(symbolTree.getChildren());
1111        } else {
1112            symbols = new ArrayList<>(symbolTree.flatten());
1113        }
1114
1115        TextView symbolPath = findViewById(R.id.symbol_picker_path);
1116
1117        StringBuilder pathStr = new StringBuilder();
1118        if (pageTrail.size() == 1) {
1119            pathStr.append("Home");
1120        } else if (activeSearch) {
1121            int searchNodeIndex = 1;
1122            while (!pageTrail.get(searchNodeIndex).getName().equals(searchNodeName)) {
1123                searchNodeIndex++;
1124            }
1125
1126            pathStr.append("Search");
1127            for (int i = searchNodeIndex + 1; i < pageTrail.size(); i++)
1128                pathStr.append(" > ").append(pageTrail.get(i).getName());
1129        } else {
1130            pathStr.append(pageTrail.get(1).getName());
1131            for (int i = 2; i < pageTrail.size(); i++)
1132                pathStr.append(" > ").append(pageTrail.get(i).getName());
1133        }
1134        symbolPath.setText(pathStr);
1135
1136        // (optional) sort symbols by name
1137        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1138            symbols.sort(Comparator.comparing(Node::getName));
1139        }
1140
1141        symbolTableAdapter.clear();
1142        symbolTableAdapter.addAll(symbols);
1143    }
1144
1145    // returns null if can't render or other error
1146    // Does not include modifiers
1147    private Bitmap getRender(Node symbolNode) {
1148        final String version = symbolNode.getVersion();
1149        final String symbolSetCode = symbolNode.getSymbolSetCode();
1150        final String entityCode = symbolNode.getCode();
1151        String symbolID = getGenericSymbolID(symbolNode);
1152
1153        if (version.equals("XX") || entityCode.equals("XXXXXX") || entityCode.equals("XX"))
1154            // Error in code
1155            return null;
1156        else if (entityCode.equals("000000") &&
1157                (symbolSetCode.equals(SymbolID.SymbolSet_Atmospheric + "") ||
1158                        symbolSetCode.equals(SymbolID.SymbolSet_ControlMeasure + "") ||
1159                        symbolSetCode.equals(SymbolID.SymbolSet_MineWarfare + "") ||
1160                        symbolSetCode.equals(SymbolID.SymbolSet_Oceanographic + "")))
1161            // Top level symbol set that can't be rendered
1162            return null;
1163        else if (!entityCode.equals("000000") && !canRender(symbolID))
1164            // Check if can render icon. canRender() says it can't render "000000" but RenderIcon() will
1165            // return an empty frame for the symbol sets not excluded above
1166            return null;
1167
1168        Map<String,String> modifiers = new HashMap<>();
1169        Map<String,String> attributes = new HashMap<>();
1170
1171        attributes.put(MilStdAttributes.PixelSize, "50");
1172        attributes.put(MilStdAttributes.DrawAsIcon, "true"); // Make all symbols same size
1173
1174        ImageInfo ii = mir.RenderIcon(symbolID, modifiers, attributes);
1175        if (ii != null)
1176            return ii.getImage();
1177        else
1178            return null;
1179    }
1180
1181    // Same functionality and MilStdIconRenderer.CanRender() - doesn't log if can't render
1182    // Symbol picker calls canRender() with symbols it doesn't expect to be valid
1183    private Boolean canRender(String symbolID) {
1184        int version = SymbolID.getVersion(symbolID);
1185        String lookupID = SymbolUtilities.getBasicSymbolID(symbolID);
1186        String lookupSVGID = SVGLookup.getMainIconID(symbolID);
1187        MSInfo msi = MSLookup.getInstance().getMSLInfo(lookupID,SymbolID.getVersion(symbolID));
1188        SVGInfo si = SVGLookup.getInstance().getSVGLInfo(lookupSVGID, version);
1189
1190        // msi should never be null
1191        return msi != null && msi.getDrawRule() != DrawRules.DONOTDRAW && si != null;
1192    }
1193
1194    private class AmplifierSelectedItemListener implements AdapterView.OnItemSelectedListener {
1195        @Override
1196        // specifically used for the Amplifier Category spinner to show/hide and change the subtype spinner
1197        public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
1198            int arrayId;
1199            switch (ampCategorySpinner.getSelectedItemPosition()) {
1200                case 1:
1201                    arrayId = R.array.amp1_echelon_bb_array;
1202                    break;
1203                case 2:
1204                    arrayId = R.array.amp2_echelon_da_array;
1205                    break;
1206                case 3:
1207                    arrayId = R.array.amp3_eqp_land_array;
1208                    break;
1209                case 4:
1210                    arrayId = R.array.amp4_eqp_snow_array;
1211                    break;
1212                case 5:
1213                    arrayId = R.array.amp5_eqp_water_array;
1214                    break;
1215                case 6:
1216                    arrayId = R.array.amp6_naval_towed_array;
1217                    break;
1218                case 0:
1219                default:
1220                    arrayId = R.array.amp0_unknown_array;
1221                    break;
1222            }
1223            if (arrayId == R.array.amp0_unknown_array) {
1224                ampTypeLayout.setVisibility(View.GONE);
1225            } else {
1226                ampTypeLayout.setVisibility(View.VISIBLE);
1227            }
1228            ArrayAdapter<CharSequence> ampTypeAdapter = ArrayAdapter.createFromResource(SymbolPickerActivity.this,
1229                    arrayId, android.R.layout.simple_spinner_item);
1230            ampTypeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1231            ampTypeSpinner.setAdapter(ampTypeAdapter);
1232
1233            updateSymbolPreview(); // necessary for ampCategorySpinner since this AmplifierSelectedItemListener is used instead of ModifierSpinnerListener
1234        }
1235
1236        @Override
1237        public void onNothingSelected(AdapterView<?> adapterView) {
1238            // Auto-generated method stub
1239        }
1240    }
1241
1242    private class SearchBoxListener implements SearchView.OnQueryTextListener {
1243        private List<Node> searchSymbolTree(String searchQuery) {
1244            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1245                // Search by name
1246                List<Node> searchResults = treeManager.mil2525Tree.flatten().stream()
1247                        .filter(node -> node.getName().trim().toLowerCase(Locale.ROOT).replaceAll("-", " ")
1248                                .contains(searchQuery.toLowerCase(Locale.ROOT).replaceAll("-", " "))).collect(Collectors.toList());
1249                // Search by entity code with symbol set
1250                searchResults.addAll(treeManager.mil2525Tree.flatten().stream()
1251                        .filter(node -> (node.getSymbolSetCode() + node.getCode())
1252                                .contains(searchQuery)).collect(Collectors.toList()));
1253                // Search by entity code
1254                searchResults.addAll(treeManager.mil2525Tree.flatten().stream()
1255                        .filter(node -> node.getCode().contains(searchQuery)).collect(Collectors.toList()));
1256
1257                // Remove duplicates
1258                searchResults = searchResults.stream().distinct().collect(Collectors.toList());
1259
1260                return searchResults;
1261            }
1262            else
1263                return null;
1264        }
1265
1266        @Override
1267        public boolean onQueryTextSubmit(String s) {
1268            return false;
1269        }
1270
1271        // Removes search page if query is empty or updates table with search results
1272        @Override
1273        public boolean onQueryTextChange(String s) {
1274            // Remove all pages at and below searchNode
1275            while (activeSearch) {
1276                if (pageTrail.pop().getName().equals(searchNodeName)) {
1277                    activeSearch = false;
1278                }
1279            }
1280
1281            if (s != null && !s.equals("")) {
1282                List<Node> searchResults = searchSymbolTree(s);
1283                Node searchNode = new Node(searchNodeName, "XX", "XX", "XX");
1284                searchNode.addChildren(searchResults);
1285                pageTrail.add(searchNode);
1286                activeSearch = true;
1287            }
1288            updateSymbolTable();
1289            return true;
1290        }
1291    }
1292
1293    /**
1294     * Symbol Grid View Adapter
1295     * Renders views for symbols from ArrayList of tree nodes for each page
1296     */
1297    private class SymbolGVAdapter extends ArrayAdapter<Node> {
1298        public SymbolGVAdapter(Context context, ArrayList<Node> nodeArrayList) {
1299            super(context, 0, nodeArrayList);
1300        }
1301
1302        // Gets individual view for symbol for table
1303        @Override
1304        public View getView(int position, View convertView, ViewGroup parent) {
1305            Node symbolTree = getItem(position);
1306
1307            if (convertView == null) {
1308                // Layout Inflater inflates each item to be displayed in GridView
1309                convertView = LayoutInflater.from(getContext()).inflate(R.layout.symbol_cell, parent, false);
1310            }
1311            ViewGroup symbolCell = (ViewGroup) convertView;
1312
1313            // On cell click either open folder or select symbol
1314            symbolCell.setOnClickListener(view -> updateSelectedSymbol(symbolTree));
1315
1316            TextView symbolTV = symbolCell.findViewById(R.id.symbol_picker_cell_TV);
1317            symbolTV.setText(symbolTree.getName());
1318            // Add auto text sizing to fit longer symbol names
1319            TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(symbolTV, 8,
1320                    12, 1, TypedValue.COMPLEX_UNIT_SP);
1321            ImageView symbolIV = symbolCell.findViewById(R.id.symbol_picker_cell_IV);
1322            Bitmap render = getRender(symbolTree);
1323            if (render != null)
1324                // Whether folder or leaf set icon if can render
1325                symbolIV.setImageBitmap(render);
1326            else if (!symbolTree.getChildren().isEmpty())
1327                // Can't render use folder icon
1328                symbolIV.setImageResource(R.drawable.baseline_folder_24);
1329            else
1330                // Can't render leaf node
1331                symbolIV.setImageBitmap(emptyBitmap);
1332
1333            // Add a folder icon in corner if rendering a symbol with children
1334            ImageView cellFolder = symbolCell.findViewById(R.id.symbol_picker_cell_folder);
1335            if (render != null && !symbolTree.getChildren().isEmpty())
1336                cellFolder.setImageResource(R.drawable.baseline_folder_24);
1337            else
1338                cellFolder.setImageBitmap(emptyBitmap);
1339
1340            ViewGroup.LayoutParams params = symbolCell.getLayoutParams();
1341            params.height = cellSize;
1342            params.width = cellSize;
1343            symbolCell.setLayoutParams(params);
1344
1345            return symbolCell;
1346        }
1347    }
1348
1349    private int convertStringIDtoInt(String key)
1350    {
1351        try {
1352            StringBuilder sb = new StringBuilder();
1353            if (key != null & key.length() > 0)
1354            {
1355                /*for (int i = 0; i < key.length(); i++) {
1356                    String temp = String.valueOf((int) key.charAt(i));
1357                    if (temp.length() == 1)
1358                        temp = "0" + temp;
1359                    sb.append(temp);
1360                }//*/
1361                for (int i = 0; (i < 3); i++) {
1362                    String temp = String.valueOf((int) key.charAt(i));
1363                    sb.append(temp);
1364                }
1365                return Integer.parseInt(sb.toString());
1366            } else
1367                return -1;
1368        }
1369        catch(Exception exc)
1370        {
1371            System.out.println(exc.getMessage());
1372        }
1373        return -1;
1374    }
1375
1376    private String convertIntToStringID(int id)
1377    {
1378        if(id==-1)
1379            return null;
1380
1381        StringBuilder sb = new StringBuilder();
1382        String tempID = String.valueOf(id);
1383        for(int i = 0; i+1 < tempID.length(); i=i+2)
1384        {
1385            int c = Integer.parseInt(tempID.substring(i,i+2));
1386
1387            sb.append(Character.toChars(c));
1388        }
1389        return sb.toString();
1390    }
1391}