Compare commits

..

7 Commits

Author SHA1 Message Date
cb39695e6c Search adjustments + building techs 2026-05-24 16:21:33 +02:00
487995366e Add markers action to search results 2026-05-24 12:29:57 +02:00
f9daaefbdd Search results handling (WIP) 2026-05-23 23:43:48 +02:00
743bbc2f3b First search request attempt 2026-05-23 22:55:04 +02:00
0de6158652 Add publication in home page + minor changes 2026-05-23 17:41:37 +02:00
692ef13574 Search sidebar (WIP) 2026-05-22 21:57:23 +02:00
f83f95a51a Stub form controller 2026-05-22 18:39:40 +02:00
12 changed files with 315 additions and 87 deletions

View File

@@ -59,4 +59,4 @@ Photo Sphere Viewer, Three.js (da cui dipende Photo Sphere Viewer) e Stimulus.
## TODO
- [ ] Auto-discovery per Stimulus?
- [ ] Refactor con app state per evitare oggetti globali
- [x] Refactor con app state per evitare oggetti globali

View File

@@ -187,3 +187,8 @@ a:visited {
.marker-cluster-small span {
color: #fff;
}
/* Apply Bulma's is-danger color based on state */
input:invalid {
border-color: #ff6685;
}

BIN
img/pub/AC_36-1.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -498,6 +498,22 @@
</div>
</div>
</article>
<article class="media mt-4 pb-6">
<figure class="media-left">
<p class="image is-128x128">
<img src="img/pub/AC_36-1.jpg" />
</p>
</figure>
<div class="media-content">
<div class="content">
<p>
Caratelli G., Giorgi C., Paraciani N. 2025,
<em>From archaeological survey to data accessibility: a WebGIS for the island of Capri</em>,
«Archeologia e Calcolatori», 36.1, 65-86 (<a href="https://doi.org/10.19282/ac.36.1.2025.04">https://doi.org/10.19282/ac.36.1.2025.04</a>)
</p>
</div>
</div>
</article>
</section>
<footer class="modal-card-foot">
</footer>

View File

@@ -65,9 +65,9 @@
</button>
<button class="button is-white mr-2 mt-1" title="Cerca"
data-id="search"
data-action="modal#open">
<span class="icon">
<i class="fa fa-search"></i>
data-action="menu#toggleMenu">
<span class="icon" data-id="search" data-action="menu#toggleMenu">
<i data-id="search" class="fa fa-search"></i>
</span>
</button>
</div>
@@ -79,7 +79,74 @@
<div class="main columns">
<div class="column mb-0 pb-0 is-full is-relative">
<div class="pb-0 is-relative" id="map" aria-describedby="map-progress" aria-busy="true">
<progress id="map-progress" class="p-2 progress is-medium is-link" aria-label="Map loading..." />
<progress id="map-progress" class="p-2 progress is-medium is-link" aria-label="Caricamento mappa..." />
</div>
</div>
<div class="menu-overlay column is-hidden is-4 is-4-desktop is-5-mobile is-pulled-right is-overlay has-background-white-ter"
data-menu-target="search" data-controller="layer search">
<button title="Chiudi ricerca" class="delete is-pulled-right" data-action="menu#closeSearch"></button>
<h1 class="is-size-5">Ricerca</h1>
<form id="search-form" method="POST" data-search-target="search" data-action="submit->search#submitSearch">
<div class="field">
<label class="label">Testo libero</label>
<div class="control is-full-width">
<input class="input" type="text" minlength="3" name="text" placeholder="Inserire parole chiave" />
</div>
</div>
<div class="field">
<label class="label">Categoria sito</label>
<div class="control">
<div class="select">
<select name="category">
<option default value="">-- Scegliere la categoria del sito --</option>
<option value="site">Sito conservato</option>
<!--<option value="not_conserved">Sito non conservato</option>-->
</select>
</div>
</div>
</div>
<div class="field">
<label class="label">Tecnica muraria</label>
<div class="control">
<div class="select">
<select name="technique">
<option default value="">-- Scegliere tecnica --</option>
<option>Opera poligonale</option>
<option>Opera incerta</option>
<option>Opera reticolata</option>
<option>Opera laterizia</option>
<option>Opera mista</option>
<option>Opera cementizia</option>
</select>
</div>
</div>
</div>
<div class="field is-grouped mt-5 mb-0 pb-0">
<div class="control">
<button class="button is-link" type="submit">
<span>Cerca</span>
<span class="icon is-small">
<i class="fa fa-search"></i>
</span>
</button>
</div>
<div class="control">
<button class="button is-link is-light" type="reset" data-action="search#clearSearch">
<span>Cancella filtri</span>
<span class="icon is-small">
<i class="fa fa-times"></i>
</span>
</button>
</div>
</div>
</form>
<div class="content pt-2 mt-3 is-hidden" data-search-target="container">
<table class="table is-fullwidth is-striped">
<thead>
<tr><td class="has-text-centered has-text-weight-bold is-size-5" colspan="2">Risultati</td></tr>
</thead>
<tbody data-search-target="results"></tbody>
</table>
</div>
</div>
<div class="menu-overlay column is-hidden is-4 is-4-desktop is-5-mobile is-pulled-right is-overlay has-background-white-ter"
@@ -312,85 +379,6 @@
<button title="Chiudi menu" class="delete is-pulled-right" data-action="menu#closeCartography"></button>
</aside>
</div>
<!-- Search modal -->
<div class="modal" id="search" data-modal-target="modal">
<div class="modal-background" data-action="click->modal#close click->tabs#reset"></div>
<div class="modal-content box has-background-white pt-4 mr-4 ml-4 pl-4 pr-4" style="min-height: 400px;">
<h1 class="is-size-4 has-text-centered">Ricerca</h1>
<div class="field">
<label class="label">Testo libero</label>
<div class="control is-full-width">
<input class="input" type="text" placeholder="Inserire parole chiave">
</div>
</div>
<div class="columns mt-5 pt-3">
<div class="field column">
<label class="label">Categoria sito</label>
<div class="control">
<div class="select">
<select>
<option default>-- Scegliere la categoria del sito --</option>
<option>Sito conservato</option>
<option>Sito non conservato</option>
</select>
</div>
</div>
</div>
<!--
<div class="field column">
<label class="label">Categoria reperto</label>
<div class="control">
<div class="select">
<select>
<option default>-- Scegliere la categoria del reperto --</option>
<option>Scultura</option>
<option>Epigrafe</option>
<option>Elemento architettonico</option>
<option>Decorazione parietale</option>
<option>Pavimentazione</option>
<option>Arredo</option>
<option>Abbigliamento e ornamenti personali</option>
</select>
</div>
</div>
</div>
-->
<div class="field column">
<label class="label">Tecnica muraria</label>
<div class="control">
<div class="select">
<select>
<option default>-- Scegliere tecnica --</option>
<option>Opera poligonale</option>
<option>Opera incerta</option>
<option>Opera reticolata</option>
<option>Opera laterizia</option>
<option>Opera mista</option>
<option>Opera cementizia</option>
</select>
</div>
</div>
</div>
</div>
<div class="field is-grouped mt-5 mb-0 pb-0 has-text-right">
<div class="control">
<button class="button is-link">
<span>Cerca</span>
<span class="icon is-small">
<i class="fa fa-search"></i>
</span>
</button>
</div>
<div class="control">
<button class="button is-link is-light">
<span>Cancella filtri</span>
<span class="icon is-small">
<i class="fa fa-times"></i>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Bibliography citations template -->

View File

@@ -6,6 +6,7 @@ export default class extends Controller {
'list',
'menu',
'cartography',
'search',
'icon'
];
@@ -86,12 +87,17 @@ export default class extends Controller {
toggleMenu(event) {
const menuId = event.target.dataset.id;
console.debug(menuId, event.target);
// Stupid...
if (menuId === 'main') {
this.menuTarget.classList.toggle('is-hidden');
if (!this.cartographyTarget.classList.contains('is-hidden')) {
this.cartographyTarget.classList.add('is-hidden');
}
if (!this.searchTarget.classList.contains('is-hidden')) {
this.searchTarget.classList.add('is-hidden');
}
}
if (menuId === 'cartography') {
@@ -99,6 +105,18 @@ export default class extends Controller {
if (!this.menuTarget.classList.contains('is-hidden')) {
this.menuTarget.classList.add('is-hidden');
}
if (!this.searchTarget.classList.contains('is-hidden')) {
this.searchTarget.classList.add('is-hidden');
}
}
if (menuId === 'search') {
this.searchTarget.classList.toggle('is-hidden');
if (!this.menuTarget.classList.contains('is-hidden')) {
this.menuTarget.classList.add('is-hidden');
}
if (!this.cartographyTarget.classList.contains('is-hidden')) {
this.cartographyTarget.classList.add('is-hidden');
}
}
}
@@ -110,6 +128,10 @@ export default class extends Controller {
this.cartographyTarget.classList.add('is-hidden');
}
closeSearch() {
this.searchTarget.classList.add('is-hidden');
}
toggleList(id) {
document.querySelector(`#${id}`).classList.toggle('is-hidden');
}

View File

@@ -0,0 +1,139 @@
import { Controller } from "@hotwired/stimulus";
import { GisState } from "../state.js";
import UI from "../ui.js";
const html = String.raw;
export default class extends Controller {
static targets = [
'search',
'results',
'clear',
'container',
];
/**
*
* @param {Event} event
*/
async submitSearch(event) {
event.preventDefault();
const data = new FormData(event.target);
const body = {};
const map = GisState.map;
const techs = GisState.layers.buildingTechs;
const techsMarkers = GisState.markers.buildingTechs;
// Reset search for building techs...
for (const key of Object.keys(techsMarkers)) {
map.removeLayer(techsMarkers[key]);
}
for (const entry of data.entries()) {
body[entry[0]] = entry[1];
}
const response = await fetch(`${GisState.apiUrl}/search?` + new URLSearchParams(body));
const results = await response.json();
console.warn(body);
this.containerTarget.classList.remove('is-hidden');
this.#injectResults(results);
if (results.length) {
this.#filterMap(results);
// Should technique always be shown after a search?
for (const key of Object.keys(techsMarkers)) {
if (techsMarkers[key].options.label === body.technique)
map.addLayer(techsMarkers[key]);
}
}
}
clearSearch() {
const map = GisState.map;
// Restore layer groups in map
for (const key of Object.keys(GisState.layers)) {
map.addLayer(GisState.layers[key]);
}
// Empty result set
this.resultsTarget.innerHTML = '';
this.containerTarget.classList.add('is-hidden');
}
#injectResults(results) {
/**
* @type {HTMLOutputElement} output
*/
const output = this.resultsTarget;
output.innerHTML = '';
if (results.length === 0) {
output.innerHTML = html`
<p class="has-background-white-bis p-4 mt-0 has-text-centered">
Nessun risultato trovato per i parametri di ricerca
</p>
`;
}
const sites = GisState.markers.sites;
for (const result of results) {
let coordinates = ''
for (let key of Object.keys(sites)) {
if (sites[key].options.data.label === result.label) {
coordinates = key;
}
}
// TODO The group value should be dynamic!!
const item = html`
<tr>
<td class="pt-4">${result.label}</td>
<td>
<button class="button is-link"
data-controller="marker"
data-action="marker#go"
data-marker-coords-value="${coordinates}"
data-marker-group-value="sites">
Vai al sito
<span class="ml-1 icon">
<i class="fa fa-chevron-right"></i>
</span>
</button>
</td>
</tr>
`;
output.innerHTML += item;
}
}
/**
*
* @param {Array<Object>} results
*/
#filterMap(results) {
const map = GisState.map;
const labels = [];
results.forEach(r => labels.push(r.label));
// Remove all layer groups first
for (const key of Object.keys(GisState.layers)) {
map.removeLayer(GisState.layers[key]);
}
const sites = GisState.markers.sites;
for (let key of Object.keys(sites)) {
// If map has layers from previous search results...
map.removeLayer(sites[key]);
for (const label of labels) {
if (sites[key].options.data.label === label) {
map.addLayer(sites[key]);
}
}
}
}
}

View File

@@ -74,6 +74,7 @@ GIS.initMap = async function (mapId, zoomLevel = this.INIT_ZOOM) {
await this.addLayerGroups(map);
await this.fetchCartographyLayers();
const buildingTechs = await this.buildingTechs();
const reprojectedWMSLayer = GIS.reprojectWMS();
const wmsLayer = new reprojectedWMSLayer(
@@ -94,6 +95,7 @@ GIS.initMap = async function (mapId, zoomLevel = this.INIT_ZOOM) {
'Fabbricati' : buildings,
'Vincoli archeologici' : layerVincoli,
'Vincoli archeologici indiretti' : layerPaesistici,
'Tecniche murarie' : buildingTechs,
};
L.control.layers(baseMap, cartography).addTo(map);
@@ -114,6 +116,35 @@ GIS.fetchCartographyLayers = async function () {
GisState.cartography.historic.push({id, label});
}
}
/**
* Create building techs layer
* @returns {L.Layer}
*/
GIS.buildingTechs = async function () {
let techsData = await fetch(`${API_URL}/building_techs`)
.then(data => data.json());
let techs = new L.LayerGroup();
for (let record of techsData) {
const marker = L.marker(
record.coordinates,
{icon: Icons.techs, label: record.technique}
)
.bindTooltip(record.technique)
.bindPopup(UI.createBuildingTechTable(record));
techs.addLayer(marker);
const markerLabel = `${record.coordinates[0]} ${record.coordinates[1]}`;
marker.options.data = record;
marker.options.site = record.site.label;
GisState.markers.buildingTechs[markerLabel] = marker;
}
GisState.layers.buildingTechs = techs;
return techs;
}
/**
* Load georeferenced image overlays layer group
* @param {Number} imageId - The API id of the georeferenced image

View File

@@ -59,4 +59,6 @@ Icons.reuse = L.icon(
Icons.camera = L.divIcon({className: 'fa fa-camera'});
Icons.techs = L.divIcon({className: 'fa fa-circle has-text-primary-25'});
export default Icons;

View File

@@ -1,5 +1,6 @@
import GIS from './gis.js';
import UI from './ui.js';
import { GisState } from './state.js';
import { Application } from '@hotwired/stimulus';
import MenuController from './controllers/menu_controller.js';
import ModalController from './controllers/modal_controller.js';
@@ -7,6 +8,7 @@ import MarkerController from './controllers/marker_controller.js';
import BiblioController from './controllers/biblio_controller.js';
import TabsController from './controllers/tabs_controller.js';
import LayerController from './controllers/layer_controller.js';
import SearchController from './controllers/search_controller.js';
document.addEventListener('DOMContentLoaded', async () => {
// Register Stimulus controllers
@@ -14,14 +16,14 @@ document.addEventListener('DOMContentLoaded', async () => {
let progress = document.querySelector('progress');
const map = await GIS.initMap('map');
progress.classList.add('is-hidden');
map._container.setAttribute('aria-busy', false);
// Trigger Stimulus buildMenu method...
const menuEvent = new Event('menu-ready');
document.dispatchEvent(menuEvent);
progress.classList.add('is-hidden');
map._container.setAttribute('aria-busy', false);
GIS.toggleSpherical(map);
UI.addCenterMapControl(map, GIS.CENTER_COORDS, GIS.INIT_ZOOM);
@@ -36,4 +38,5 @@ function initStimulus() {
Stimulus.register("biblio", BiblioController);
Stimulus.register("tabs", TabsController);
Stimulus.register("layer", LayerController);
Stimulus.register("search", SearchController);
}

View File

@@ -28,6 +28,7 @@ export const GisState = {
prehistoric: {},
underwater: {},
reuse: {},
buildingTechs: {},
},
layers: {
sites: {},
@@ -36,6 +37,7 @@ export const GisState = {
prehistoric: {},
underwater: {},
reuse: {},
buildingTechs: {},
},
bibliography: null,
apiUrl : null,

View File

@@ -12,6 +12,8 @@ import { Underwater } from './components/Underwater.js';
import { GisState } from "./state.js";
import { Reuse } from './components/Reuse.js';
const html = String.raw;
/**
* @namespace UI
*/
@@ -270,5 +272,23 @@ UI.imageGallery = function (galleryId, items, video = false) {
});
}
}
/**
* @param {Object} record
* @returns {string}
*/
UI.createBuildingTechTable = function(record) {
return html`
<table class="table is-striped is-size-6 m-2">
<tbody>
<tr><th>Tecnica</th><td>${record.technique}</td></tr>
<tr><th>Descrizione</th><td>${record.description}</td></tr>
<tr><th>Funzione</th><td>${record.function}</td></tr>
<tr><th>Materiale</th><td>${record.material}</td></tr>
<tr><th>Sito</th><td>${record.site.label}</td></tr>
<tr><th>Comune</th><td>${record.municipality}</td></tr>
</tbody>
</table>
;`
}
export default UI;