Commit 90dc27bf authored by CLOUARD Regis's avatar CLOUARD Regis
Browse files

Add legend for track names

parent cf9eae4d
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Fixed
- Fix some bugs
### Changed
- Look and Feel page of list of courses
## [2.0.0] 2021-06-06
### Fixed
- Fix a lot of bugs
### Added
- Events
### Changed
- result statistics
\ No newline at end of file
......@@ -30,7 +30,11 @@ sa localisation et un mode promenade où l’orienteur est positionné sur la ca
Le présent projet correspond au site web de l'application [Vikazimut](https://vikazimut.vikazim.fr).
## L'équipe Vikazimut ([Université de Caen Normandie](https://www.unicaen.fr/) - ([spécialité informatique](https://uniform.unicaen.fr/catalogue/formation/licences/5744-licence-informatique))
## Projet lié
- Application mobile développée en Flutter ([projet gitlab](https://gitlab.ecole.ensicaen.fr/rclouard/vikazimut-app)).
## L'équipe Vikazimut ([Université de Caen Normandie](https://www.unicaen.fr/) - [spécialité informatique](https://uniform.unicaen.fr/catalogue/formation/licences/5744-licence-informatique))
### 2019/2020
- Martin FÉAUX DE LA CROIX
......
......@@ -37,8 +37,8 @@ function convertTimeToString(time) {
}
function deltaDistanceCalculator(idealDistance, realDistance) {
let distanceString = idealDistance.toString();
(idealDistance > +realDistance) ? distanceString += " (+" : distanceString += " (";
let distanceString = idealDistance.toLocaleString();
(idealDistance > realDistance) ? distanceString += " (+" : distanceString += " (";
distanceString += Math.round(((idealDistance / realDistance) - 1) * 100) + "%)";
return distanceString;
}
......@@ -86,10 +86,10 @@ function renderTimeSheet(distances) {
// Body
let body = document.createElement("tbody");
let rows = [];
let cps = timeSheetData[0][1];
let checkpoints = timeSheetData[0][1];
let totalTheoreticalDistance = 0;
for (let i = 0; i < cps.length; i++) {
const index = cps[i]["controlPoint"];
for (let i = 0; i < checkpoints.length; i++) {
const index = checkpoints[i]["controlPoint"];
let controlCell = document.createElement("th");
let lengthCell = document.createElement("td");
controlCell.setAttribute("scope", "col");
......@@ -97,11 +97,11 @@ function renderTimeSheet(distances) {
controlCell.innerHTML = translations.start;
} else if (index === timeSheetData[0][1].length - 1) {
controlCell.innerHTML = translations.finish;
lengthCell.innerHTML = distances[i];
lengthCell.innerHTML = distances[i].toLocaleString();
totalTheoreticalDistance += distances[i];
} else {
controlCell.innerHTML = index;
lengthCell.innerHTML = distances[i];
lengthCell.innerHTML = distances[i].toLocaleString();
totalTheoreticalDistance += distances[i];
}
let row = document.createElement("tr");
......@@ -113,11 +113,12 @@ function renderTimeSheet(distances) {
let totalTitleCell = document.createElement("th");
totalTitleCell.innerHTML = translations.total;
let totalLengthCell = document.createElement("th");
totalLengthCell.innerHTML = totalTheoreticalDistance.toString();
totalLengthCell.innerHTML = totalTheoreticalDistance.toLocaleString();
let totalsRow = document.createElement("tr");
totalsRow.style.background = "#FAF5F0";
body.appendChild(totalsRow);
totalsRow.appendChild(totalTitleCell);
totalsRow.appendChild(totalLengthCell);
body.appendChild(totalsRow);
// Body tracks
timeSheetData.forEach(function (orienteerData) {
......
function addTrack(event, map, track) {
function addTrack(event, map, gpxTrack) {
let color = colorPicker.removeColor();
let gpx = new L.GPX(track, {
let gpx = new L.GPX(gpxTrack, {
async: true,
marker_options: {
startIconUrl: '/public/leaflet/images/pin-icon-start.png',
......
......@@ -8,12 +8,10 @@ class CustomeSvg {
constructor(type) {
this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
this.svg.setAttribute("viewBox", "0 0 12 16");
this.svg.setAttribute("width", "12");
this.svg.setAttribute("height", "16");
this.svg.setAttribute("style", "fill: currentColor;");
this.changeSvg(type);
}
......
window.addEventListener("load", function () {
msTimes = document.getElementsByClassName("msTime");
let msTimes = document.getElementsByClassName("msTime");
for (let i = 0; i < msTimes.length; i++) {
convertTime(msTimes[i]);
}
......@@ -10,28 +10,28 @@ function convertTime(event) {
let ms = event.innerHTML;
let sec = ms / 1000;
let min = 0;
let hou = 0;
let str = "";
let hrs = 0;
if (sec >= 3600) {
hou = Math.floor(sec / 3600);
hrs = Math.floor(sec / 3600);
sec = sec % 3600;
}
if (sec >= 60) {
min = Math.floor(sec / 60);
sec = sec % 60;
}
if (hou.toString().length == 1) {
str = "0" + hou.toString() + "h";
let str;
if (hrs.toString().length === 1) {
str = "0" + hrs.toString() + "h";
} else {
str = hou.toString() + "h";
str = hrs.toString() + "h";
}
if (min.toString().length == 1) {
if (min.toString().length === 1) {
str += "0" + min.toString() + "m";
} else {
str += min.toString() + "m";
}
sec = Math.floor(sec);
if (sec.toString().length == 1) {
if (sec.toString().length === 1) {
str += "0" + sec.toString() + "s";
} else {
str += sec.toString() + "s";
......
......@@ -107,7 +107,15 @@ function previewXml(data, divXml) {
}
function previewMap(data, divMap, divLeaflet, map, overlay, img) {
fetch(window.location.origin + "/" + data.image).then(function (response) {
let url = window.location.origin;
if (url.endsWith("/")) {
url = url.substr(0, url.length - 2);
}
if (data.image.startsWith("/")) {
data.image = data.image.substr(1);
}
let imageUrl = url + "/" + data.image;
fetch(imageUrl).then(function (response) {
if (response.status === 200) {
overlay.setUrl(response.url);
img.onload = function () {
......@@ -194,9 +202,17 @@ function showPreview(data, divXml, divMap, divLeaflet, map, overlay, img) {
addControlPointOnMap(data, map);
}
function downloadMap(imageURL) { //see library printJS for further print options.
let printableMap = window.open(window.location.origin + "/" + imageURL);
printableMap.download();
async function downloadMap(imageSrc) {
const image = await fetch(window.location.origin + "/" + imageSrc);
const imageBlog = await image.blob();
const imageURL = URL.createObjectURL(imageBlog);
const link = document.createElement('a');
link.setAttribute('download', 'image');
link.style.display = 'none';
link.href = imageURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function switchMapDisplay(overlay, overlayOpacity) {
......
function openTab(tabTitle, tabId, tabContentClass) {
let i, tabContent, tabLinks;
let activatedTab = document.getElementById(tabTitle);
tabContent = document.getElementsByClassName(tabContentClass);
for (i = 0; i < tabContent.length; i++) {
let tabContent = document.getElementsByClassName(tabContentClass);
for (let i = 0; i < tabContent.length; i++) {
tabContent[i].style.display = "none";
}
tabLinks = document.getElementsByClassName("tab-links");
for (i = 0; i < tabLinks.length; i++) {
if (activatedTab.parentNode == tabLinks[i].parentNode){
let tabLinks = document.getElementsByClassName("tab-links");
for (let i = 0; i < tabLinks.length; i++) {
if (activatedTab.parentNode === tabLinks[i].parentNode){
tabLinks[i].className = tabLinks[i].className.replace(" active", "");
}
}
document.getElementById(tabId).style.display = "block";
activatedTab.className += " active";
}
}
\ No newline at end of file
......@@ -19,7 +19,7 @@ use function simplexml_load_string;
class CourseController extends AbstractController
{
const MAX_DISTANCE_IN_METERS = 100_000;
const MAX_DISTANCE_IN_METERS = 30_000;
/**
* @Route("/routes/{lat}-{lng}", name="course", requirements={"lat"="{\-?\d+(\.\d+)?}", "lng"="{\-?\d+(\.\d+)?}"}, defaults={"lat": 10000, "lng": 1000})
......@@ -55,7 +55,7 @@ class CourseController extends AbstractController
$courses = $allCourses;
}
foreach ($courses as $course) {
$course->setImage("data/" . $course->getId() . "/image");
$course->setImage("data/".$course->getId()."/image");
$courseDate = $course->getStartDate();
if ($courseDate !== null) {
$dateDiff = $courseDate->diff(new DateTime('NOW'));
......@@ -96,7 +96,6 @@ class CourseController extends AbstractController
);
}
}
$openImportTab = 0;
$success = "";
$xml = $course->getXml();
$simplexml = simplexml_load_string($xml);
......@@ -108,7 +107,6 @@ class CourseController extends AbstractController
$gpxForm = $this->createForm(GpxImportType::class, $gpxImport);
$gpxForm->handleRequest($request);
if ($gpxForm->isSubmitted() && $gpxForm->isValid()) {
$openImportTab = 1;
if ($gpxImport->checkGpx()) {
$courseRepository = $this->getDoctrine()->getRepository(Course::class);
if ($gpxImport->loadGpx()) {
......@@ -134,7 +132,6 @@ class CourseController extends AbstractController
'route_track' => $this->generateUrl('course_track', ['id' => $id]),
'form' => $gpxForm->createView(),
'error' => $gpxImport->getError(),
'openImportTab' => $openImportTab,
'success' => $success,
]
);
......@@ -201,8 +198,7 @@ class CourseController extends AbstractController
return new JsonResponse(
[
'kml' => $course->getKml(),
'image' => substr($this->generateUrl('image', ['id' => $id]),1),
'image' => substr($this->generateUrl('image', ['id' => $id]), 1),
]
);
}
......
......@@ -8,6 +8,7 @@ use App\Entity\Track;
use DateTime;
use PHPUnit\Util\Exception;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\Response;
......@@ -93,14 +94,25 @@ class DataController extends AbstractController
return $this->json(["error" => "invalid id"]);
}
$filename = $course->getImage();
if (!file_exists($filename)) {
$filename = "../".$filename;
}
$response = new Response();
$ext = pathinfo($filename, PATHINFO_EXTENSION);
$acceptedExtensions = array("png", "jpeg", "jpg");
if (!in_array("BB", $acceptedExtensions)) {
$ext = "jpeg";
$contentType = "image/";
if ($ext == "png") {
$contentType .= "png";
} else {
$contentType .= "jpeg";
}
$response->headers->set('Content-Type', $ext);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_INLINE, $id.".".$ext);
$response->headers->set('Content-Disposition', $disposition);
$response->headers->set('Content-Type', $contentType);
$response->headers->set('Content-Length', filesize($filename));
$response->headers->set('Cache-Control', 'private, max-age=10800, pre-check=10800');
$response->headers->set('Pragma', ' private');
$response->headers->set('Expires', date(DATE_RFC822, strtotime(" 2 day")));
$response->setContent(file_get_contents($filename));
return $response;
......
......@@ -220,7 +220,7 @@ class PlannerController extends AbstractController
'xml1' => $data1,
'xml2' => $data2,
'kml' => $course->getKml(),
'image' => $course->getImage(),
'image' => $this->generateUrl('image', ['id' => $id]),
]
);
}
......@@ -231,17 +231,17 @@ class PlannerController extends AbstractController
public function upload_file(): JsonResponse
{
$request = Request::createFromGlobals();
$success = "";
$error = "";
$response = new JsonResponse();
foreach ($request->files->all() as $file) {
$user = $this->security->getUser();
$success = FileUploader::uploadFile($file, $user->getId());
if (!$success) {
return $response->fromJsonString($success);
$error = FileUploader::uploadFile($file, $user->getId());
if (!$error) {
return $response->fromJsonString($error);
}
}
return $response->fromJsonString($success);
return $response->fromJsonString($error);
}
/**
......
......@@ -59,7 +59,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
* @Encrypted()
* @var int
*/
private ?string $phone;
private ?string $phone = null;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Course", mappedBy="creator")
......
......@@ -147,11 +147,11 @@ class CreateCourse
}
}
private function generatePreviewImage(string $image): string
private function generatePreviewImage(int $imageId, string $image): string
{
$error = CourseValidator::checkImage($image);
if ($error == CourseValidator::$NO_ERROR) {
return $image;
return $image; // TODO envoyer les données de /upload/...??????
} else {
return $this->translateError($error);
}
......@@ -185,10 +185,10 @@ class CreateCourse
}
if ($post->get("image") == "true") {
$image = FileUploader::getFile($this->user->getId(), "image");
$data["image"] = $this->generatePreviewImage($image);
$image = FileUploader::getFile($this->user->getId(), "image"); // TODO ne pas envoyer image mais /data/id/image (ou /upload/image.jpg....)
$data["image"] = $this->generatePreviewImage($this->user->getId(), $image);
} elseif ($this->course->getImage() != null) {
$data["image"] = $this->generatePreviewImage($this->course->getImage());
$data["image"] = $this->generatePreviewImage($this->user->getId(), $this->course->getImage());
} else {
$data["image"] = $this->translator->trans("no.image.submitted");
}
......
......@@ -22,13 +22,13 @@ class GpxImport
$ext = pathinfo($this->gpx->getClientOriginalName(), PATHINFO_EXTENSION);
if ($ext === "gpx") {
$content = file_get_contents($this->gpx->getPathname());
$posBaliseGpx = strpos($content, "<gpx");
$posBaliseTrk = strpos($content, "<trk");
$posBaliseTrkseg = strpos($content, "<trkseg");
$posBaliseTrkpt = strpos($content, "<trkpt");
$posBaliseTime = strpos($content, "<time");
$posGpxTag = strpos($content, "<gpx");
$posTrkTag = strpos($content, "<trk");
$posTrksegTag = strpos($content, "<trkseg");
$posTrkptTag = strpos($content, "<trkpt");
$posTimeTag = strpos($content, "<time");
if ($posBaliseGpx !== false && $posBaliseTrk !== false && $posBaliseTrkseg !== false && $posBaliseTrkpt !== false && $posBaliseTime !== false) {
if ($posGpxTag !== false && $posTrkTag !== false && $posTrksegTag !== false && $posTrkptTag !== false && $posTimeTag !== false) {
return true;
} else {
$this->error = "invalid Gpx file";
......@@ -86,9 +86,6 @@ class GpxImport
$track->setName($this->nickname);
$track->setFormat($this->format);
$gpxContent = str_replace(array("\n", "\r"), '', (string)$this->loadedGpx->asXML());
$posStrGpx = strpos($gpxContent, "<trk>");
$gpxContent = substr($gpxContent, $posStrGpx);
$gpxContent = "<gpx version=\"1.0\">" . $gpxContent;
$track->setTrace($gpxContent);
$track->setTotalTime($this->extractTotalTime());
$track->setControlPoints($this->extractControlPoints(simplexml_load_string($courseRepository->find($this->idCourse)->getXml())));
......@@ -189,11 +186,11 @@ class GpxImport
{
$sec = (int)($msTotal / 1000);
$ms = $msTotal - ($sec * 1000);
$hou = 0;
$hrs = 0;
$min = 0;
if ($sec >= 3600) {
$hou = floor($sec / 3600);
$hrs = floor($sec / 3600);
$sec = $sec % 3600;
}
if ($sec >= 60) {
......@@ -201,8 +198,8 @@ class GpxImport
$sec = $sec % 60;
}
if (strlen((string)$hou) === 1) {
$hou = "0".$hou;
if (strlen((string)$hrs) === 1) {
$hrs = "0".$hrs;
}
if (strlen((string)$min) === 1) {
$min = "0".$min;
......@@ -217,7 +214,7 @@ class GpxImport
$ms = "0".$ms;
}
return "1970-01-01T".$hou.":".$min.":".$sec.".".$ms."Z";
return "1970-01-01T".$hrs.":".$min.":".$sec.".".$ms."Z";
}
public function distanceBetween($coord1, $coord2): float
......
......@@ -44,6 +44,9 @@ class TrackStatistics
$distance = 0;
$currentLeg = 1;
if (count($wayPoints) < 2) {
return $distances;
}
$previousWayPoint = $wayPoints[0];
foreach ($wayPoints as $wayPoint) {
$nextLegTimeInMillis = $controlPoints[$currentLeg]->punchTime;
......
......@@ -3,12 +3,11 @@
{% block title %}{% trans %}admin_list_planners.title{% endtrans %}{% endblock %}
{% block body %}
<div class="text-center">
<div class="container" style="padding-top: 20pt">
<h2>{% trans %}admin_list_planners.title{% endtrans %}</h2>
<div class="alert-success" role="alert">{{ app.request.get('message') }}</div>
<div class="overflow-auto h-50" style="max-height: 400px;">
<div class="overflow-auto" style="height: 70vh;">
<ul class="list-group">
{% for user in users %}
<li class="list-group-item">
......
......@@ -4,22 +4,30 @@
{% block javascripts %}
<script src="{{ asset('/public/javascript/showMap.js') }}?0.0.1"></script>
<script src="{{ asset('/public/javascript/showMap.js') }}?0.0.1"></script>
{% endblock %}
{% block body %}
<div class="container" style="padding-top: 20pt">
<h2 class="text-center">{% trans %}courses.title{% endtrans %}</h2>
{% set color1 = "#FAF5F0" %}
{% set color2 = "#FFFFFF" %}
{% set color = color2 %}
<div class="border border-secondary rounded">
{% for course in courses %}
<div class="list-group list-group-item-action d-flex flex-row flex-wrap justify-content-end align-items-center p-2">
{% if color == color2 %}
{% set color = color1 %}
{% else %}
{% set color = color2 %}
{% endif %}
<div class="list-group list-group-item-action d-flex flex-row flex-wrap justify-content-end align-items-center p-2" style="background: {{ color }}">
<div class="d-flex flex-column mr-auto" style="max-width: 35vw;">
<span style="overflow-wrap: break-word;">{{ course.name }}</span>
<span style="overflow-wrap: break-word; color: #d24300; font-size: 0.90em;">{{ course.club }}</span>
<span style="overflow-wrap: break-word; color: #d24300; font-size: 0.90em;">{% if course.club != "" %} Création : {{ course.club }} {% endif %}</span>
<span class="flex-shrink-1 badge badge-primary badge-pill">{% trans %}course.number_of_routes{% endtrans %}: {{ course.orienteer|length }}</span>
</div>
{% if course.printable %}
<button onclick="downloadMap('{{ course.image }}')" class="btn btn-secondary">{% trans %}course.print.map{% endtrans %}</button>
<button onclick="downloadMap('{{ course.image }}')" class="btn btn-primary">{% trans %}course.print.map{% endtrans %}</button>
{% endif %}
<a href="{{ path('show_info', {id: course.id}) }}" class="ml-2 btn btn-primary">{% trans %}course.show.info{% endtrans %}</a>
<a href="{{ path('show_tracks', {id: course.id}) }}" class="ml-2 btn btn-primary">{% trans %}course.show.tracks{% endtrans %}</a>
......
......@@ -101,17 +101,17 @@
<div id="description-tab" class="tab-content list-group">
<div class="d-flex">
<div class="">
<p>{% trans %}course.description.author{% endtrans %}{{ creator }}</p>
<p>{% trans %}course.description.author{% endtrans %} {{ creator }}</p>
{% if club != null %}
<p>{% trans %}course.description.club.name{% endtrans %}{{ club }}</p>
<p>{% trans %}course.description.club.name{% endtrans %} {{ club }}</p>
{% endif %}
{% if app.request.locale == 'fr' %}
<p>{% trans %}course.description.date{% endtrans %}{{ day }}/{{ month }}/{{ year }}</p>
<p>{% trans %}course.description.date{% endtrans %} {{ day }}/{{ month }}/{{ year }}</p>
{% else %}
<p>{% trans %}course.description.date{% endtrans %}{{ month }}/{{ day }}/{{ year }}</p>
<p>{% trans %}course.description.date{% endtrans %} {{ year }}/{{ month }}/{{ day }}</p>
{% endif %}
<p>{% trans %}course.description.control.point.number{% endtrans %}{{ control_point_number }}</p>
<p>{% trans %}course.description.length{% endtrans %}{{ length }} m</p>
<p>{% trans %}course.description.control.point.number{% endtrans %} {{ control_point_number }}</p>
<p>{% trans %}course.description.length{% endtrans %} {{ length|number_format(0, null, ' ') }} m</p>
</div>
</div>
</div>
......
......@@ -14,7 +14,6 @@
border-top: 0;
border-bottom: 0;
border-right: 20px solid white;
padding: 10px 5px;
margin-bottom: 2px;
font-size: 14px;
cursor: pointer;
......@@ -64,11 +63,11 @@
const tableEnable = false;
let gpxDictionary = {};
const buttonDictionary = {};
let divLeaflet, overlay;
let divLeaflet, map, overlay;
let timeSheetData = [];
let displayedFormat = 0;
function showMap(map) {
function showMap(map, overlay) {
let xhr = new XMLHttpRequest();
xhr.open("GET", "{{ route_map }}");
xhr.onload = function () {
......@@ -89,22 +88,21 @@
window.addEventListener("load", function () {
divLeaflet = document.createElement("div");
const map = generateMap(divLeaflet);
overlay = L.imageOverlay.rotated("/none", L.latLng([-5, -5]), L.latLng([5, 5]), L.latLng([-5, 5]));
map = generateMap(divLeaflet);
overlay = L.imageOverlay.rotated("/", L.latLng([-5, -5]), L.latLng([5, 5]), L.latLng([-5, 5]));
overlay.addTo(map);
showMap(map);
showMap(map, overlay);
showTimeSheet();
let xhr = new XMLHttpRequest();
xhr.open("GET", "{{ route_track }}");
xhr.onload = function () {
let data = JSON.parse(xhr.response);
if (data.tracks.length === 0) {
let div = document.getElementById("empty-track-list-message");
div.hidden = false;
let div1 = document.getElementById("empty-track-list-message");
div1.hidden = false;
} else {
let div =