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