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