mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 01:31:39 -05:00
Merge branch 'dev' into fix/trip-visited-countries
This commit is contained in:
commit
4918ae94f4
9 changed files with 252 additions and 185 deletions
|
|
@ -4,18 +4,25 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
|
||||||
# [0.30.9] - 2025-08-18
|
# [0.30.9] - 2025-08-18
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
- Countries, visited during a trip, are now being calculated from points to improve performance.
|
- Countries, visited during a trip, are now being calculated from points to improve performance.
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- X-Dawarich-Response and X-Dawarich-Version headers are now returned for all API responses.
|
||||||
|
|
||||||
|
|
||||||
# [0.30.8] - 2025-08-01
|
# [0.30.8] - 2025-08-01
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
- Fog of war is now working correctly on zoom and map movement. #1603
|
- Fog of war is now working correctly on zoom and map movement. #1603
|
||||||
|
- Possibly fixed a bug where visits were no suggested correctly. #984
|
||||||
|
- Scratch map is now working correctly.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Countries::BordersController < ApplicationController
|
class Api::V1::Countries::BordersController < ApiController
|
||||||
def index
|
def index
|
||||||
countries = Rails.cache.fetch('dawarich/countries_codes', expires_in: 1.day) do
|
countries = Rails.cache.fetch('dawarich/countries_codes', expires_in: 1.day) do
|
||||||
Oj.load(File.read(Rails.root.join('lib/assets/countries.geojson')))
|
Oj.load(File.read(Rails.root.join('lib/assets/countries.geojson')))
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,6 @@ class Api::V1::HealthController < ApiController
|
||||||
skip_before_action :authenticate_api_key
|
skip_before_action :authenticate_api_key
|
||||||
|
|
||||||
def index
|
def index
|
||||||
if current_api_user
|
|
||||||
response.set_header('X-Dawarich-Response', 'Hey, I\'m alive and authenticated!')
|
|
||||||
else
|
|
||||||
response.set_header('X-Dawarich-Response', 'Hey, I\'m alive!')
|
|
||||||
end
|
|
||||||
|
|
||||||
response.set_header('X-Dawarich-Version', APP_VERSION)
|
|
||||||
|
|
||||||
render json: { status: 'ok' }
|
render json: { status: 'ok' }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,18 @@
|
||||||
|
|
||||||
class ApiController < ApplicationController
|
class ApiController < ApplicationController
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
|
before_action :set_version_header
|
||||||
before_action :authenticate_api_key
|
before_action :authenticate_api_key
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_version_header
|
||||||
|
message = "Hey, I\'m alive#{current_api_user ? ' and authenticated' : ''}!"
|
||||||
|
|
||||||
|
response.set_header('X-Dawarich-Response', message)
|
||||||
|
response.set_header('X-Dawarich-Version', APP_VERSION)
|
||||||
|
end
|
||||||
|
|
||||||
def authenticate_api_key
|
def authenticate_api_key
|
||||||
return head :unauthorized unless current_api_user
|
return head :unauthorized unless current_api_user
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import { showFlashMessage } from "../maps/helpers";
|
||||||
import { fetchAndDisplayPhotos } from "../maps/photos";
|
import { fetchAndDisplayPhotos } from "../maps/photos";
|
||||||
import { countryCodesMap } from "../maps/country_codes";
|
import { countryCodesMap } from "../maps/country_codes";
|
||||||
import { VisitsManager } from "../maps/visits";
|
import { VisitsManager } from "../maps/visits";
|
||||||
|
import { ScratchLayer } from "../maps/scratch_layer";
|
||||||
|
|
||||||
import "leaflet-draw";
|
import "leaflet-draw";
|
||||||
import { initializeFogCanvas, drawFogCanvas, createFogOverlay } from "../maps/fog_of_war";
|
import { initializeFogCanvas, drawFogCanvas, createFogOverlay } from "../maps/fog_of_war";
|
||||||
|
|
@ -49,7 +50,6 @@ export default class extends BaseController {
|
||||||
layerControl = null;
|
layerControl = null;
|
||||||
visitedCitiesCache = new Map();
|
visitedCitiesCache = new Map();
|
||||||
trackedMonthsCache = null;
|
trackedMonthsCache = null;
|
||||||
currentPopup = null;
|
|
||||||
tracksLayer = null;
|
tracksLayer = null;
|
||||||
tracksVisible = false;
|
tracksVisible = false;
|
||||||
tracksSubscription = null;
|
tracksSubscription = null;
|
||||||
|
|
@ -181,7 +181,7 @@ export default class extends BaseController {
|
||||||
this.areasLayer = new L.FeatureGroup();
|
this.areasLayer = new L.FeatureGroup();
|
||||||
this.photoMarkers = L.layerGroup();
|
this.photoMarkers = L.layerGroup();
|
||||||
|
|
||||||
this.setupScratchLayer(this.countryCodesMap);
|
this.initializeScratchLayer();
|
||||||
|
|
||||||
if (!this.settingsButtonAdded) {
|
if (!this.settingsButtonAdded) {
|
||||||
this.addSettingsButton();
|
this.addSettingsButton();
|
||||||
|
|
@ -197,7 +197,7 @@ export default class extends BaseController {
|
||||||
Tracks: this.tracksLayer,
|
Tracks: this.tracksLayer,
|
||||||
Heatmap: this.heatmapLayer,
|
Heatmap: this.heatmapLayer,
|
||||||
"Fog of War": this.fogOverlay,
|
"Fog of War": this.fogOverlay,
|
||||||
"Scratch map": this.scratchLayer,
|
"Scratch map": this.scratchLayerManager?.getLayer() || L.layerGroup(),
|
||||||
Areas: this.areasLayer,
|
Areas: this.areasLayer,
|
||||||
Photos: this.photoMarkers,
|
Photos: this.photoMarkers,
|
||||||
"Suggested Visits": this.visitsManager.getVisitCirclesLayer(),
|
"Suggested Visits": this.visitsManager.getVisitCirclesLayer(),
|
||||||
|
|
@ -348,127 +348,23 @@ export default class extends BaseController {
|
||||||
appendPoint(data) {
|
appendPoint(data) {
|
||||||
if (this.liveMapHandler && this.liveMapEnabled) {
|
if (this.liveMapHandler && this.liveMapEnabled) {
|
||||||
this.liveMapHandler.appendPoint(data);
|
this.liveMapHandler.appendPoint(data);
|
||||||
|
// Update scratch layer manager with new markers
|
||||||
|
if (this.scratchLayerManager) {
|
||||||
|
this.scratchLayerManager.updateMarkers(this.markers);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('LiveMapHandler not initialized or live mode not enabled');
|
console.warn('LiveMapHandler not initialized or live mode not enabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setupScratchLayer(countryCodesMap) {
|
async initializeScratchLayer() {
|
||||||
this.scratchLayer = L.geoJSON(null, {
|
this.scratchLayerManager = new ScratchLayer(this.map, this.markers, this.countryCodesMap, this.apiKey);
|
||||||
style: {
|
this.scratchLayer = await this.scratchLayerManager.setup();
|
||||||
fillColor: '#FFD700',
|
|
||||||
fillOpacity: 0.3,
|
|
||||||
color: '#FFA500',
|
|
||||||
weight: 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Up-to-date version can be found on Github:
|
|
||||||
// https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson
|
|
||||||
const response = await fetch('/api/v1/countries/borders.json', {
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/geo+json,application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const worldData = await response.json();
|
|
||||||
// Cache the world borders data for future use
|
|
||||||
this.worldBordersData = worldData;
|
|
||||||
|
|
||||||
const visitedCountries = this.getVisitedCountries(countryCodesMap)
|
|
||||||
const filteredFeatures = worldData.features.filter(feature =>
|
|
||||||
visitedCountries.includes(feature.properties["ISO3166-1-Alpha-2"])
|
|
||||||
)
|
|
||||||
|
|
||||||
this.scratchLayer.addData({
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: filteredFeatures
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading GeoJSON:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getVisitedCountries(countryCodesMap) {
|
|
||||||
if (!this.markers) return [];
|
|
||||||
|
|
||||||
return [...new Set(
|
|
||||||
this.markers
|
|
||||||
.filter(marker => marker[7]) // Ensure country exists
|
|
||||||
.map(marker => {
|
|
||||||
// Convert country name to ISO code, or return the original if not found
|
|
||||||
return countryCodesMap[marker[7]] || marker[7];
|
|
||||||
})
|
|
||||||
)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional: Add methods to handle user interactions
|
|
||||||
toggleScratchLayer() {
|
toggleScratchLayer() {
|
||||||
if (this.map.hasLayer(this.scratchLayer)) {
|
if (this.scratchLayerManager) {
|
||||||
this.map.removeLayer(this.scratchLayer)
|
this.scratchLayerManager.toggle();
|
||||||
} else {
|
|
||||||
this.scratchLayer.addTo(this.map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshScratchLayer() {
|
|
||||||
console.log('Refreshing scratch layer with current data');
|
|
||||||
|
|
||||||
if (!this.scratchLayer) {
|
|
||||||
console.log('Scratch layer not initialized, setting up');
|
|
||||||
await this.setupScratchLayer(this.countryCodesMap);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Clear existing data
|
|
||||||
this.scratchLayer.clearLayers();
|
|
||||||
|
|
||||||
// Get current visited countries based on current markers
|
|
||||||
const visitedCountries = this.getVisitedCountries(this.countryCodesMap);
|
|
||||||
console.log('Current visited countries:', visitedCountries);
|
|
||||||
|
|
||||||
if (visitedCountries.length === 0) {
|
|
||||||
console.log('No visited countries found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch country borders data (reuse if already loaded)
|
|
||||||
if (!this.worldBordersData) {
|
|
||||||
console.log('Loading world borders data');
|
|
||||||
const response = await fetch('/api/v1/countries/borders.json', {
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/geo+json,application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.worldBordersData = await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter for visited countries
|
|
||||||
const filteredFeatures = this.worldBordersData.features.filter(feature =>
|
|
||||||
visitedCountries.includes(feature.properties["ISO3166-1-Alpha-2"])
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Filtered features for visited countries:', filteredFeatures.length);
|
|
||||||
|
|
||||||
// Add the filtered country data to the scratch layer
|
|
||||||
this.scratchLayer.addData({
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: filteredFeatures
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error refreshing scratch layer:', error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -591,9 +487,11 @@ export default class extends BaseController {
|
||||||
this.visitsManager.fetchAndDisplayVisits();
|
this.visitsManager.fetchAndDisplayVisits();
|
||||||
}
|
}
|
||||||
} else if (event.name === 'Scratch map') {
|
} else if (event.name === 'Scratch map') {
|
||||||
// Refresh scratch map with current visited countries
|
// Add scratch map layer
|
||||||
console.log('Scratch map layer enabled via layer control');
|
console.log('Scratch map layer enabled via layer control');
|
||||||
this.refreshScratchLayer();
|
if (this.scratchLayerManager) {
|
||||||
|
this.scratchLayerManager.addToMap();
|
||||||
|
}
|
||||||
} else if (event.name === 'Fog of War') {
|
} else if (event.name === 'Fog of War') {
|
||||||
// Enable fog of war when layer is added
|
// Enable fog of war when layer is added
|
||||||
this.fogOverlay = event.layer;
|
this.fogOverlay = event.layer;
|
||||||
|
|
@ -626,6 +524,12 @@ export default class extends BaseController {
|
||||||
// Clear the visit circles when layer is disabled
|
// Clear the visit circles when layer is disabled
|
||||||
this.visitsManager.visitCircles.clearLayers();
|
this.visitsManager.visitCircles.clearLayers();
|
||||||
}
|
}
|
||||||
|
} else if (event.name === 'Scratch map') {
|
||||||
|
// Handle scratch map layer removal
|
||||||
|
console.log('Scratch map layer disabled via layer control');
|
||||||
|
if (this.scratchLayerManager) {
|
||||||
|
this.scratchLayerManager.remove();
|
||||||
|
}
|
||||||
} else if (event.name === 'Fog of War') {
|
} else if (event.name === 'Fog of War') {
|
||||||
// Fog canvas will be automatically removed by the layer's onRemove method
|
// Fog canvas will be automatically removed by the layer's onRemove method
|
||||||
this.fogOverlay = null;
|
this.fogOverlay = null;
|
||||||
|
|
@ -703,7 +607,7 @@ export default class extends BaseController {
|
||||||
Routes: this.polylinesLayer || L.layerGroup(),
|
Routes: this.polylinesLayer || L.layerGroup(),
|
||||||
Heatmap: this.heatmapLayer || L.layerGroup(),
|
Heatmap: this.heatmapLayer || L.layerGroup(),
|
||||||
"Fog of War": this.fogOverlay,
|
"Fog of War": this.fogOverlay,
|
||||||
"Scratch map": this.scratchLayer || L.layerGroup(),
|
"Scratch map": this.scratchLayerManager?.getLayer() || L.layerGroup(),
|
||||||
Areas: this.areasLayer || L.layerGroup(),
|
Areas: this.areasLayer || L.layerGroup(),
|
||||||
Photos: this.photoMarkers || L.layerGroup()
|
Photos: this.photoMarkers || L.layerGroup()
|
||||||
};
|
};
|
||||||
|
|
@ -741,16 +645,12 @@ export default class extends BaseController {
|
||||||
const markerId = parseInt(marker[6]);
|
const markerId = parseInt(marker[6]);
|
||||||
return markerId !== numericId;
|
return markerId !== numericId;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addLastMarker(map, markers) {
|
// Update scratch layer manager with updated markers
|
||||||
if (markers.length > 0) {
|
if (this.scratchLayerManager) {
|
||||||
const lastMarker = markers[markers.length - 1].slice(0, 2);
|
this.scratchLayerManager.updateMarkers(this.markers);
|
||||||
const marker = L.marker(lastMarker).addTo(map);
|
}
|
||||||
return marker; // Return marker reference for tracking
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFog(markers, clearFogRadius, fogLineThreshold) {
|
updateFog(markers, clearFogRadius, fogLineThreshold) {
|
||||||
|
|
@ -1104,7 +1004,7 @@ export default class extends BaseController {
|
||||||
Tracks: this.tracksLayer ? this.map.hasLayer(this.tracksLayer) : false,
|
Tracks: this.tracksLayer ? this.map.hasLayer(this.tracksLayer) : false,
|
||||||
Heatmap: this.map.hasLayer(this.heatmapLayer),
|
Heatmap: this.map.hasLayer(this.heatmapLayer),
|
||||||
"Fog of War": this.map.hasLayer(this.fogOverlay),
|
"Fog of War": this.map.hasLayer(this.fogOverlay),
|
||||||
"Scratch map": this.map.hasLayer(this.scratchLayer),
|
"Scratch map": this.scratchLayerManager?.isVisible() || false,
|
||||||
Areas: this.map.hasLayer(this.areasLayer),
|
Areas: this.map.hasLayer(this.areasLayer),
|
||||||
Photos: this.map.hasLayer(this.photoMarkers)
|
Photos: this.map.hasLayer(this.photoMarkers)
|
||||||
};
|
};
|
||||||
|
|
@ -1646,14 +1546,6 @@ export default class extends BaseController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chunk(array, size) {
|
|
||||||
const chunked = [];
|
|
||||||
for (let i = 0; i < array.length; i += size) {
|
|
||||||
chunked.push(array.slice(i, i + size));
|
|
||||||
}
|
|
||||||
return chunked;
|
|
||||||
}
|
|
||||||
|
|
||||||
getWholeYearLink() {
|
getWholeYearLink() {
|
||||||
// First try to get year from URL parameters
|
// First try to get year from URL parameters
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
@ -1918,30 +1810,6 @@ export default class extends BaseController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLayerControl() {
|
|
||||||
if (!this.layerControl) return;
|
|
||||||
|
|
||||||
// Remove existing layer control
|
|
||||||
this.map.removeControl(this.layerControl);
|
|
||||||
|
|
||||||
// Create new controls layer object
|
|
||||||
const controlsLayer = {
|
|
||||||
Points: this.markersLayer || L.layerGroup(),
|
|
||||||
Routes: this.polylinesLayer || L.layerGroup(),
|
|
||||||
Tracks: this.tracksLayer || L.layerGroup(),
|
|
||||||
Heatmap: this.heatmapLayer || L.heatLayer([]),
|
|
||||||
"Fog of War": this.fogOverlay,
|
|
||||||
"Scratch map": this.scratchLayer || L.layerGroup(),
|
|
||||||
Areas: this.areasLayer || L.layerGroup(),
|
|
||||||
Photos: this.photoMarkers || L.layerGroup(),
|
|
||||||
"Suggested Visits": this.visitsManager?.getVisitCirclesLayer() || L.layerGroup(),
|
|
||||||
"Confirmed Visits": this.visitsManager?.getConfirmedVisitCirclesLayer() || L.layerGroup()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Re-add the layer control
|
|
||||||
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTracksVisibility(event) {
|
toggleTracksVisibility(event) {
|
||||||
this.tracksVisible = event.target.checked;
|
this.tracksVisible = event.target.checked;
|
||||||
|
|
||||||
|
|
@ -1949,8 +1817,4 @@ export default class extends BaseController {
|
||||||
toggleTracksVisibility(this.tracksLayer, this.map, this.tracksVisible);
|
toggleTracksVisibility(this.tracksLayer, this.map, this.tracksVisible);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
171
app/javascript/maps/scratch_layer.js
Normal file
171
app/javascript/maps/scratch_layer.js
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import L from "leaflet";
|
||||||
|
|
||||||
|
export class ScratchLayer {
|
||||||
|
constructor(map, markers, countryCodesMap, apiKey) {
|
||||||
|
this.map = map;
|
||||||
|
this.markers = markers;
|
||||||
|
this.countryCodesMap = countryCodesMap;
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.scratchLayer = null;
|
||||||
|
this.worldBordersData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setup() {
|
||||||
|
this.scratchLayer = L.geoJSON(null, {
|
||||||
|
style: {
|
||||||
|
fillColor: '#FFD700',
|
||||||
|
fillOpacity: 0.3,
|
||||||
|
color: '#FFA500',
|
||||||
|
weight: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Up-to-date version can be found on Github:
|
||||||
|
// https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson
|
||||||
|
const worldData = await this._fetchWorldBordersData();
|
||||||
|
|
||||||
|
const visitedCountries = this.getVisitedCountries();
|
||||||
|
console.log('Current visited countries:', visitedCountries);
|
||||||
|
|
||||||
|
if (visitedCountries.length === 0) {
|
||||||
|
console.log('No visited countries found');
|
||||||
|
return this.scratchLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredFeatures = worldData.features.filter(feature =>
|
||||||
|
visitedCountries.includes(feature.properties["ISO3166-1-Alpha-2"])
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Filtered features for visited countries:', filteredFeatures.length);
|
||||||
|
|
||||||
|
this.scratchLayer.addData({
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: filteredFeatures
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading GeoJSON:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.scratchLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fetchWorldBordersData() {
|
||||||
|
if (this.worldBordersData) {
|
||||||
|
return this.worldBordersData;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Loading world borders data');
|
||||||
|
const response = await fetch('/api/v1/countries/borders.json', {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/geo+json,application/json',
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.worldBordersData = await response.json();
|
||||||
|
return this.worldBordersData;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVisitedCountries() {
|
||||||
|
if (!this.markers) return [];
|
||||||
|
|
||||||
|
return [...new Set(
|
||||||
|
this.markers
|
||||||
|
.filter(marker => marker[7]) // Ensure country exists
|
||||||
|
.map(marker => {
|
||||||
|
// Convert country name to ISO code, or return the original if not found
|
||||||
|
return this.countryCodesMap[marker[7]] || marker[7];
|
||||||
|
})
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
if (!this.scratchLayer) {
|
||||||
|
console.warn('Scratch layer not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.map.hasLayer(this.scratchLayer)) {
|
||||||
|
this.map.removeLayer(this.scratchLayer);
|
||||||
|
} else {
|
||||||
|
this.scratchLayer.addTo(this.map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
console.log('Refreshing scratch layer with current data');
|
||||||
|
|
||||||
|
if (!this.scratchLayer) {
|
||||||
|
console.log('Scratch layer not initialized, setting up');
|
||||||
|
await this.setup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Clear existing data
|
||||||
|
this.scratchLayer.clearLayers();
|
||||||
|
|
||||||
|
// Get current visited countries based on current markers
|
||||||
|
const visitedCountries = this.getVisitedCountries();
|
||||||
|
console.log('Current visited countries:', visitedCountries);
|
||||||
|
|
||||||
|
if (visitedCountries.length === 0) {
|
||||||
|
console.log('No visited countries found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch country borders data (reuse if already loaded)
|
||||||
|
const worldData = await this._fetchWorldBordersData();
|
||||||
|
|
||||||
|
// Filter for visited countries
|
||||||
|
const filteredFeatures = worldData.features.filter(feature =>
|
||||||
|
visitedCountries.includes(feature.properties["ISO3166-1-Alpha-2"])
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Filtered features for visited countries:', filteredFeatures.length);
|
||||||
|
|
||||||
|
// Add the filtered country data to the scratch layer
|
||||||
|
this.scratchLayer.addData({
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: filteredFeatures
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing scratch layer:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update markers reference when they change
|
||||||
|
updateMarkers(markers) {
|
||||||
|
this.markers = markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the Leaflet layer for use in layer controls
|
||||||
|
getLayer() {
|
||||||
|
return this.scratchLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if layer is currently visible on map
|
||||||
|
isVisible() {
|
||||||
|
return this.scratchLayer && this.map.hasLayer(this.scratchLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove layer from map
|
||||||
|
remove() {
|
||||||
|
if (this.scratchLayer && this.map.hasLayer(this.scratchLayer)) {
|
||||||
|
this.map.removeLayer(this.scratchLayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add layer to map
|
||||||
|
addToMap() {
|
||||||
|
if (this.scratchLayer) {
|
||||||
|
this.scratchLayer.addTo(this.map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -114,7 +114,7 @@ module Visits
|
||||||
|
|
||||||
# Look for existing place with this name
|
# Look for existing place with this name
|
||||||
existing = Place.where(name: name)
|
existing = Place.where(name: name)
|
||||||
.near([point.latitude, point.longitude], SIMILARITY_RADIUS, :m)
|
.near([point.lat, point.lon], SIMILARITY_RADIUS, :m)
|
||||||
.first
|
.first
|
||||||
|
|
||||||
return existing if existing
|
return existing if existing
|
||||||
|
|
@ -122,9 +122,9 @@ module Visits
|
||||||
# Create new place
|
# Create new place
|
||||||
place = Place.new(
|
place = Place.new(
|
||||||
name: name,
|
name: name,
|
||||||
lonlat: "POINT(#{point.longitude} #{point.latitude})",
|
lonlat: "POINT(#{point.lon} #{point.lat})",
|
||||||
latitude: point.latitude,
|
latitude: point.lat,
|
||||||
longitude: point.longitude,
|
longitude: point.lon,
|
||||||
city: properties['city'],
|
city: properties['city'],
|
||||||
country: properties['country'],
|
country: properties['country'],
|
||||||
geodata: point.geodata,
|
geodata: point.geodata,
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,38 @@ require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Api::V1::Countries::Borders', type: :request do
|
RSpec.describe 'Api::V1::Countries::Borders', type: :request do
|
||||||
describe 'GET /index' do
|
describe 'GET /index' do
|
||||||
it 'returns a list of countries with borders' do
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
context 'when user is not authenticated' do
|
||||||
|
it 'returns http unauthorized' do
|
||||||
get '/api/v1/countries/borders'
|
get '/api/v1/countries/borders'
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns X-Dawarich-Response header' do
|
||||||
|
get '/api/v1/countries/borders'
|
||||||
|
|
||||||
|
expect(response.headers['X-Dawarich-Response']).to eq('Hey, I\'m alive!')
|
||||||
|
expect(response.headers['X-Dawarich-Version']).to eq(APP_VERSION)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is authenticated' do
|
||||||
|
it 'returns a list of countries with borders' do
|
||||||
|
get '/api/v1/countries/borders', headers: { 'Authorization' => "Bearer #{user.api_key}" }
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(response.body).to include('AF')
|
expect(response.body).to include('AF')
|
||||||
expect(response.body).to include('ZW')
|
expect(response.body).to include('ZW')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns X-Dawarich-Response header' do
|
||||||
|
get '/api/v1/countries/borders', headers: { 'Authorization' => "Bearer #{user.api_key}" }
|
||||||
|
|
||||||
|
expect(response.headers['X-Dawarich-Response']).to eq('Hey, I\'m alive and authenticated!')
|
||||||
|
expect(response.headers['X-Dawarich-Version']).to eq(APP_VERSION)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,7 @@ RSpec.describe Visits::PlaceFinder do
|
||||||
context 'with places from points data' do
|
context 'with places from points data' do
|
||||||
let(:point_with_geodata) do
|
let(:point_with_geodata) do
|
||||||
build_stubbed(:point,
|
build_stubbed(:point,
|
||||||
latitude: latitude,
|
lonlat: "POINT(#{longitude} #{latitude})",
|
||||||
longitude: longitude,
|
|
||||||
geodata: {
|
geodata: {
|
||||||
'properties' => {
|
'properties' => {
|
||||||
'name' => 'POI from Point',
|
'name' => 'POI from Point',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue