001package jmri.server.json.panel; 002 003import com.fasterxml.jackson.databind.JsonNode; 004import java.beans.PropertyChangeEvent; 005import java.beans.PropertyChangeListener; 006import java.io.IOException; 007import java.util.concurrent.Executors; 008import java.util.concurrent.ScheduledExecutorService; 009import java.util.concurrent.ScheduledFuture; 010import java.util.concurrent.ThreadFactory; 011import java.util.concurrent.TimeUnit; 012import jmri.InstanceManager; 013import jmri.JmriException; 014import jmri.jmrit.display.Editor; 015import jmri.jmrit.display.EditorManager; 016import jmri.jmrit.display.controlPanelEditor.ControlPanelEditor; 017import jmri.jmrit.display.panelEditor.PanelEditor; 018import jmri.server.json.JsonConnection; 019import jmri.server.json.JsonException; 020import jmri.server.json.JsonRequest; 021import jmri.server.json.JsonSocketService; 022 023/** WebSocket notifications for panel display-option changes. */ 024public class JsonPanelSocketService extends JsonSocketService<JsonPanelHttpService> { 025 026 private static final long DISPLAY_UPDATE_DELAY_MS = 200; 027 private static final ScheduledExecutorService UPDATE_SCHEDULER = Executors.newSingleThreadScheduledExecutor( 028 new ThreadFactory() { 029 @Override 030 public Thread newThread(Runnable task) { 031 Thread thread = new Thread(task, "JMRI web panel updates"); 032 thread.setDaemon(true); 033 return thread; 034 } 035 }); 036 037 private Editor editor; 038 private String panelName; 039 private ScheduledFuture<?> pendingUpdate; 040 private boolean closed; 041 private final PropertyChangeListener listener = this::propertyChange; 042 043 public JsonPanelSocketService(JsonConnection connection) { 044 super(connection, new JsonPanelHttpService(connection.getObjectMapper())); 045 } 046 047 @Override 048 public void onMessage(String type, JsonNode data, JsonRequest request) throws IOException, JmriException, JsonException { 049 String name = data.path("name").asText(); 050 Editor requested = InstanceManager.getDefault(EditorManager.class).getAll().stream() 051 .filter(candidate -> candidate instanceof PanelEditor || candidate instanceof ControlPanelEditor) 052 .filter(candidate -> candidate.getTargetFrame() != null 053 && candidate.getTargetFrame().getTitle().equals(name)) 054 .findFirst().orElse(null); 055 if (requested != editor) { 056 removeEditorListeners(); 057 editor = requested; 058 if (editor != null) { 059 editor.addPropertyChangeListener("webPanelDisplayOptions", listener); 060 } 061 } 062 panelName = name; 063 } 064 065 private synchronized void propertyChange(PropertyChangeEvent event) { 066 String property = event.getPropertyName(); 067 if (closed || editor == null || !"webPanelDisplayOptions".equals(property)) { 068 return; 069 } 070 if (pendingUpdate != null) { 071 pendingUpdate.cancel(false); 072 } 073 pendingUpdate = UPDATE_SCHEDULER.schedule(this::sendDisplayOptionsChanged, 074 DISPLAY_UPDATE_DELAY_MS, TimeUnit.MILLISECONDS); 075 } 076 077 private void sendDisplayOptionsChanged() { 078 synchronized (this) { 079 if (closed || editor == null || panelName == null) { 080 return; 081 } 082 pendingUpdate = null; 083 } 084 try { 085 com.fasterxml.jackson.databind.node.ObjectNode message = connection.getObjectMapper().createObjectNode() 086 .put("type", JsonPanelServiceFactory.PANEL_DISPLAY_OPTIONS); 087 message.putObject("data").put("name", panelName).put("value", "displayOptionsChanged"); 088 connection.sendMessage(message, 0); 089 } catch (IOException ignored) { 090 // The connection is closing; no further update can be delivered. 091 } 092 } 093 094 @Override 095 public void onList(String type, JsonNode data, JsonRequest request) { 096 // Panel notifications are subscriptions, not list requests. 097 } 098 099 @Override 100 public synchronized void onClose() { 101 closed = true; 102 if (pendingUpdate != null) { 103 pendingUpdate.cancel(false); 104 pendingUpdate = null; 105 } 106 removeEditorListeners(); 107 panelName = null; 108 } 109 110 private void removeEditorListeners() { 111 if (editor != null) { 112 editor.removePropertyChangeListener("webPanelDisplayOptions", listener); 113 editor = null; 114 } 115 } 116}