diff --git a/README.md b/README.md index 3da9f495..590ad2a0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Tabula +![Demo image](docs/demo.png) + This repository contains two subprojects, one is the backend of the app which is a Java-SpringBoot-PostgreSQL-Gradle application, while the other is the frontend of the app which is a TypeScript-Angular-NPM app. diff --git a/database/schema.sql b/database/schema.sql index 416f8157..a96f3d04 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -35,7 +35,8 @@ CREATE TABLE data_type INSERT INTO data_type (name) VALUES ('Textual'), ('Numeric'), - ('Monetary'); + ('Monetary'), + ('Maps'); CREATE TABLE tbl_table ( diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..3e4a931a --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,10 @@ +*.aux +*.fdb_latexmk +*.fls +*.log +*.nav +*.out +*.snm +*.toc +*.dvi +*.pdf diff --git a/docs/.latexmkrc b/docs/.latexmkrc new file mode 100644 index 00000000..6eec4fe7 --- /dev/null +++ b/docs/.latexmkrc @@ -0,0 +1 @@ +$pdf_previewer = 'start papers'; diff --git a/docs/classes.drawio b/docs/classes.drawio new file mode 100644 index 00000000..b8226b98 --- /dev/null +++ b/docs/classes.drawio @@ -0,0 +1,468 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/classes.png b/docs/classes.png new file mode 100644 index 00000000..dd88b7ef Binary files /dev/null and b/docs/classes.png differ diff --git a/docs/demo.png b/docs/demo.png new file mode 100644 index 00000000..e11212f2 Binary files /dev/null and b/docs/demo.png differ diff --git a/docs/entity-relation.drawio b/docs/entity-relation.drawio new file mode 100644 index 00000000..94065389 --- /dev/null +++ b/docs/entity-relation.drawio @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/entity_relation.png b/docs/entity_relation.png new file mode 100644 index 00000000..51039933 Binary files /dev/null and b/docs/entity_relation.png differ diff --git a/doc/entity_relation_diagram.drawio.svg b/docs/entity_relation_diagram.drawio.svg similarity index 100% rename from doc/entity_relation_diagram.drawio.svg rename to docs/entity_relation_diagram.drawio.svg diff --git a/docs/gerarchia_visiva.png b/docs/gerarchia_visiva.png new file mode 100644 index 00000000..954eb8be Binary files /dev/null and b/docs/gerarchia_visiva.png differ diff --git a/docs/presentation.pdf b/docs/presentation.pdf new file mode 100644 index 00000000..79320824 Binary files /dev/null and b/docs/presentation.pdf differ diff --git a/docs/presentation.tex b/docs/presentation.tex new file mode 100644 index 00000000..1e80807d --- /dev/null +++ b/docs/presentation.tex @@ -0,0 +1,143 @@ +\documentclass{beamer} + +\usepackage{inter} +\usepackage{url} +\usepackage[italian]{babel} +\usepackage{graphicx} +\usepackage{hyperref} +\usepackage{tikz} +\usepackage{fontawesome} + +\hypersetup{ + pdfpagemode=FullScreen, + colorlinks=true +} + +\setbeamertemplate{navigation symbols}{} % remove navigation symbols + +\title{Tabula} +\author{ + Alfredo Carlino \texttt{246025} \newline + \and Giorgio Carlino \texttt{246038} \newline + \and Mario G. D'Andrea \texttt{245940}} +\institute{Corso di Web Applications \\ C.d.S. Informatica $\cdot$ DeMaCs \\ Università della Calabria} +\titlegraphic{\includegraphics[width=2cm]{unical}} +\date{Giugno 2025} + +\begin{document} +\maketitle + +\begin{frame} + \includegraphics[width=\textwidth]{demo} + \centering \href{https://github.com/bytestrick/tabula}{\faGithub\ github.com/bytestrick/tabula} +\end{frame} + +\begin{frame} + \frametitle{Descrizione generale} + + Tabula consente di organizzare dati in formato tabellare facilitando l'accesso e la modifica. Piuttosto che un foglio di calcolo, si può considerare l'app come una base di dati con cui si può interagire direttamente. + + \vspace{10pt} + + Ogni tabella in Tabula è composta da diverse colonne, ognuna con un \textbf{tipo} assegnato: numero, testo, data, posizione geografica, somma monetaria, ecc. + + \vspace{10pt} + + La facilità di utilizzo delle tabelle le rende uno strumento perfetto per tenere traccia delle letture, sia concluse che pianificate; oppure raggruppare in un unico punto i task pianificati (to-do) e aggregarli in classi usando il tipo \textit{tag}, corredandoli di una data e una posizione geografica opzionalmente; o ancora tracciare la performance di una squadra di calcio, derivando informazioni statistiche dagli esiti delle singole partite. + +\end{frame} + +\begin{frame} + \frametitle{Layout e schermate} + + L’applicazione si compone di tre schermate principali: + + \begin{itemize} + \item[\faUser] \textbf{\textit{Sign Up} \& \textit{Sign In}} \hspace{3pt} Viene mostrata esclusivamente agli utenti non ancora registrati, permette la creazione di un account, necessario per accedere alla piattaforma. + +Una volta completata la registrazione, l’utente viene automaticamente reindirizzato alla schermata di \textit{sign-in}, dove può autenticarsi utilizzando le credenziali appena fornite. + + \item[\faHome] \label{ui_home} \textbf{Home} \hspace{3pt} È punto di accesso principale dell’utente dopo l'accesso. Qui è possibile visualizzare un riepilogo di tutte le tabelle create, con la possibilità di aggiungerne di nuove. Ogni tabella elencata consente operazioni di modifica o eliminazione. + + \item[\faTable] \label{ui_table} \textbf{Table} \hspace{3pt} Dedicata alla modifica e gestione dei contenuti all'interno di una singola tabella. + + \end{itemize} +\end{frame} + +\begin{frame} + \frametitle{Navigazione tra schermate} + + La navigazione segue un \textbf{modello ad hub}, con il seguente flusso: + + \begin{enumerate} + \item L’utente accede alla schermata principale (\hyperref[ui_home]{Home}) + \item Da qui può essere reindirizzato a pagine secondarie (es. \hyperref[ui_table]{Table}) per svolgere azioni +specifiche + \item Una volta completata l’attività, viene riportato alla schermata principale + \end{enumerate} + +\end{frame} + +\begin{frame} + \frametitle{Account e Navbar} + + \begin{columns} + \begin{column}{0.53\textwidth} + Per rendere disponibili in qualsiasi momento le informazioni dell’account e delle impostazioni (come il tema dell’interfaccia), è presente una sidebar integrata nella navbar, accessibile da qualsiasi schermata. + \end{column} + \begin{column}{0.48\textwidth} + \includegraphics[width=5cm]{sidebar.png} + \end{column} + \end{columns} +\end{frame} + +\begin{frame} + \frametitle{Gerarchia visiva} + + L’interfaccia dell’applicazione è organizzata secondo una struttura a griglia. + + \begin{figure} + \includegraphics[width=0.9\textwidth]{gerarchia_visiva.png} + \end{figure} +\end{frame} + +\begin{frame} + \frametitle{Tecnologie usate} + + \begin{itemize} + \item[\faLeaf] Spring Boot + \item[\faEnvelope] Spring Email + \item[\faDatabase] PostgreSQL + \end{itemize} + + \vspace{15pt} + + \begin{itemize} + \item[\faHtml5] Angular + \item[\faCss3] Bootstrap + \end{itemize} + + \vspace{18pt} + + API esterne: + \begin{itemize} + \item[\faMapMarker] \href{https://www.openstreetmap.org/}{OpenStreetMap} + \item[\faMapSigns] \href{https://nominatim.openstreetmap.org/}{Nominatim} + \end{itemize} +\end{frame} + +\begin{frame} + \frametitle{Modello entità-relazione} + \begin{figure} + \includegraphics[width=\textwidth]{entity_relation} + \end{figure} +\end{frame} + +\begin{frame} + \frametitle{Diagramma di classi (back-end)} + \begin{figure} + \includegraphics[height=8cm]{classes} + \end{figure} +\end{frame} + +\end{document} diff --git a/docs/sidebar.png b/docs/sidebar.png new file mode 100644 index 00000000..2e9b3cff Binary files /dev/null and b/docs/sidebar.png differ diff --git a/docs/unical.jpg b/docs/unical.jpg new file mode 100644 index 00000000..237d6d85 Binary files /dev/null and b/docs/unical.jpg differ diff --git a/frontend/angular.json b/frontend/angular.json index 5cb8dc60..230ea72c 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -31,15 +31,22 @@ { "glob": "**/*", "input": "public" + }, + { + "glob": "*.*", + "input": "node_modules/leaflet/dist/images", + "output": "media" } ], "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "node_modules/bootstrap-icons/font/bootstrap-icons.min.css", - "src/styles.css" + "src/styles.css", + "node_modules/leaflet/dist/leaflet.css" ], "scripts": [ - "node_modules/bootstrap/dist/js/bootstrap.min.js" + "node_modules/bootstrap/dist/js/bootstrap.min.js", + "node_modules/leaflet/dist/leaflet.js" ] }, "configurations": { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index afa1bb85..8284be59 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,6 +20,7 @@ "@angular/router": "^19.2.7", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", + "leaflet": "^1.9.4", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -30,6 +31,7 @@ "@angular/compiler-cli": "^19.2.7", "@types/bootstrap": "^5.2.10", "@types/jasmine": "~5.1.0", + "@types/leaflet": "^1.9.17", "jasmine-core": "~5.2.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", @@ -5532,6 +5534,13 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -5563,6 +5572,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/leaflet": { + "version": "1.9.17", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.17.tgz", + "integrity": "sha512-IJ4K6t7I3Fh5qXbQ1uwL3CFVbCi6haW9+53oLWgdKlLP7EaS21byWFJxxqOx9y8I0AP0actXSJLVMbyvxhkUTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -9832,6 +9851,12 @@ "shell-quote": "^1.8.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/less": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/less/-/less-4.2.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index db518228..17bcfb75 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "@angular/router": "^19.2.7", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", + "leaflet": "^1.9.4", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -32,6 +33,7 @@ "@angular/compiler-cli": "^19.2.7", "@types/bootstrap": "^5.2.10", "@types/jasmine": "~5.1.0", + "@types/leaflet": "^1.9.17", "jasmine-core": "~5.2.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index b896f611..7afcb444 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,4 +1,3 @@ - diff --git a/frontend/src/app/model/data-types/concrete-data-type/maps-data-type.ts b/frontend/src/app/model/data-types/concrete-data-type/maps-data-type.ts new file mode 100644 index 00000000..f9bb3e97 --- /dev/null +++ b/frontend/src/app/model/data-types/concrete-data-type/maps-data-type.ts @@ -0,0 +1,39 @@ +import { Type } from '@angular/core'; +import { BaseInputComponent } from '../../../table-components/input-components/base-input-component'; +import { BaseCellComponent } from '../../../table-components/table/cells/base-cell-component'; +import {IDataType} from '../i-data-type'; +import {TextualCellComponent} from '../../../table-components/table/cells/textual-cell/textual-cell.component'; +import {DataTypeRegistryService} from '../../../services/data-type-registry.service'; +import {MapsInputComponent} from '../../../table-components/input-components/maps-input/maps-input.component'; + + +export class MapsDataType implements IDataType { + + getDataTypeId(): number { + return DataTypeRegistryService.MAPS_ID; + } + + getInputComponent(): Type { + return MapsInputComponent; + } + + + getNewDataType(): IDataType { + return new MapsDataType(); + } + + + getCellComponent(): Type { + return TextualCellComponent; + } + + + getIconName(): string { + return 'bi-geo-alt'; + } + + + getDataTypeName(): string { + return 'Maps'; + } +} diff --git a/frontend/src/app/services/data-type-registry.service.ts b/frontend/src/app/services/data-type-registry.service.ts index 19b1384a..52db3768 100644 --- a/frontend/src/app/services/data-type-registry.service.ts +++ b/frontend/src/app/services/data-type-registry.service.ts @@ -6,6 +6,7 @@ import {MonetaryDataType} from '../model/data-types/concrete-data-type/monetary- import {map, Observable} from 'rxjs'; import {HttpClient, HttpParams} from '@angular/common/http'; import {DataTypeDTO} from '../model/dto/table/data-type-dto'; +import {MapsDataType} from '../model/data-types/concrete-data-type/maps-data-type'; @Injectable({ providedIn: 'root' @@ -17,6 +18,7 @@ export class DataTypeRegistryService { public static readonly TEXTUAL_ID: number = 1; public static readonly NUMERIC_ID: number = 2; public static readonly MONETARY_ID: number = 3; + public static readonly MAPS_ID: number = 4; getDataType(term: string = ''): Observable { @@ -35,6 +37,7 @@ export class DataTypeRegistryService { case DataTypeRegistryService.TEXTUAL_ID: return new TextualDataType(); case DataTypeRegistryService.NUMERIC_ID: return new NumericDataType(); case DataTypeRegistryService.MONETARY_ID: return new MonetaryDataType(); + case DataTypeRegistryService.MAPS_ID: return new MapsDataType(); default: throw new Error(`${dataTypeId} does not match any data type`); } } diff --git a/frontend/src/app/table-components/input-components/base-input-component.ts b/frontend/src/app/table-components/input-components/base-input-component.ts index 2b4b060d..155fe61c 100644 --- a/frontend/src/app/table-components/input-components/base-input-component.ts +++ b/frontend/src/app/table-components/input-components/base-input-component.ts @@ -68,7 +68,7 @@ export abstract class BaseInputComponent implements PopUpContent { * @param value - The string value entered by the user * @param dataTypeId - Numeric identifier representing the chosen data type */ - protected confirmInputDataType(value: any, dataTypeId: number): void { + protected confirmInputDataType(value: string, dataTypeId: number): void { if (this.doAfterInputDataTypeConfirmation) this.doAfterInputDataTypeConfirmation(value, dataTypeId); diff --git a/frontend/src/app/table-components/input-components/maps-input/maps-input.component.html b/frontend/src/app/table-components/input-components/maps-input/maps-input.component.html new file mode 100644 index 00000000..81ebc726 --- /dev/null +++ b/frontend/src/app/table-components/input-components/maps-input/maps-input.component.html @@ -0,0 +1,25 @@ +
+
+ + {{ longitudeLatitudeSeparator }} + + +
+
+
diff --git a/frontend/src/app/table-components/input-components/maps-input/maps-input.component.ts b/frontend/src/app/table-components/input-components/maps-input/maps-input.component.ts new file mode 100644 index 00000000..c2970beb --- /dev/null +++ b/frontend/src/app/table-components/input-components/maps-input/maps-input.component.ts @@ -0,0 +1,137 @@ +import {AfterViewInit, Component, ElementRef, viewChild} from '@angular/core'; +import {BaseInputComponent} from '../base-input-component'; +import * as L from 'leaflet'; +import {DataTypeRegistryService} from '../../../services/data-type-registry.service'; + +@Component({ + selector: 'tbl-maps-input', + imports: [], + templateUrl: './maps-input.component.html', +}) +export class MapsInputComponent extends BaseInputComponent implements AfterViewInit { + private map!: L.Map; + private currentMarker?: L.Marker; + private latitudeInput = viewChild.required>('latitudeInput'); + private longitudeInput = viewChild.required>('longitudeInput'); + + protected longitudeLatitudeSeparator = ':'; + + + ngAfterViewInit(): void { + this.initMap(); + } + + private initMap(): void { + this.map = L.map('map'); + this.setMapView(); + + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: '© OpenStreetMap' + }).addTo(this.map); + + this.map.on('click', this.onMapClick.bind(this)); + } + + private setLongitudeLatitudeOnInput(lat: number, lng: number): void + { + this.latitudeInput().nativeElement.value = lat.toString(); + this.longitudeInput().nativeElement.value = lng.toString(); + } + + private getCurrentLongitudeLatitude(): string { + return `${this.latitudeInput().nativeElement.value}${this.longitudeLatitudeSeparator}${this.longitudeInput().nativeElement.value}`; + } + + private setMapView(lat: number = 51.505, lng: number = -0.09): void { + this.setLongitudeLatitudeOnInput(lat, lng); + this.map.setView([lat, lng], 13); + } + + private locateUser(): void { + if (!navigator.geolocation) { + console.warn('Geolocation is not supported by the browser'); + this.setMapView(); + return; + } + + navigator.geolocation.getCurrentPosition( + pos => { + const lat = pos.coords.latitude; + const lng = pos.coords.longitude; + + this.setMapView(lat, lng); + + this.currentMarker = L.marker([lat, lng]) + .addTo(this.map) + .bindPopup('You\'re here!') + .openPopup(); + }, + err => { + console.error('Error in position detection:', err.message); + }, + { + enableHighAccuracy: true, + timeout: 5000, + maximumAge: 0 + } + ); + } + + private setMarker(lat: number, lng: number): void { + if (this.currentMarker) + this.map.removeLayer(this.currentMarker); + + this.currentMarker = L.marker([lat, lng]) + .addTo(this.map); + this.setLongitudeLatitudeOnInput(lat, lng); + } + + private onMapClick(e: L.LeafletMouseEvent): void { + this.setMarker(e.latlng.lat, e.latlng.lng); + } + + protected override onPopUpShowUp(): void { + if (!this.startingValue) + this.locateUser(); + else { + const latLng: string[] = (this.startingValue as string).split(this.longitudeLatitudeSeparator); + const lat = Number(latLng[0]) || 0; + const lng = Number(latLng[1]) || 0; + this.setMapView(lat, lng); + this.setMarker(lat, lng); + } + } + + private applyInput(): void { + const coordinateRegex: RegExp = /^(-?\d+(\.\d+)?):(-?\d+(\.\d+)?)$/; + const cord = this.getCurrentLongitudeLatitude(); + + if (cord == this.longitudeLatitudeSeparator) + this.confirmInputDataType('', DataTypeRegistryService.MAPS_ID); + else if (cord && coordinateRegex.test(cord)) + this.confirmInputDataType(cord, DataTypeRegistryService.MAPS_ID); + else + this.abortInput(); + } + + protected override onPopUpHiddenWithLeftClick(): void { + this.applyInput(); + } + + protected override onPopUpHiddenWithRightClick(): void { + this.abortInput(); + } + + onPositionConfirmed(): void { + this.applyInput(); + } + + onLatitudeLongitudeChange(lat: string, lng: string): void { + const _lat = Number(lat); + const _lng = Number(lng); + + if (_lat && _lng) + this.setMapView(_lat, _lng); + } +}