001/*
002 * Copyright (C) Photon Vision.
003 *
004 * This program is free software: you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License as published by
006 * the Free Software Foundation, either version 3 of the License, or
007 * (at your option) any later version.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
016 */
017
018package org.photonvision.vision.processes;
019
020import java.lang.reflect.Field;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Comparator;
024import java.util.List;
025import org.photonvision.common.configuration.CameraConfiguration;
026import org.photonvision.common.configuration.ConfigManager;
027import org.photonvision.common.dataflow.DataChangeService;
028import org.photonvision.common.dataflow.events.OutgoingUIEvent;
029import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
030import org.photonvision.common.logging.LogGroup;
031import org.photonvision.common.logging.Logger;
032import org.photonvision.vision.pipeline.*;
033
034@SuppressWarnings({"rawtypes", "unused"})
035public class PipelineManager {
036    private static final Logger logger = new Logger(PipelineManager.class, LogGroup.VisionModule);
037
038    public static final int DRIVERMODE_INDEX = -1;
039    public static final int CAL_3D_INDEX = -2;
040
041    protected final List<CVPipelineSettings> userPipelineSettings;
042    protected final Calibrate3dPipeline calibration3dPipeline;
043    protected final DriverModePipeline driverModePipeline = new DriverModePipeline();
044
045    /** Index of the currently active pipeline. Defaults to 0. */
046    private int currentPipelineIndex = DRIVERMODE_INDEX;
047
048    /** The currently active pipeline. */
049    private CVPipeline currentUserPipeline = driverModePipeline;
050
051    /**
052     * Index of the last active user-created pipeline. <br>
053     * <br>
054     * Used only when switching from any of the built-in pipelines back to a user-created pipeline.
055     */
056    private int lastUserPipelineIdx;
057
058    /**
059     * Creates a PipelineManager with a DriverModePipeline, a Calibration3dPipeline, and all provided
060     * pipelines.
061     */
062    PipelineManager(
063            DriverModePipelineSettings driverSettings,
064            List<CVPipelineSettings> userPipelines,
065            int defaultIndex) {
066        this.userPipelineSettings = new ArrayList<>(userPipelines);
067        // This is to respect the default res idx for vendor cameras
068
069        this.driverModePipeline.setSettings(driverSettings);
070
071        if (userPipelines.isEmpty()) addPipeline(PipelineType.AprilTag);
072
073        calibration3dPipeline = new Calibrate3dPipeline();
074
075        // We know that at this stage, VisionRunner hasn't yet started so we're good to
076        // do this from
077        // this thread
078        this.setIndex(defaultIndex);
079        updatePipelineFromRequested();
080    }
081
082    public PipelineManager(CameraConfiguration config) {
083        this(config.driveModeSettings, config.pipelineSettings, config.currentPipelineIndex);
084    }
085
086    /**
087     * Get the settings for a pipeline by index.
088     *
089     * @param index Index of pipeline whose settings need getting.
090     * @return The gotten settings of the pipeline whose index was provided.
091     */
092    public CVPipelineSettings getPipelineSettings(int index) {
093        if (index < 0) {
094            switch (index) {
095                case DRIVERMODE_INDEX:
096                    return driverModePipeline.getSettings();
097                case CAL_3D_INDEX:
098                    return calibration3dPipeline.getSettings();
099            }
100        }
101
102        for (var setting : userPipelineSettings) {
103            if (setting.pipelineIndex == index) return setting;
104        }
105        return null;
106    }
107
108    /**
109     * Get the settings for a pipeline by index.
110     *
111     * @param index Index of pipeline whose nickname needs getting.
112     * @return the nickname of the pipeline whose index was provided.
113     */
114    public String getPipelineNickname(int index) {
115        if (index < 0) {
116            switch (index) {
117                case DRIVERMODE_INDEX:
118                    return driverModePipeline.getSettings().pipelineNickname;
119                case CAL_3D_INDEX:
120                    return calibration3dPipeline.getSettings().pipelineNickname;
121            }
122        }
123
124        for (var setting : userPipelineSettings) {
125            if (setting.pipelineIndex == index) return setting.pipelineNickname;
126        }
127        return null;
128    }
129
130    /**
131     * Gets a list of nicknames for all user pipelines
132     *
133     * @return The list of nicknames for all user pipelines
134     */
135    public List<String> getPipelineNicknames() {
136        List<String> ret = new ArrayList<>();
137        for (var p : userPipelineSettings) {
138            ret.add(p.pipelineNickname);
139        }
140        return ret;
141    }
142
143    /**
144     * Gets the index of the currently active pipeline
145     *
146     * @return The index of the currently active pipeline
147     */
148    public int getCurrentPipelineIndex() {
149        return currentPipelineIndex;
150    }
151
152    /**
153     * Get the currently active pipeline.
154     *
155     * @return The currently active pipeline.
156     */
157    public CVPipeline getCurrentPipeline() {
158        updatePipelineFromRequested();
159        if (currentPipelineIndex < 0) {
160            switch (currentPipelineIndex) {
161                case CAL_3D_INDEX:
162                    return calibration3dPipeline;
163                case DRIVERMODE_INDEX:
164                    return driverModePipeline;
165            }
166        }
167
168        // Just return the current user pipeline, we're not on aa built-in one
169        return currentUserPipeline;
170    }
171
172    /**
173     * Get the currently active pipelines settings
174     *
175     * @return The currently active pipelines settings
176     */
177    public CVPipelineSettings getCurrentPipelineSettings() {
178        return getPipelineSettings(currentPipelineIndex);
179    }
180
181    private volatile int requestedIndex = 0;
182
183    /**
184     * Grab the currently requested pipeline index. The VisionRunner may not have changed over to this
185     * pipeline yet.
186     */
187    public int getRequestedIndex() {
188        return requestedIndex;
189    }
190
191    /**
192     * Internal method for setting the active pipeline. <br>
193     * <br>
194     * All externally accessible methods that intend to change the active pipeline MUST go through
195     * here to ensure all proper steps are taken.
196     *
197     * @param newIndex Index of pipeline to be active
198     */
199    private void setPipelineInternal(int newIndex) {
200        requestedIndex = newIndex;
201    }
202
203    /**
204     * Based on a requested pipeline index, create/destroy pipelines as necessary. We do this as a
205     * side effect of the main thread that calls getCurrentPipeline to avoid race conditions between
206     * server threads and the VisionRunner TODO: this should be refactored. Shame Java doesn't have
207     * RAII
208     */
209    private void updatePipelineFromRequested() {
210        int newIndex = requestedIndex;
211        if (newIndex == currentPipelineIndex) {
212            // nothing to do, probably no change -- give up
213            return;
214        }
215
216        if (newIndex < 0 && currentPipelineIndex >= 0) {
217            // Transitioning to a built-in pipe, save off the current user one
218            lastUserPipelineIdx = currentPipelineIndex;
219        }
220
221        if (userPipelineSettings.size() - 1 < newIndex) {
222            logger.warn("User attempted to set index to non-existent pipeline!");
223            return;
224        }
225
226        currentPipelineIndex = newIndex;
227
228        if (newIndex >= 0) {
229            recreateUserPipeline();
230        }
231
232        DataChangeService.getInstance()
233                .publishEvent(
234                        new OutgoingUIEvent<>(
235                                "fullsettings",
236                                UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
237    }
238
239    /**
240     * Recreate the current user pipeline with the current pipeline index. Useful to force a
241     * recreation after changing pipeline type
242     */
243    private void recreateUserPipeline() {
244        // Cleanup potential old native resources before swapping over from a user pipeline
245        if (currentUserPipeline != null && !(currentPipelineIndex < 0)) {
246            currentUserPipeline.release();
247        }
248
249        var desiredPipelineSettings = userPipelineSettings.get(currentPipelineIndex);
250        switch (desiredPipelineSettings.pipelineType) {
251            case Reflective -> {
252                logger.debug("Creating Reflective pipeline");
253                currentUserPipeline =
254                        new ReflectivePipeline((ReflectivePipelineSettings) desiredPipelineSettings);
255            }
256            case ColoredShape -> {
257                logger.debug("Creating ColoredShape pipeline");
258                currentUserPipeline =
259                        new ColoredShapePipeline((ColoredShapePipelineSettings) desiredPipelineSettings);
260            }
261            case AprilTag -> {
262                logger.debug("Creating AprilTag pipeline");
263                currentUserPipeline =
264                        new AprilTagPipeline((AprilTagPipelineSettings) desiredPipelineSettings);
265            }
266            case Aruco -> {
267                logger.debug("Creating Aruco Pipeline");
268                currentUserPipeline = new ArucoPipeline((ArucoPipelineSettings) desiredPipelineSettings);
269            }
270            case ObjectDetection -> {
271                logger.debug("Creating ObjectDetection Pipeline");
272                currentUserPipeline =
273                        new ObjectDetectionPipeline((ObjectDetectionPipelineSettings) desiredPipelineSettings);
274            }
275            case Calib3d, DriverMode -> {}
276        }
277    }
278
279    /**
280     * Enters or exits calibration mode based on the parameter. <br>
281     * <br>
282     * Exiting returns to the last used user pipeline.
283     *
284     * @param wantsCalibration True to enter calibration mode, false to exit calibration mode.
285     */
286    public void setCalibrationMode(boolean wantsCalibration) {
287        if (!wantsCalibration) calibration3dPipeline.finishCalibration();
288        setPipelineInternal(wantsCalibration ? CAL_3D_INDEX : lastUserPipelineIdx);
289    }
290
291    /**
292     * Enters or exits driver mode based on the parameter. <br>
293     * <br>
294     * Exiting returns to the last used user pipeline.
295     *
296     * @param state True to enter driver mode, false to exit driver mode.
297     */
298    public void setDriverMode(boolean state) {
299        setPipelineInternal(state ? DRIVERMODE_INDEX : lastUserPipelineIdx);
300    }
301
302    /**
303     * Returns whether driver mode is active.
304     *
305     * @return Whether driver mode is active.
306     */
307    public boolean getDriverMode() {
308        return currentPipelineIndex == DRIVERMODE_INDEX;
309    }
310
311    public static final Comparator<CVPipelineSettings> PipelineSettingsIndexComparator =
312            Comparator.comparingInt(o -> o.pipelineIndex);
313
314    /**
315     * Sorts the pipeline list by index, and reassigns their indexes to match the new order. <br>
316     * <br>
317     * I don't like this, but I have no other ideas, and it works so
318     */
319    private void reassignIndexes() {
320        userPipelineSettings.sort(PipelineSettingsIndexComparator);
321        for (int i = 0; i < userPipelineSettings.size(); i++) {
322            userPipelineSettings.get(i).pipelineIndex = i;
323        }
324    }
325
326    public CVPipelineSettings addPipeline(PipelineType type) {
327        return addPipeline(type, "New Pipeline");
328    }
329
330    public CVPipelineSettings addPipeline(PipelineType type, String nickname) {
331        var added = createSettingsForType(type, nickname);
332        if (added == null) {
333            logger.error("Cannot add null pipeline!");
334            return null;
335        }
336        addPipelineInternal(added);
337        reassignIndexes();
338        return added;
339    }
340
341    private CVPipelineSettings createSettingsForType(PipelineType type, String nickname) {
342        switch (type) {
343            case Reflective -> {
344                var added = new ReflectivePipelineSettings();
345                added.pipelineNickname = nickname;
346                return added;
347            }
348            case ColoredShape -> {
349                var added = new ColoredShapePipelineSettings();
350                added.pipelineNickname = nickname;
351                return added;
352            }
353            case AprilTag -> {
354                var added = new AprilTagPipelineSettings();
355                added.pipelineNickname = nickname;
356                return added;
357            }
358            case Aruco -> {
359                var added = new ArucoPipelineSettings();
360                added.pipelineNickname = nickname;
361                return added;
362            }
363            case ObjectDetection -> {
364                var added = new ObjectDetectionPipelineSettings();
365                added.pipelineNickname = nickname;
366                return added;
367            }
368            case Calib3d, DriverMode -> {
369                logger.error("Got invalid pipeline type: " + type);
370                return null;
371            }
372        }
373
374        // This can never happen, this is here to satisfy the compiler.
375        throw new IllegalStateException("Got impossible pipeline type: " + type);
376    }
377
378    private void addPipelineInternal(CVPipelineSettings settings) {
379        settings.pipelineIndex = userPipelineSettings.size();
380        userPipelineSettings.add(settings);
381        reassignIndexes();
382    }
383
384    /**
385     * Remove a pipeline settings at the given index and return the new current index
386     *
387     * @param index The idx to remove
388     */
389    private int removePipelineInternal(int index) {
390        userPipelineSettings.remove(index);
391        currentPipelineIndex = Math.min(index, userPipelineSettings.size() - 1);
392        reassignIndexes();
393        return currentPipelineIndex;
394    }
395
396    public void setIndex(int index) {
397        this.setPipelineInternal(index);
398    }
399
400    public int removePipeline(int index) {
401        if (index < 0) {
402            return currentPipelineIndex;
403        }
404        // TODO should we block/lock on a mutex?
405        return removePipelineInternal(index);
406    }
407
408    public void renameCurrentPipeline(String newName) {
409        getCurrentPipelineSettings().pipelineNickname = newName;
410    }
411
412    /**
413     * Duplicate a pipeline at a given index
414     *
415     * @param index the index of the target pipeline
416     * @return The new index
417     */
418    public int duplicatePipeline(int index) {
419        var settings = userPipelineSettings.get(index);
420        var newSettings = settings.clone();
421        newSettings.pipelineNickname =
422                createUniqueName(settings.pipelineNickname, userPipelineSettings);
423        newSettings.pipelineIndex = Integer.MAX_VALUE;
424        logger.debug("Duplicating pipe " + index + " to " + newSettings.pipelineNickname);
425        userPipelineSettings.add(newSettings);
426        reassignIndexes();
427
428        // Now we look for the index of the new pipeline and return it
429        return userPipelineSettings.indexOf(newSettings);
430    }
431
432    private static String createUniqueName(
433            String nickname, List<CVPipelineSettings> existingSettings) {
434        StringBuilder uniqueName = new StringBuilder(nickname);
435        while (true) {
436            String finalUniqueName = uniqueName.toString(); // To get around lambda capture
437            var conflictingName =
438                    existingSettings.stream().anyMatch(it -> it.pipelineNickname.equals(finalUniqueName));
439
440            if (!conflictingName) {
441                // If no conflict, we're done
442                return uniqueName.toString();
443            } else {
444                // Otherwise, we need to add a suffix to the name
445                // If the string doesn't already end in "([0-9]*)", we'll add it
446                // If it does, we'll increment the number in the suffix
447
448                if (uniqueName.toString().matches(".*\\([0-9]*\\)")) {
449                    // Because java strings are immutable, we have to do this curstedness
450                    // This is like doing "New pipeline (" + 2 + ")"
451
452                    var parenStart = uniqueName.toString().lastIndexOf('(');
453                    var parenEnd = uniqueName.length() - 1;
454                    var number = Integer.parseInt(uniqueName.substring(parenStart + 1, parenEnd)) + 1;
455
456                    uniqueName = new StringBuilder(uniqueName.substring(0, parenStart + 1) + number + ")");
457                } else {
458                    uniqueName.append(" (1)");
459                }
460            }
461        }
462    }
463
464    private static List<Field> getAllFields(Class base) {
465        List<Field> ret = new ArrayList<>();
466        ret.addAll(List.of(base.getDeclaredFields()));
467        var superclazz = base.getSuperclass();
468        if (superclazz != null) {
469            ret.addAll(getAllFields(superclazz));
470        }
471
472        return ret;
473    }
474
475    public void changePipelineType(int newType) {
476        // Find the PipelineType proposed
477        // To do this we look at all the PipelineType entries and look for one with
478        // matching
479        // base indexes
480        PipelineType type =
481                Arrays.stream(PipelineType.values())
482                        .filter(it -> it.baseIndex == newType)
483                        .findAny()
484                        .orElse(null);
485        if (type == null) {
486            logger.error("Could not match type " + newType + " to a PipelineType!");
487            return;
488        }
489
490        if (type.baseIndex == getCurrentPipelineSettings().pipelineType.baseIndex) {
491            logger.debug(
492                    "Not changing settings as "
493                            + type
494                            + " and "
495                            + getCurrentPipelineSettings().pipelineType
496                            + " are identical!");
497            return;
498        }
499
500        var idx = currentPipelineIndex;
501        if (idx < 0) {
502            logger.error("Cannot replace non-user pipeline!");
503            return;
504        }
505
506        // The settings we used to have
507        var oldSettings = userPipelineSettings.get(idx);
508
509        var name = getCurrentPipelineSettings().pipelineNickname;
510        // Dummy settings to copy common fields over
511        var newSettings = createSettingsForType(type, name);
512
513        // Copy all fields from AdvancedPipelineSettings/its superclasses from old to new
514        try {
515            for (Field field : getAllFields(AdvancedPipelineSettings.class)) {
516                if (field.isAnnotationPresent(SuppressSettingCopy.class)) {
517                    // Skip fields that are annotated with SuppressSettingCopy
518                    continue;
519                }
520                Object value = field.get(oldSettings);
521                logger.debug("setting " + field.getName() + " to " + value);
522                field.set(newSettings, value);
523            }
524        } catch (Exception e) {
525            logger.error("Couldn't copy old settings", e);
526        }
527
528        logger.info("Adding new pipe of type " + type + " at idx " + idx);
529
530        userPipelineSettings.set(idx, newSettings);
531
532        setPipelineInternal(idx);
533        reassignIndexes();
534        recreateUserPipeline();
535    }
536}