diff --git a/config.example.json b/config.example.json index 9c139a9..009ff6c 100644 --- a/config.example.json +++ b/config.example.json @@ -28,6 +28,7 @@ "MAP_INITIAL_LON": 2.5, "PDM_TILES_URL": "http://localhost:7800", "DB_USE_IMPOSM_UPDATE": true, + "USE_SOFT_DATES": false, "WORK_DIR": "/tmp/pdm", "GEOJSON_BOUNDS": { "type": "Polygon", diff --git a/db/01_setup_schema.sql b/db/01_setup_schema.sql index 14458e7..e299272 100644 --- a/db/01_setup_schema.sql +++ b/db/01_setup_schema.sql @@ -303,7 +303,7 @@ BEGIN LIMIT 1; id := 'best_contributor'; - name := 'N°1 des contributions'; + name := 'Top contributor'; acquired := result_userid = the_userid; IF acquired THEN diff --git a/db/20_changes_update.js b/db/20_changes_update.js index b091183..730f507 100644 --- a/db/20_changes_update.js +++ b/db/20_changes_update.js @@ -281,7 +281,9 @@ Object.values(projects).forEach(project => { if (projectLength > 0){ projectsQry = `${projectsQry.substring(0, projectsQry.length-1)} ON CONFLICT (project_id) DO UPDATE SET start_date=EXCLUDED.start_date, end_date=EXCLUDED.end_date`; pgPool.query(projectsQry, (err, res) => { - if (err){ + if(err?.message?.includes("cannot affect row a second time")) { + throw new Error(`Error when installing projects: ${err}\n\nMake sure all projects have a distinct id, query was: ${projectsQry}`); + } else if (err) { throw new Error(`Error when installing projects: ${err}`); } console.log(projectLength+" project(s) installed"); @@ -494,7 +496,7 @@ Object.values(projects).forEach(project => { script += ` echo " => [\$((\$(date -d now +%s) - \$process_start_t0))s] Seek for all changes related to selected features and convert to OPL" rm -f "${oplProject}" - osmium getid ${getIdOptions} "\$history_osh" -I "${oshProjectTags}" -f opl,history=true -o "${oplProject}" + osmium getid ${getIdOptions} "\$history_osh" -I "${oshProjectTags}" -f opl,history=true -o "${oplProject}" || { echo "osmium getid failed, check ${oshProjectTags}"; exit 1; } rm -f "${csvFeatures}" "${csvMembers}" "${oshProjectTags}" ${macroChangesCsv ("init", project, oplProject, csvFeatures, csvUsers, csvMembers, "\$process_start_ts", "\$process_end_tss")} diff --git a/docs/DEVELOP.fr.md b/docs/DEVELOP.fr.md index 4a54f1e..6445790 100644 --- a/docs/DEVELOP.fr.md +++ b/docs/DEVELOP.fr.md @@ -53,6 +53,7 @@ La configuration générale de l'outil est à renseigner dans `config.json`. Un - `OSM_PBF_URL`: URL du fichier OSM.PBF (etat courant de la base, exemple `https://download.geofabrik.de/europe/france-latest.osm.pbf`). Ce fichier n'est pas concerné par le processus d'autorisation. - `POLY_URL`: URL d'un fichier de polygone dans lequel les projets existent (exemple `https://download.geofabrik.de/europe/france.poly`) Ce fichier n'est pas concerné par le processus d'autorisation. - `DB_USE_IMPOSM_UPDATE` : Active ou désactive l'intégration d'imposm3 (permet d'utiliser une base existante et tenue à jour par d'autres moyens, par défaut `true`) +- `USE_SOFT_DATES` : utiliser soft_start_date et soft_end_date (au lieu de start_date et end_date) pour déterminer si un projet est passé, en cours ou à venir - `WORK_DIR` : dossier de téléchargement et stockage temporaire (doit pouvoir contenir le fichier OSH PBF, exemple `/tmp/pdm`) - `OSM_URL` : instance OpenStreetMap à utiliser (exemple `https://www.openstreetmap.org`) - `OSM_API_URL` : instance API OpenStreetMap à utiliser (exemple `https://api.openstreetmap.org`) @@ -101,8 +102,9 @@ Les propriétés dans `info.json` sont les suivantes : - `name` : identifiant de la mission (caractères autorisés : A-Z, 0-9, \_ et -) - `title` : nom de la mission (assez court) - `start_date` : date de début de la mission (format AAAA-MM-JJ) -- `end_date` : date de fin de la mission (format AAAA-MM-JJ) +- `soft_start_date`: date de début de la période de _forte_ animation communautaire (format AAAA-MM-JJ). Donnée purement à titre informatif, n'affecte pas le traitement des données. - `soft_end_date`: date de fin de la période de _forte_ animation communautaire (format AAAA-MM-JJ). Donnée purement à titre informatif, n'affecte pas le traitement des données. +- `end_date` : date de fin de la mission (format AAAA-MM-JJ) - `summary` : résumé de la mission - `links` : définition des URL pour les liens vers des pages tierces (wiki OSM, forum OSM ou page de blog) avec ce format "osmwiki|osmblog|osmforum": "projetdumois.fr" - `database.osmium_tag_filter` : filtre Osmium sur les tags à appliquer pour ne conserver que les objets OSM pertinents (par exemple `nwr/*:covid19`, [syntaxe décrite ici](https://osmcode.org/osmium-tool/manual.html#filtering-by-tags)). Il est possible d'enchaîner plusieurs filtres par & et en répétant l'indication de primitive à chaque niveau. L'opérateur != n'est pour l'instant pas pris en compte. diff --git a/docs/DEVELOP.md b/docs/DEVELOP.md index 5309b70..6e8a6eb 100644 --- a/docs/DEVELOP.md +++ b/docs/DEVELOP.md @@ -53,6 +53,7 @@ The general configuration of the tool is to be filled in `config.json`. There is - `OSM_PBF_URL`: URL of the OSM.PBF file (current state, example `https://download.geofabrik.de/europe/france-latest.osm.pbf`). This file isn't covered by authorization process. - `POLY_URL`: URL of a polygon file holding the perimeter in which projects are considered (example `https://download.geofabrik.de/europe/france.poly`). This file isn't covered by authorization process. - `DB_USE_IMPOSM_UPDATE` : enable or disabled Imposm3 integration (to use an existing database which would be maintained by other means, by default `true`) +- `USE_SOFT_DATES`: whether to use soft_start_date and soft_end_date (instead of start_date and end_date) to decide whether a projects is past, current or next - `WORK_DIR`: download and temporary storage folder (must have capacity to store the OSH PBF file, example `/tmp/pdm`) - `OSM_URL`: OpenStreetMap instance to use (example `https://www.openstreetmap.org`) - `OSM_API_URL` : API OpenStreetMap instance to use (example `https://www.api.openstreetmap.org`) @@ -101,8 +102,9 @@ The properties in `info.json` are as follows: - `name`: mission identifier (authorized characters: A-Z, 0-9, \_ and -) - `title`: name of the mission (short enough) - `start_date`: start date of the mission (format YYYYY-MM-DD) -- `end_date`: end date of the mission (format YYYYY-MM-DD) +- `soft_start_date`: start date of the _strong_ community animation period (format YYYYY-MM-DD). This is only informational, it doesn't affect backend processing. - `soft_end_date`: end date of the _strong_ community animation period (format YYYYY-MM-DD). This is only informational, it doesn't affect backend processing. +- `end_date`: end date of the mission (format YYYYY-MM-DD) - `summary`: summary of the mission - `links`: definition of the URLs for links to third party pages (OSM wiki, OSM forum or blog page) with this format "osmwiki|osmblog|osmforum": "projetdumois.fr" - `database.osmium_tag_filter` : Osmium filter on the tags to be applied to keep only the relevant OSM objects (for example `nwr/*:covid19`, [syntax described here](https://osmcode.org/osmium-tool/manual.html#filtering-by-tags)). It is possible to list many filters using `&` character and same syntax. diff --git a/package-lock.json b/package-lock.json index e1039c9..d07cffc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "bootstrap.native": "^4.2.0", "chart.js": "^4.5.1", "chartjs-adapter-moment": "^1.0.1", + "chartjs-plugin-annotation": "^3.1.0", "compression": "^1.8.1", "cors": "^2.8.5", "express": "^5.2.0", @@ -1140,6 +1141,15 @@ "moment": "^2.10.2" } }, + "node_modules/chartjs-plugin-annotation": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.1.0.tgz", + "integrity": "sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=4.0.0" + } + }, "node_modules/cipher-base": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", @@ -5430,6 +5440,12 @@ "integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==", "requires": {} }, + "chartjs-plugin-annotation": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.1.0.tgz", + "integrity": "sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==", + "requires": {} + }, "cipher-base": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", diff --git a/package.json b/package.json index 66e49d9..967edcc 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "bootstrap.native": "^4.2.0", "chart.js": "^4.5.1", "chartjs-adapter-moment": "^1.0.1", + "chartjs-plugin-annotation": "^3.1.0", "compression": "^1.8.1", "cors": "^2.8.5", "express": "^5.2.0", diff --git a/website/index.js b/website/index.js index 0f7350e..7713609 100644 --- a/website/index.js +++ b/website/index.js @@ -332,24 +332,27 @@ app.get("/projects/:name", (req, res) => { .concat(all.current.filter((p) => p.name !== req.params.name)); const isActive = all.current.length > 0 && - all.current.find((p) => p.name === req.params.name) !== undefined; + all.current.some((p) => p.name === req.params.name); const isNext = - all.next && all.next.find((p) => p.name === req.params.name) !== undefined; - const isHardEnded = all.past.find(p => ( + all.next && all.next.some((p) => p.name === req.params.name); + const isHardEnded = all.past.some(p => ( p.id === req.params.id && p.end_date != null && new Date(p.end_date + "T23:59:59Z").getTime() < Date.now() - )) !== undefined; + )); + const RECENT_PAST_DAYS = 30, + recentPastThreshold = Date.now() - RECENT_PAST_DAYS * 24 * 60 * 60 * 1000; const isRecentPast = all.past && all.past.length > 0 && - all.past.find( - (p) => - p.name === req.params.name && p.end_date != null && - new Date(p.end_date + "T23:59:59Z").getTime() >= - Date.now() - 30 * 24 * 60 * 60 * 1000, - ) !== undefined; + all.past.some((p) =>{ + const endDate = CONFIG.USE_SOFT_DATES && p.soft_end_date || p.end_date; + return p.name === req.params.name && + endDate != null && + new Date(endDate + "T23:59:59Z").getTime() >= recentPastThreshold; + }); + res.render( "pages/project", Object.assign( @@ -433,9 +436,10 @@ app.get("/projects/:name/stats", (req, res) => { const osmUserAuthentified = typeof req.query.osm_user === "string" && req.query.osm_user.trim().length > 0; + const startDate = CONFIG.USE_SOFT_DATES && p.soft_start_date || p.start_date; const daysToKeep = (day) => { if ( - Date.now() - new Date(p.start_date).getTime() < + Date.now() - new Date(startDate).getTime() < 1000 * 60 * 60 * 24 * 60 ) { return true; @@ -455,7 +459,7 @@ app.get("/projects/:name/stats", (req, res) => { const params = { item: ds.item, class: ds.class, - start_date: p.start_date, + start_date: startDate, country: ds.country, }; return fetch( @@ -565,39 +569,26 @@ app.get("/projects/:name/stats", (req, res) => { `, [p.id] ) - .then((results) => ({ - chart: [ - { - label: res.__("Count in OSM"), - data: results.rows.map((r) => ({ x: r.ts, y: r.amount })), - fill: false, - borderColor: "#388E3C", - lineTension: 0, + .then((results) => { + const firstItem = results.rows[0], + lastItem = results.rows.length > 0 ? results.rows[results.rows.length - 1] : null, + secondLastItem = results.rows.length > 1 ? results.rows[results.rows.length - 2] : null; + return { + osm_counts: results.rows, + "daily":{ + "ts": lastItem?.ts, + "ts_start": firstItem?.ts, + "count": lastItem?.amount, + "added": firstItem && lastItem && lastItem.amount - firstItem.amount }, - ], - "daily":{ - "ts": results.rows.length > 0 && - results.rows[results.rows.length - 1].ts, - "ts_start": results.rows[0].ts, - "count":results.rows.length > 0 && - results.rows[results.rows.length - 1].amount, - "added": - results.rows.length > 0 && - results.rows[results.rows.length - 1].amount - - results.rows[0].amount - }, - "past": { - "ts": results.rows.length > 1 && - results.rows[results.rows.length - 1].ts, - "ts_start": results.rows[0].ts, - "count": results.rows.length > 1 && - results.rows[results.rows.length - 2].amount, - "added": - results.rows.length > 1 && - results.rows[results.rows.length - 2].amount - - results.rows[0].amount - } - })), + "past": { + "ts": secondLastItem?.ts, + "ts_start": firstItem?.ts, + "count": secondLastItem?.amount, + "added": firstItem && secondLastItem && secondLastItem.amount - firstItem.amount + } + }; + }), ); // Current time point is only available if a live table is maintained @@ -1121,6 +1112,9 @@ const authorized = { "chartjs-adapter-moment": { "chartjs-adapter-moment.js": "dist/chartjs-adapter-moment.min.js", }, + "chartjs-plugin-annotation": { + "annotation.js": "dist/chartjs-plugin-annotation.min.js", + }, "maplibre-gl": { "maplibre-gl.js": "dist/maplibre-gl.js", "maplibre-gl.css": "dist/maplibre-gl.css", diff --git a/website/locales/en.json b/website/locales/en.json index b8a01a8..4786237 100644 --- a/website/locales/en.json +++ b/website/locales/en.json @@ -141,7 +141,9 @@ "Count in OSM": "Count in OSM", "Number of objects per key": "Number of objects per key", "%s added since %s": "%s added since %s", + "%s added (%s)": "%s added (%s)", "%s currently in OSM": "%s currently in OSM", + "%s in OSM on the last update": "%s in OSM on the last update", "Thank you!": "Thank you!", "Thank you all!": "Thank you all!", "Thanks to all the contributors for making this project a success. Without all of you, OpenStreetMap would be nothing!": "Thanks to all the contributors for making this project a success. Without all of you, OpenStreetMap would be nothing!", @@ -149,5 +151,6 @@ "Participated": "Participated", "They have participated to the project": "They have participated to the project", "Always present": "Always present", - "They have participated to all the projects": "They have participated to all the projects" + "They have participated to all the projects": "They have participated to all the projects", + "Top contributor": "Top contributor" } diff --git a/website/locales/fr.json b/website/locales/fr.json index 4797737..d2704d1 100644 --- a/website/locales/fr.json +++ b/website/locales/fr.json @@ -141,7 +141,9 @@ "Count in OSM": "Nombre dans OSM", "Number of objects per key": "Nombre d'objets pour la clé", "%s added since %s": "%s ajoutés depuis %s", + "%s added (%s)": "%s ajoutés (%s)", "%s currently in OSM": "%s actuellement dans OSM", + "%s in OSM on the last update": "%s dans OSM lors de la dernière mise à jour", "Thank you!": "Merci!", "Thank you all!": "Merci à toutes et tous!", "Thanks to all the contributors for making this project a success. Without all of you, OpenStreetMap would be nothing!": "Merci à l'ensemble des personnes contributrices pour la réussite de ce projet. Sans vous, OpenStreetMap ne serait rien!", @@ -153,5 +155,6 @@ "Near the podium": "Près du podium", "1st place": "1ère place", "2nd place": "2ème place", - "3rd place": "3ème place" + "3rd place": "3ème place", + "Top contributor": "N°1 des contributions" } diff --git a/website/locales/it.json b/website/locales/it.json index 290c487..7b7e378 100644 --- a/website/locales/it.json +++ b/website/locales/it.json @@ -36,7 +36,7 @@ "Total": "Totale", "Country": "Nazione", "Region": "Regione", - "Department": "Provincia", + "Depart.": "Provincia", "City": "Città", "Find all data directly on OpenStreetMap website.": "Trova tutti i dati direttamente sul sito di OpenStreetMap.", "Other projects": "Altri progetti", @@ -140,8 +140,10 @@ "Discuss": "Discussione", "Count in OSM": "Conteggio in OSM", "Number of objects per key": "Numero di oggetti per chiave", - "%s added since %s": "%s aggiunti da allora %s", - "%s currently in OSM": "%s attualmente in OSM", + "%s added since %s": "%s aggiunti dal %s", + "%s added (%s)": "%s aggiunti (%s)", + "%s currently in OSM": "%s attualmente su OSM", + "%s in OSM on the last update": "%s su OSM all'ultimo aggiornamento", "Thank you!": "Grazie!", "Thank you all!": "Grazie a tutti!", "Thanks to all the contributors for making this project a success. Without all of you, OpenStreetMap would be nothing!": "Grazie a tutti i contributori per aver reso questo progetto un successo! Senza di voi OpenStreetMap non sarebbe quello che è!", @@ -161,5 +163,6 @@ "42+ points": "42+ punti", "70+ points": "70+ punti", "100+ points": "100+ punti", - "500+ points": "500+ punti" + "500+ points": "500+ punti", + "Top contributor": "N°1 dei contributi" } diff --git a/website/templates/common/head.pug b/website/templates/common/head.pug index 5614935..432ca9a 100644 --- a/website/templates/common/head.pug +++ b/website/templates/common/head.pug @@ -25,6 +25,7 @@ script(src="/lib/bootstrap.native/bootstrap.js") script(src="/lib/moment/moment.js") script(src="/lib/chart.js/chart.js") script(src="/lib/chartjs-adapter-moment/chartjs-adapter-moment.js") +script(src="/lib/chartjs-plugin-annotation/annotation.js") script(src="/lib/osm-auth/osmauth.js") script(src="/lib/osm-request/osmrequest.js") diff --git a/website/templates/components/stats.pug b/website/templates/components/stats.pug index 12c14e3..3644813 100644 --- a/website/templates/components/stats.pug +++ b/website/templates/components/stats.pug @@ -57,8 +57,10 @@ script. .then(res => res.json()) .then(res => { // Last stats update time - const lastUpdate = document.getElementById("stats-update"); - lastUpdate.innerHTML = "#{__("Last update: ")}" + (new Date(res.lastUpdate)).toLocaleDateString(); + if(res.lastUpdate) { + const lastUpdate = document.getElementById("stats-update"); + lastUpdate.innerHTML = "#{__('Last update: ')}" + (new Date(res.lastUpdate)).toLocaleDateString(); + } // Blocks for small stats const blocks = document.getElementById("stats-blocks"); @@ -96,7 +98,7 @@ script. const sd = document.createElement("div") sd.classList.add(dVal > 0 ? "text-success" : "text-danger") sd.appendChild(addIcon(dVal > 0 ? "arrow-trend-up" : "arrow-trend-down")); - sd.appendChild(document.createTextNode(dClass+" "+dVal)); + sd.appendChild(document.createTextNode(" " + dClass + " " + dVal)); d.appendChild(sd); }else{ dLen--; @@ -123,10 +125,66 @@ script. blocks.appendChild(b); }; - if(res.daily && res.daily.added) { addBlock("plus", numberFormat.format(res.daily.added), `#{__('%s added since %s', title.toLowerCase(), "${(new Date(res.daily.ts_start)).toLocaleDateString()}")}`, {"24h": res.daily.added - res.past.added}); } - if(res.current && res.current.count) { addBlock("location-dot", numberFormat.format(res.current.count), "#{__('%s currently in OSM', title.toLowerCase())}", {"live": res.current.count - res.daily.count}); } - if(res.tasksSolved !== undefined) { addBlock("triangle-exclamation", numberFormat.format(res.tasksSolved), "#{statistics.osmose_tasks}", null); } - if(res.daily && res.daily.nbContributors) { addBlock("users", numberFormat.format(res.daily.nbContributors), "#{__("contributors")}", {"24h": res.daily.nbContributors - res.past.nbContributors}); } + const softStartDate = "#{soft_start_date || ''}", + softEndDate = "#{soft_end_date || ''}", + softStartDateObj = new Date(softStartDate), + softEndDateObj = new Date(softEndDate), + useSoftDates = "#{CONFIG.USE_SOFT_DATES || ''}" === "true" && !isNaN(softStartDateObj) && !isNaN(softEndDateObj), + isDailyDateToday = res.daily?.ts && (new Date().getTime() - new Date(res.daily.ts).getTime()) <= 24 * 60 * 60 * 1000; + + if (useSoftDates && res.osm_counts?.length > 1) { + // Showing added element count between soft dates + const startIndex = res.osm_counts.findIndex(x => new Date(x.ts) >= softStartDateObj), + endIndex = res.osm_counts.findIndex(x => new Date(x.ts) >= softEndDateObj), + softStartItem = res.osm_counts[Math.max(0, startIndex)], + softEndItem = res.osm_counts[endIndex >= 0 ? endIndex : res.osm_counts.length - 1], + softDateRange = new Intl.DateTimeFormat().formatRange(new Date(softStartItem.ts), new Date(softEndItem.ts)); + addBlock( + "plus", + numberFormat.format(softEndItem.amount - softStartItem.amount), + `#{__('%s added (%s)', title.toLowerCase(), "${softDateRange}")}`, + isDailyDateToday ? { "24h": res.daily.added - res.past.added } : null + ); + } else if (res.daily?.added) { + // Showing added element count in the whole project duration + addBlock( + "plus", + numberFormat.format(res.daily.added), + `#{__('%s added since %s', title.toLowerCase(), "${(new Date(res.daily.ts_start)).toLocaleDateString()}")}`, + isDailyDateToday ? {"24h": res.daily.added - res.past.added } : null + ); + } + if(res.current?.count) { + addBlock( + "location-dot", + numberFormat.format(res.current.count), + "#{__('%s currently in OSM', title.toLowerCase())}", + res.daily?.count ? {"live": res.current.count - res.daily.count} : null + ); + } else if (res.daily?.count) { + addBlock( + "location-dot", + numberFormat.format(res.daily.count), + `#{__('%s in OSM on the last update', title.toLowerCase())}`, + null + ); + } + if(res.tasksSolved !== undefined) { + addBlock("triangle-exclamation", numberFormat.format(res.tasksSolved), "#{statistics.osmose_tasks}", null); + } + //- if (useSoftDates) { + //if(res.nbContributors) { addBlock(numberFormat.format(res.nbContributors), "#{__('contributors')}", addedDateRange); } // We use addedDateRange here as well because it's more reliable than the project start/end dates, for example if they have been changed after data initialization + // TODO + //- } else if (res.daily && res.daily.nbContributors) { + if (res.daily?.nbContributors) { + addBlock( + "users", + numberFormat.format(res.daily.nbContributors), + `#{__('contributors')} (${(new Intl.DateTimeFormat()).formatRange(new Date(res.daily.ts_start), new Date(res.daily.ts))})`, + isDailyDateToday ? { "24h": res.daily.nbContributors - res.past.nbContributors } : null + ); + } + // Leaderboard if(res.leaderboard && res.leaderboard.length > 0) { @@ -262,6 +320,21 @@ script. const dateFmt = #{isActive || isNext || isRecentPast || "false"} ? "DD/MM" : "MM/YYYY"; + const annotationOpts = (softStartDate && softEndDate) ? { + annotation: { + annotations: { + softRange: { + type: "box", + xMin: softStartDate, + xMax: softEndDate, + backgroundColor: "rgba(33, 150, 243, 0.1)", + borderColor: "rgba(33, 150, 243, 0.3)", + borderWidth: 1 + } + } + } + } : {}; + // Notes count + chart if(res.chartNotes) { addBlock("pencil", numberFormat.format(res.openedNotes),`#{__("notes opened")} (${res.statClosedNotes} #{__("resolved")})`, null); @@ -273,6 +346,7 @@ script. }, options: { responsive: true, + plugins: annotationOpts, scales: { x: { type: 'time', @@ -294,29 +368,40 @@ script. } // Evolution chart - const ctx = document.getElementById("stats-chart").getContext("2d"); - const myChart = new Chart(ctx, { - type: "line", - data: { - datasets: res.chart - }, - options: { - responsive: true, - scales: { - x: { - type: 'time', - bounds: 'ticks', - time: { - tooltipFormat: "DD/MM/YYYY", - displayFormats: { day: dateFmt }, - round: "day", - unit: "day" - } - }, - y: res.pctTasksDone === undefined || parseInt(res.pctTasksDone) >= 30 ? yAxeLinear : yAxeLogarithmic + if(res.osm_counts?.length) { + const ctx = document.getElementById("stats-chart").getContext("2d"); + const myChart = new Chart(ctx, { + type: "line", + data: { + datasets: [ + { + label: "#{__('Count in OSM')}", + data: res.osm_counts.map((r) => ({ x: r.ts, y: r.amount })), + fill: false, + borderColor: "#388E3C", + lineTension: 0, + }, + ] + }, + options: { + responsive: true, + plugins: annotationOpts, + scales: { + x: { + type: 'time', + bounds: 'ticks', + time: { + tooltipFormat: "DD/MM/YYYY", + displayFormats: { day: dateFmt }, + round: "day", + unit: "day" + } + }, + y: res.pctTasksDone === undefined || parseInt(res.pctTasksDone) >= 30 ? yAxeLinear : yAxeLogarithmic + } } - } - }); + }); + } // Keys chart if(res.chartKeys) { diff --git a/website/templates/pages/project.pug b/website/templates/pages/project.pug index 0827af3..dfcd645 100644 --- a/website/templates/pages/project.pug +++ b/website/templates/pages/project.pug @@ -78,7 +78,7 @@ block content i.fa.fa-question-circle.mr-2(style="font-size: 2.1rem") | #{__("How to contribute")} if isNext - p= __("This project will start on %s, but it's never too soon to start! You can already improve data using our tools or improve the documentation.", new Date(start_date).toLocaleString(getLocale(), { day: "numeric", month: "long" })) + p= __("This project will start on %s, but it's never too soon to start! You can already improve data using our tools or improve the documentation.", new Date(CONFIG.USE_SOFT_DATES ? soft_start_date : start_date).toLocaleString(getLocale(), { day: "numeric", month: "long" })) else if !isActive if isHardEnded p= __("This project is done, but mapping in OpenStreetMap never ends! You can contribute on other topics.") @@ -118,7 +118,7 @@ block content | #{__("Statistics")} if isNext - p= __("Progress statistics will be shown at project start on %s.", new Date(start_date).toLocaleString(getLocale(), { day: "numeric", month: "long" })) + p= __("Progress statistics will be shown at project start on %s.", new Date(CONFIG.USE_SOFT_DATES ? soft_start_date : start_date).toLocaleString(getLocale(), { day: "numeric", month: "long" })) else include ../components/stats.pug diff --git a/website/utils.js b/website/utils.js index e205b65..c939610 100644 --- a/website/utils.js +++ b/website/utils.js @@ -50,16 +50,21 @@ exports.foldProjects = (projects) => { const prjs = { past: [], current: [], next: [] }; Object.values(projects).forEach(project => { const slug = project.name.split("_").pop(); + + // When USE_SOFT_DATES is enabled, prefer soft dates over hard dates + const startDate = (CONFIG.USE_SOFT_DATES && project.soft_start_date) || project.start_date; + const endDate = (CONFIG.USE_SOFT_DATES && project.soft_end_date) || project.end_date; + // Check dates - if(new Date(project.start_date).getTime() <= Date.now() && ((project.end_date == null && project.soft_end_date == null) || Date.now() <= new Date(project.end_date+"T23:59:59Z").getTime() || Date.now() <= new Date(project.soft_end_date+"T23:59:59Z").getTime())) { + if(new Date(startDate).getTime() <= Date.now() && ((endDate == null && project.soft_end_date == null) || Date.now() <= new Date(endDate+"T23:59:59Z").getTime() || Date.now() <= new Date(project.soft_end_date+"T23:59:59Z").getTime())) { prjs.current.push(project); } - else if(Date.now() <= new Date(project.start_date).getTime()) { + else if(Date.now() <= new Date(startDate).getTime()) { prjs.next.push(project); } else if( - (project.end_date != null && new Date(project.end_date+"T23:59:59Z").getTime() < Date.now()) - || (project.end_date == null && project.soft_end_date != null && new Date(project.soft_end_date+"T23:59:59Z").getTime() < Date.now()) + (endDate != null && new Date(endDate+"T23:59:59Z").getTime() < Date.now()) + || (endDate == null && project.soft_end_date != null && new Date(project.soft_end_date+"T23:59:59Z").getTime() < Date.now()) ) { prjs.past.push({ id: project.id,