From 9c99a835de9a49ebfb280b39e483006a80e6219d Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sun, 3 Nov 2024 16:48:43 +0100 Subject: [PATCH] Implement live map updates with WebSockets --- app/channels/points_channel.rb | 7 ++ app/javascript/channels/index.js | 1 + app/javascript/channels/points_channel.js | 15 ++++ app/javascript/controllers/maps_controller.js | 81 +++++++++++++++++++ app/models/notification.rb | 10 +-- app/models/point.rb | 19 +++++ app/views/map/index.html.erb | 4 +- config/importmap.rb | 1 + spec/channels/points_channel_spec.rb | 5 ++ 9 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 app/channels/points_channel.rb create mode 100644 app/javascript/channels/points_channel.js create mode 100644 spec/channels/points_channel_spec.rb diff --git a/app/channels/points_channel.rb b/app/channels/points_channel.rb new file mode 100644 index 00000000..8ce13209 --- /dev/null +++ b/app/channels/points_channel.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class PointsChannel < ApplicationCable::Channel + def subscribed + stream_for current_user + end +end diff --git a/app/javascript/channels/index.js b/app/javascript/channels/index.js index 79ce4754..ed929ca4 100644 --- a/app/javascript/channels/index.js +++ b/app/javascript/channels/index.js @@ -1,2 +1,3 @@ // Import all the channels to be used by Action Cable import "notifications_channel" +import "points_channel" diff --git a/app/javascript/channels/points_channel.js b/app/javascript/channels/points_channel.js new file mode 100644 index 00000000..17b0b5d1 --- /dev/null +++ b/app/javascript/channels/points_channel.js @@ -0,0 +1,15 @@ +import consumer from "./consumer" + +consumer.subscriptions.create("PointsChannel", { + connected() { + // Called when the subscription is ready for use on the server + }, + + disconnected() { + // Called when the subscription has been terminated by the server + }, + + received(data) { + // Called when there's incoming data on the websocket for this channel + } +}); diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 8a3f31b1..6ca50260 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -1,6 +1,7 @@ import { Controller } from "@hotwired/stimulus"; import L from "leaflet"; import "leaflet.heat"; +import consumer from "../channels/consumer"; // Add this import import { createMarkersArray } from "../maps/markers"; @@ -138,12 +139,92 @@ export default class extends Controller { this.map.removeControl(this.drawControl); } }); + + this.setupSubscription(); // Add this line } disconnect() { this.map.remove(); } + setupSubscription() { + consumer.subscriptions.create("PointsChannel", { + received: (data) => { + this.appendPoint(data); + } + }); + } + + appendPoint(data) { + // Parse the received point data + console.log(data) + const newPoint = data; + + // Add the new point to the markers array + this.markers.push(newPoint); + + // Create a new marker for the point + const markerOptions = { + ...this.userSettings, // Pass any relevant settings + id: newPoint[6], // Assuming index 6 contains the point ID + timestamp: newPoint[4] // Assuming index 2 contains the timestamp + }; + + const newMarker = this.createMarker(newPoint, markerOptions); + this.markersArray.push(newMarker); + + // Update the markers layer + this.markersLayer.clearLayers(); + this.markersLayer.addLayer(L.layerGroup(this.markersArray)); + + // Update heatmap + this.heatmapMarkers.push([newPoint[0], newPoint[1], 0.2]); + this.heatmapLayer.setLatLngs(this.heatmapMarkers); + + // Update polylines + this.polylinesLayer.clearLayers(); + this.polylinesLayer = createPolylinesLayer( + this.markers, + this.map, + this.timezone, + this.routeOpacity, + this.userSettings + ); + + // Pan map to new location + this.map.setView([newPoint[0], newPoint[1]], 14); + + // Update fog of war if enabled + if (this.map.hasLayer(this.fogOverlay)) { + this.updateFog(this.markers, this.clearFogRadius); + } + + // Update the last marker + this.map.eachLayer((layer) => { + if (layer instanceof L.Marker && !layer._popup) { + this.map.removeLayer(layer); + } + }); + this.addLastMarker(this.map, this.markers); + } + + createMarker(point, options) { + const marker = L.marker([point[0], point[1]]); + + // Add popup content based on point data + const popupContent = ` +
+

Time: ${new Date(point[2]).toLocaleString()}

+ ${point[3] ? `

Address: ${point[3]}

` : ''} + ${point[7] ? `

Country: ${point[7]}

` : ''} + Delete +
+ `; + + marker.bindPopup(popupContent); + return marker; + } + async setupScratchLayer(countryCodesMap) { this.scratchLayer = L.geoJSON(null, { style: { diff --git a/app/models/notification.rb b/app/models/notification.rb index c62840d2..8c13db4d 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -21,11 +21,11 @@ class Notification < ApplicationRecord NotificationsChannel.broadcast_to( user, { - id: id, - title: title, - content: content, - kind: kind, - timestamp: Time.current.to_i + # id: id, + # title: title, + # content: content, + # kind: kind, + # timestamp: Time.current.to_i } ) end diff --git a/app/models/point.rb b/app/models/point.rb index bc92783d..c4b00d14 100644 --- a/app/models/point.rb +++ b/app/models/point.rb @@ -23,6 +23,7 @@ class Point < ApplicationRecord scope :not_visited, -> { where(visit_id: nil) } after_create :async_reverse_geocode + after_create_commit :broadcast_coordinates def self.without_raw_data select(column_names - ['raw_data']) @@ -37,4 +38,22 @@ class Point < ApplicationRecord ReverseGeocodingJob.perform_later(self.class.to_s, id) end + + private + + def broadcast_coordinates + PointsChannel.broadcast_to( + user, + [ + latitude.to_f, + longitude.to_f, + battery.to_s, + altitude.to_s, + timestamp.to_s, + velocity.to_s, + id.to_s, + country.to_s + ] + ) + end end diff --git a/app/views/map/index.html.erb b/app/views/map/index.html.erb index 7b3ed8ef..a4d639fc 100644 --- a/app/views/map/index.html.erb +++ b/app/views/map/index.html.erb @@ -41,8 +41,10 @@ <% end %>
diff --git a/config/importmap.rb b/config/importmap.rb index 44a10e2d..4832b78f 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -20,3 +20,4 @@ pin 'leaflet-draw' # @1.0.4 pin '@rails/actioncable', to: 'actioncable.esm.js' pin_all_from 'app/javascript/channels', under: 'channels' pin 'notifications_channel', to: 'channels/notifications_channel.js' +pin 'points_channel', to: 'channels/points_channel.js' diff --git a/spec/channels/points_channel_spec.rb b/spec/channels/points_channel_spec.rb new file mode 100644 index 00000000..5ca00454 --- /dev/null +++ b/spec/channels/points_channel_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe PointsChannel, type: :channel do + pending "add some examples to (or delete) #{__FILE__}" +end