diff --git a/data/ddl/07_transport_types.sql b/data/ddl/07_transport_types.sql new file mode 100644 index 0000000..7974d5d --- /dev/null +++ b/data/ddl/07_transport_types.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS `transport_types` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL UNIQUE, + `descriptor` VARCHAR(100) NOT NULL UNIQUE, + `glyphUrl` VARCHAR(2048), + `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (`id`) +); + +INSERT IGNORE INTO `transport_types` (name, descriptor, glyphUrl) VALUES + ('Bus', 'bus', "https://upload.wikimedia.org/wikipedia/commons/4/46/Paris_Bus_icon.svg"), + ('Métro', 'metro', "https://upload.wikimedia.org/wikipedia/commons/5/5e/Metro-M.svg"), + ('RER', 'rer', "https://upload.wikimedia.org/wikipedia/commons/1/13/RER.svg"), + ('Transilien', 'transilien', "https://upload.wikimedia.org/wikipedia/fr/f/f3/Logo_Transilien_%28RATP%29.svg"), + ('Trains TER', 'tertrain', "https://upload.wikimedia.org/wikipedia/commons/9/98/Logo_TER.svg"); \ No newline at end of file diff --git a/data/ddl/08_lines.sql b/data/ddl/08_lines.sql new file mode 100644 index 0000000..dca6388 --- /dev/null +++ b/data/ddl/08_lines.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS `lines` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `code` VARCHAR(50) NOT NULL UNIQUE, + `name` VARCHAR(255), + `textColorHex` VARCHAR(7), + `backColorHex` VARCHAR(7), + `transportTypeId` INT UNSIGNED NOT NULL, + `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (`id`), + + CONSTRAINT `fk_lines_transport_type` + FOREIGN KEY (`transportTypeId`) + REFERENCES `transport_types` (`id`), + + INDEX `idx_transport_type` (`transportTypeId`) +); \ No newline at end of file diff --git a/data/ddl/09_stations.sql b/data/ddl/09_stations.sql new file mode 100644 index 0000000..e19ce15 --- /dev/null +++ b/data/ddl/09_stations.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS `stations` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `description` TEXT NULL, + `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (id), + INDEX idx_stations_name (name) +); \ No newline at end of file diff --git a/data/ddl/10_platforms.sql b/data/ddl/10_platforms.sql new file mode 100644 index 0000000..5a263fd --- /dev/null +++ b/data/ddl/10_platforms.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS `platforms` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `stationId` INT UNSIGNED NOT NULL, + `name` VARCHAR(255) NULL, + `direction` VARCHAR(255) NULL, + `latitude` DECIMAL(10, 8) NOT NULL, + `longitude` DECIMAL(11, 8) NOT NULL, + `location` POINT NOT NULL, + `isPRM` BOOLEAN NOT NULL DEFAULT FALSE, + `hasElevator` BOOLEAN NOT NULL DEFAULT FALSE, + `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (id), + SPATIAL INDEX idx_platforms_location (location), + INDEX idx_platforms_station (stationId), + + CONSTRAINT fk_platforms_station FOREIGN KEY (stationId) + REFERENCES stations(id) ON DELETE CASCADE, + + CONSTRAINT chk_platforms_latitude CHECK (latitude BETWEEN -90 AND 90), + CONSTRAINT chk_platforms_longitude CHECK (longitude BETWEEN -180 AND 180) +); \ No newline at end of file diff --git a/data/ddl/11_platform_lines.sql b/data/ddl/11_platform_lines.sql new file mode 100644 index 0000000..df01be3 --- /dev/null +++ b/data/ddl/11_platform_lines.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS `platform_lines` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `platformId` INT UNSIGNED NOT NULL, + `lineId` INT UNSIGNED NOT NULL, + `directionId` TINYINT UNSIGNED NOT NULL DEFAULT 0, + `orderInLine` SMALLINT UNSIGNED NOT NULL, + + PRIMARY KEY (id), + UNIQUE KEY uq_platform_line_dir (platformId, lineId, directionId), + UNIQUE KEY uq_line_dir_order (lineId, directionId, orderInLine), + INDEX idx_pl_line (lineId), + + CONSTRAINT fk_pl_platform FOREIGN KEY (platformId) + REFERENCES platforms(id) ON DELETE CASCADE, + CONSTRAINT fk_pl_line FOREIGN KEY (lineId) + REFERENCES `lines`(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/data/ddl/12_platforms_trigger.sql b/data/ddl/12_platforms_trigger.sql new file mode 100644 index 0000000..8bded70 --- /dev/null +++ b/data/ddl/12_platforms_trigger.sql @@ -0,0 +1,13 @@ +CREATE TRIGGER IF NOT EXISTS trg_platforms_before_insert + BEFORE INSERT ON `platforms` + FOR EACH ROW + BEGIN + SET NEW.location = POINT(NEW.longitude, NEW.latitude); + END; + +CREATE TRIGGER IF NOT EXISTS trg_platforms_before_update + BEFORE UPDATE ON `platforms` + FOR EACH ROW + BEGIN + SET NEW.location = POINT(NEW.longitude, NEW.latitude); + END; \ No newline at end of file diff --git a/repositories/lineRepo.js b/repositories/lineRepo.js new file mode 100644 index 0000000..06a5e08 --- /dev/null +++ b/repositories/lineRepo.js @@ -0,0 +1,74 @@ +const { pool } = require("../modules/database") +const { DefaultError } = require("../errors/errors") + +async function findAllLines() { + try { + const sql = "SELECT * FROM \`lines\`" + const rows = await pool.query(sql) + return rows + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function findLineById(lineId) { + try { + const sql = "SELECT * FROM \`lines\` WHERE id = ?" + const rows = await pool.query(sql, [lineId]) + return rows[0] || null + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function findLinesByTransportType(transportTypeId) { + try { + const sql = "SELECT * FROM \`lines\` WHERE transportTypeId = ?" + const rows = await pool.query(sql, [transportTypeId]) + return rows + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function createLine(code, name, textColorHex, backColorHex, transportTypeId) { + try { + const sql = "INSERT INTO \`lines\` (code, name, textColorHex, backColorHex, transportTypeId) VALUES (?, ?, ?, ?, ?)" + const rows = await pool.query(sql, [code, name, textColorHex, backColorHex, transportTypeId]) + return rows.insertId + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function updateLine(lineId, updateData) { + try { + const fields = Object.keys(updateData) + const values = Object.values(updateData) + const setClause = fields.map(f => `${f} = ?`).join(", ") + const sql = `UPDATE \`lines\` SET ${setClause} WHERE id = ?` + const rows = await pool.query(sql, [...values, lineId]) + return rows.affectedRows > 0 + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function deleteLine(lineId) { + try { + const sql = "DELETE FROM \`lines\` WHERE id = ?" + const rows = await pool.query(sql, [lineId]) + return rows.affectedRows > 0 + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +module.exports = { + findAllLines, + findLineById, + findLinesByTransportType, + createLine, + updateLine, + deleteLine +} \ No newline at end of file diff --git a/repositories/platformLineRepo.js b/repositories/platformLineRepo.js new file mode 100644 index 0000000..76b34f4 --- /dev/null +++ b/repositories/platformLineRepo.js @@ -0,0 +1,88 @@ +const { pool } = require("../modules/database") +const { DefaultError } = require("../errors/errors") + +// lignes d'une station (dédupliquées, tous quais confondus) +async function findLinesByStation(stationId) { + try { + const sql = ` + SELECT DISTINCT l.* + FROM \`lines\` l + JOIN platform_lines pl ON pl.lineId = l.id + JOIN platforms p ON p.id = pl.platformId + WHERE p.stationId = ? + ` + return await pool.query(sql, [stationId]) + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function findLinesByPlatform(platformId) { + try { + const sql = ` + SELECT l.*, pl.directionId, pl.orderInLine + FROM \`lines\` l + JOIN platform_lines pl ON pl.lineId = l.id + WHERE pl.platformId = ? + ` + return await pool.query(sql, [platformId]) + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +// parcours ordonné d'une ligne dans un sens donné +async function findStationsByLine(lineId, directionId = 0) { + try { + const sql = ` + SELECT s.*, p.id AS platformId, p.name AS platformName, pl.orderInLine + FROM stations s + JOIN platforms p ON p.stationId = s.id + JOIN platform_lines pl ON pl.platformId = p.id + WHERE pl.lineId = ? AND pl.directionId = ? + ORDER BY pl.orderInLine + ` + return await pool.query(sql, [lineId, directionId]) + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function addLineToPlatform(platformId, lineId, directionId, orderInLine) { + try { + const sql = "INSERT INTO platform_lines (platformId, lineId, directionId, orderInLine) VALUES (?, ?, ?, ?)" + const rows = await pool.query(sql, [platformId, lineId, directionId, orderInLine]) + return rows.insertId + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function removeLineFromPlatform(platformId, lineId, directionId) { + try { + const sql = "DELETE FROM platform_lines WHERE platformId = ? AND lineId = ? AND directionId = ?" + const rows = await pool.query(sql, [platformId, lineId, directionId]) + return rows.affectedRows > 0 + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function updateOrder(platformLineId, orderInLine) { + try { + const sql = "UPDATE platform_lines SET orderInLine = ? WHERE id = ?" + const rows = await pool.query(sql, [orderInLine, platformLineId]) + return rows.affectedRows > 0 + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +module.exports = { + findLinesByStation, + findLinesByPlatform, + findStationsByLine, + addLineToPlatform, + removeLineFromPlatform, + updateOrder +} \ No newline at end of file diff --git a/repositories/platformRepo.js b/repositories/platformRepo.js new file mode 100644 index 0000000..920cb21 --- /dev/null +++ b/repositories/platformRepo.js @@ -0,0 +1,85 @@ +const { pool } = require("../modules/database") +const { DefaultError } = require("../errors/errors") + +async function findPlatformsByStation(stationId) { + try { + const sql = "SELECT * FROM platforms WHERE stationId = ?" + return await pool.query(sql, [stationId]) + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function findPlatformById(platformId) { + try { + const sql = "SELECT * FROM platforms WHERE id = ?" + const rows = await pool.query(sql, [platformId]) + return rows[0] || null + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +// cœur de la validation : quai le plus proche desservant CETTE ligne +async function findNearestPlatformForLine(lineId, latitude, longitude, radiusM) { + try { + const sql = ` + SELECT p.*, ST_Distance_Sphere(p.location, ST_SRID(POINT(?, ?), 4326)) AS distance + FROM platforms p + JOIN platform_lines pl ON pl.platformId = p.id + WHERE pl.lineId = ? + HAVING distance <= ? + ORDER BY distance + LIMIT 1 + ` + const rows = await pool.query(sql, [longitude, latitude, lineId, radiusM]) + return rows[0] || null + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function createPlatform(stationId, name, direction, latitude, longitude, isPRM, hasElevator) { + try { + const sql = ` + INSERT INTO platforms (stationId, name, direction, latitude, longitude, isPRM, hasElevator) + VALUES (?, ?, ?, ?, ?, ?, ?) + ` + const rows = await pool.query(sql, [ + stationId, name, direction, latitude, longitude, + longitude, latitude, isPRM, hasElevator + ]) + return rows.insertId + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function updatePlatformAccessibility(platformId, isPRM, hasElevator) { + try { + const sql = "UPDATE platforms SET isPRM = ?, hasElevator = ? WHERE id = ?" + const rows = await pool.query(sql, [isPRM, hasElevator, platformId]) + return rows.affectedRows > 0 + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function deletePlatform(platformId) { + try { + const sql = "DELETE FROM platforms WHERE id = ?" + const rows = await pool.query(sql, [platformId]) + return rows.affectedRows > 0 + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +module.exports = { + findPlatformsByStation, + findPlatformById, + findNearestPlatformForLine, + createPlatform, + updatePlatformAccessibility, + deletePlatform +} \ No newline at end of file diff --git a/repositories/stationRepo.js b/repositories/stationRepo.js new file mode 100644 index 0000000..0406d58 --- /dev/null +++ b/repositories/stationRepo.js @@ -0,0 +1,99 @@ +const { pool } = require("../modules/database") +const { DefaultError } = require("../errors/errors") + +async function findAllStations() { + try { + const sql = "SELECT * FROM stations" + return await pool.query(sql) + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function findStationById(stationId) { + try { + const sql = "SELECT * FROM stations WHERE id = ?" + const rows = await pool.query(sql, [stationId]) + return rows[0] || null + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +// distance = celle du quai le plus proche +async function findStationsByDistance(latitude, longitude, radiusKm) { + try { + const sql = ` + SELECT s.*, MIN(ST_Distance_Sphere(p.location, ST_SRID(POINT(?, ?), 4326))) / 1000 AS distance + FROM stations s + JOIN platforms p ON p.stationId = s.id + GROUP BY s.id + HAVING distance <= ? + ORDER BY distance + ` + return await pool.query(sql, [longitude, latitude, radiusKm]) + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +// une station est "accessible" si AU MOINS un quai l'est +async function findAccessibleStations(latitude, longitude, radiusKm) { + try { + const sql = ` + SELECT s.*, MIN(ST_Distance_Sphere(p.location, ST_SRID(POINT(?, ?), 4326))) / 1000 AS distance + FROM stations s + JOIN platforms p ON p.stationId = s.id + WHERE p.isPRM = TRUE AND p.hasElevator = TRUE + GROUP BY s.id + HAVING distance <= ? + ORDER BY distance + ` + return await pool.query(sql, [longitude, latitude, radiusKm]) + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function createStation(name, description) { + try { + const sql = "INSERT INTO stations (name, description) VALUES (?, ?)" + const rows = await pool.query(sql, [name, description]) + return rows.insertId + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function updateStation(stationId, updateData) { + try { + const fields = Object.keys(updateData) + const values = Object.values(updateData) + const setClause = fields.map(f => `${f} = ?`).join(", ") + const sql = `UPDATE stations SET ${setClause} WHERE id = ?` + const rows = await pool.query(sql, [...values, stationId]) + return rows.affectedRows > 0 + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +async function deleteStation(stationId) { + try { + const sql = "DELETE FROM stations WHERE id = ?" + const rows = await pool.query(sql, [stationId]) + return rows.affectedRows > 0 + } catch (error) { + throw new DefaultError(500, "Internal Server Error", error) + } +} + +module.exports = { + findAllStations, + findStationById, + findStationsByDistance, + findAccessibleStations, + createStation, + updateStation, + deleteStation +} \ No newline at end of file diff --git a/services/transportService.js b/services/transportService.js new file mode 100644 index 0000000..69ae972 --- /dev/null +++ b/services/transportService.js @@ -0,0 +1,220 @@ +const lineRepo = require("../repositories/lineRepo") +const stationRepo = require("../repositories/stationRepo") +const platformRepo = require("../repositories/platformRepo") +const platformLineRepo = require("../repositories/platformLineRepo") + +/* ---------- LINES ---------- */ + +async function getAllLines() { + return await lineRepo.findAllLines() +} + +async function getLineById({ lineId }) { + return await lineRepo.findLineById(lineId) +} + +async function getLinesByTransportType({ transportTypeId }) { + return await lineRepo.findLinesByTransportType(transportTypeId) +} + +async function createLine({ + code, + name, + textColorHex, + backColorHex, + transportTypeId +}) { + return await lineRepo.createLine( + code, + name, + textColorHex, + backColorHex, + transportTypeId + ) +} + +async function updateLine({ lineId, updateData }) { + return await lineRepo.updateLine(lineId, updateData) +} + +async function deleteLine({ lineId }) { + return await lineRepo.deleteLine(lineId) +} + +/* ---------- STATIONS ---------- */ + +async function getAllStations() { + return await stationRepo.findAllStations() +} + +async function getStationById({ stationId }) { + return await stationRepo.findStationById(stationId) +} + +async function getStationsByDistance({ latitude, longitude, radiusKm }) { + return await stationRepo.findStationsByDistance( + latitude, + longitude, + radiusKm + ) +} + +async function getAccessibleStations({ latitude, longitude, radiusKm }) { + return await stationRepo.findAccessibleStations( + latitude, + longitude, + radiusKm + ) +} + +async function createStation({ name, description }) { + return await stationRepo.createStation(name, description) +} + +async function updateStation({ stationId, updateData }) { + return await stationRepo.updateStation(stationId, updateData) +} + +async function deleteStation({ stationId }) { + return await stationRepo.deleteStation(stationId) +} + +/* ---------- PLATFORMS ---------- */ + +async function getPlatformsByStation({ stationId }) { + return await platformRepo.findPlatformsByStation(stationId) +} + +async function getPlatformById({ platformId }) { + return await platformRepo.findPlatformById(platformId) +} + +async function createPlatform({ + stationId, + name, + direction, + latitude, + longitude, + isPRM = false, + hasElevator = false +}) { + return await platformRepo.createPlatform( + stationId, + name, + direction, + latitude, + longitude, + isPRM, + hasElevator + ) +} + +async function updatePlatformAccessibility({ + platformId, + isPRM, + hasElevator +}) { + return await platformRepo.updatePlatformAccessibility( + platformId, + isPRM, + hasElevator + ) +} + +async function deletePlatform({ platformId }) { + return await platformRepo.deletePlatform(platformId) +} + +/* ---------- PLATFORM <-> LINES ---------- */ + +async function getStationLines({ stationId }) { + return await platformLineRepo.findLinesByStation(stationId) +} + +async function getPlatformLines({ platformId }) { + return await platformLineRepo.findLinesByPlatform(platformId) +} + +async function getLineStations({ lineId, directionId = 0 }) { + return await platformLineRepo.findStationsByLine(lineId, directionId) +} + +async function addLineToPlatform({ + platformId, + lineId, + directionId = 0, + orderInLine +}) { + return await platformLineRepo.addLineToPlatform( + platformId, + lineId, + directionId, + orderInLine + ) +} + +async function removeLineFromPlatform({ + platformId, + lineId, + directionId = 0 +}) { + return await platformLineRepo.removeLineFromPlatform( + platformId, + lineId, + directionId + ) +} + +async function updatePlatformLineOrder({ platformLineId, orderInLine }) { + return await platformLineRepo.updateOrder(platformLineId, orderInLine) +} + +/* ---------- VALIDATION ---------- */ + +const DEFAULT_RADIUS_M = 100 + +async function findNearestPlatformForLine({ + lineId, + latitude, + longitude, + radiusM = DEFAULT_RADIUS_M +}) { + return await platformRepo.findNearestPlatformForLine( + lineId, + latitude, + longitude, + radiusM + ) +} + +module.exports = { + getAllLines, + getLineById, + getLinesByTransportType, + createLine, + updateLine, + deleteLine, + + getAllStations, + getStationById, + getStationsByDistance, + getAccessibleStations, + createStation, + updateStation, + deleteStation, + + getPlatformsByStation, + getPlatformById, + createPlatform, + updatePlatformAccessibility, + deletePlatform, + + getStationLines, + getPlatformLines, + getLineStations, + addLineToPlatform, + removeLineFromPlatform, + updatePlatformLineOrder, + + findNearestPlatformForLine +} \ No newline at end of file