commit da75a023e3c4c9359d983b2dd15288b2b25be897
Author: Filippo Gentile
Date: Tue Mar 16 10:46:22 2021 +0100
Initial commit
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 0000000..229a78a
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,682 @@
+Current: (***STILL IN PROGRESS***) future 5.25.0
+
+- Removed Lua scripting feature
+
+ROLLINGSTOCK
+- RS Import: show sheet index as 1-based
+- RS Import: FixDuplicatesDlg fix not showing remaining item count in messagebox
+- RS Import: FixDuplicatesDlg do not close if user selects Hide Fixed Items
+- SessionStartEndRSViewer: new title Rollingstock Summary
+- RollingstockManager: added toolbar action to show SessionStartEndRSViewer
+
+ODT EXPORT:
+- SessionRSExport: new P5 style, table contents are non-bold and sans serif now, except header row
+- JobWriter: writeJob() fixed query not reset after use, now reset before binding again
+
+STATIONS:
+- LinesMatchModel: can free query after timer expires
+- LinesSQLModel: on-demand model replacing old LinesMatchModel, LineObj live now inside LineStorage class, member of MeetingSession
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.24.0:
+
+GENERAL
+- Changed cmdline parameter 'test' to '--test' so in case a file is named 'test' we don't interpret it as cmdline parameter
+- QFileDialog: set initial directory to Documents or Pictures
+
+JOBS:
+- EditStopDialog: fixed warning electrical engine on non electricfied line, wrong logic
+
+ROLLINGSTOCK
+- Fixed RS Import: tolerate office version 1.3 and fixed ods keys settings
+- Rs Import: added default type and speed selection for model created from scratch
+- RollingstockSQLModel: UTILS format right justify with '0' engine numbers to at least 3 digits
+
+ODT EXPORT:
+- changed '-Sg:' and '+Ag:' to translatable 'Cp:' and 'Unc:' removed +/- sign, made bold with new T1 text style
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.23.1:
+
+ODT EXPORT:
+- SessionRSExport: prevent overlapping border between table cells
+- StationSheetExport: fixed wrong table-row termination and prevent overlapping borders
+
+JOBS:
+- fixed categories not translated because MeetingSession constructor is called before translations are loaded
+ now categories are loaded on-demand instead of cached in MeetingSession constructor
+- StopModel: prevent setting departure = arrival on normal stops (at least 1 minute stop)
+- StopModel: fixed wrong query always using default freight platform even for passengers
+- StopEditor: prevent setting departure < arrival and on normal stops at least 1 minute
+- StopEditor: holding shift while changing arrival doesn't update departure, without shift deparure follows arrival (stop duration is preserved)
+- JobCategory: added HIGH_SPEED catecory (HSP)
+- JobCategory: FAST_REGIONAL is abbreviated RF (instead of RV) and FREIGHT is abbreviated FRG (instead of FR)
+- JobEditor: warn if user tries to set jobId to an already existent number
+
+SETTINGS:
+- Changed default JobCategory colors
+- Default auto inster transit to true
+- Bigger SettingsDialog size
+
+SEARCHBOX:
+- fixed weird bug sometimes not showing results, turns out that when re-using task we must mReceiver again
+ otherwise we don't receive result (thus results are not shown to the user) and the task deletes himself
+ while SearchResultModel still holds a reference to it causing memory corruption and sometimes also forever hanging on held-lock when stopping all task
+
+MAINWINDOW:
+- Renamed 'Save As' action to 'Save Copy As' because when saving a copy we keep editing original file, while with 'Save As' the user would expect to edit the new file
+- Added warning label that tells user to create at least 1 line in session if there aren't lines and prevents showing QGraphicsView until 1 line is created
+- New managing that goes through setCentralWidgetMode() and checkLineNumber(), tooltips and action are enabled/disabled in a central place
+
+BACKGROUND_MANAGER
+- Added new subfolder backgroundmanager
+- New generic class BackgroundManager which is a central place to manage tasks
+- RsChackerManager: new class to manage Rs Error Checking, tied to BackgroundManager
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.20.1:
+
+DOXYGEN
+- Added Doxygen documentation system to project. Now function must be documented!
+
+METADATA:
+- Added option key to hide/show dates in sheet export
+- Ensure end date is not before start date
+- MeetingInformationDialog: use QTextEdit instead of QPlainTextEdit in order to align text to center and use bold 18pt font to mimic ODT output
+- MetadataManager: new function hasKey() to check presence without retriving value
+
+JOBS:
+- StopModel: uncoupleStillCoupledAtLastStop() fixed uncoupling in all stops instead of just the last one
+- TrainAssetModel: fixed query wrong bind placeholders and table name
+- EditStopDialog: removed redundant refreshData() calls after setStop() calls which internally already refreshes model
+- EditStopDialog: refresh TrainAssetModel after retriving originalArrival from StopModel, otherwise it doesn't load properly
+
+ODT EXPORT:
+- JobWriter: fixed wrong styles in job asset table and rollingstock column
+- JobWriter: new styles to fix overlapping borders in job asset table
+- StationWriter: show also station short name if present
+- ODF compliance: mimetype file must be the first in the zip archive
+- OdtDocument: zip also images
+- OdtDocument: added meta.xml metadata file with some informations and description of the session
+- OdtDocument: added possibility to specify a title which will be stored in meta.xml
+- OdtDocument: use Liberation Sans font for header/footer
+- ShiftSheetExport: added logo picture and other meeting information in the cover page
+- ShiftSheetExport: use Liberation Sand for shift name and meeting location
+- removed function 'writeCellList()' from odtutils.h (use only writeCellListStart/End)
+
+SETTINGS:
+- Added option: SheetStoreLocationDateInMeta, default true, store meeting location and dates in sheets metadata
+
+STATIONS:
+- StationsMatchModel: fixed wrong parenthesis precedence in query leading to duplicates item shown if a station has multiple lines
+
+UTILS
+- ImageViewer: added info lablel showing text saved into image and size in px and cm
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.20.1:
+
+JOBS:
+- StopModel: removed some queries, now they are prepared only when needed
+- StopModel: do not depend on StationsModel and validate platform (ex. platform 6 but station has only 5 platforms)
+- Removed ShiftCombo class in favour of generic CustomCompletionLineEdit
+- RSCoupleDialog: added legend box, explaining the meanings of RS colors
+
+ROLLINGSTOCK:
+- RS Import: allow custom importation like import only RS Models but no owners or viceversa. (Still to import RS you have to import also both Models and Owners)
+- RS Import: custom options per Import Source (Currently ODS and TrainTimetable Session)
+- ImportTask: fixed wrong suffix imported
+- Duplicates: allow not to import one to avoid having to fake its name/number to only then pass and unselect
+- RSImportedOwnersModel::clearDatabeseData(): removed in favour of MeetingSession equivalent function
+- RSImportedRollingstockModel: format number according to RS type
+- FixDuplicatesDlg: allow to go back to previous page
+- RollingStockManager: removed UI file, all UI is now C++ based
+- RollingStockManager: added 'Delete All Models' and 'Delete All Owners'
+- RollingStockManager: allow to search for a RS and view it's plan instead of having to scroll and find it in the list
+- RSJobViewer: fixed wrong owner name displayed
+
+STATIONS:
+- KmSpinBox: better focus on single sections, pressing '+' switches sections
+- RailwayNodeModel: add at 1000 m (1 km) after highest km in line when adding new station
+- StationsModel: prevent invalid default platform (like out of bound)
+
+
+SESSION:
+- rs_list.number NOT NULL, rs_models.suffix NOT NULL, rs_models.type NOT NULL, rs_models.sub_type NOT NULL
+
+METADATA:
+- Fixed memory leak, not deleting ImageMetaData::ImageBlobDevice, QPointer is weak, use std::unique_pointer
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.19.0:
+
+UTILS:
+- IQuittableTask: unified base class for QThreadPool worker tasks
+
+SEARCHBOX:
+- SearchEngine: moved to ISqlFKMatchModel, and use unified CustomCompletionLineEdit
+
+SESSION:
+- removed unused 'rs_view' sql view
+- 'railways' table: renamed column 'km' to 'pos_meters', now position is expressed in meters to achieve a better precision
+
+SQL CONSOLE:
+- Moved files to /sqlconsole/ sub directory and splitted in multiple files
+
+JOBS:
+- StopModel: when propagating time shift, first shift all subsequent stops by 24 hours to avoid hitting UNIQUE constraint, then manually reset one by one
+- StopModel: auto uncouple RS at end of job, and move uncoupled in last stop when adding a new stop after.
+- JobPathEditor: allow multiple stops contiguous selection.
+- StopModel: allow to toggle/set/unset transit on multiple stops at once
+- StopEditor: use CustomCompletionLineEdit to choose station, restrict items to all stations of current line except from previous station
+- JobManager: added 'Remove all Jobs' button
+
+ROLLINGSTOCK:
+- RS Import: IRsImportModel common base class now is a IPagedItemModel
+- RS Import: allow to not import duplicates directly instead of having to choose a fake number and the set not to import.
+- RS Import: allow to import from anothe session
+- RS Import: use custom spinbox delegate for NewNumber column
+- RollingstockManager: added 'Remove all rollingstock' button
+
+STATIONS:
+- Moved railway managing to sub directory /stations/manager/railwanode
+- railwanode: new KmSpinBox delegate to edit station position with format like '12+186'
+- StationManager: use custom spinbox delegate for platform/depots, do not allow negative values
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.18.0:
+
+UTILS:
+- CustomCompletionLineEdit: generic class to complete typing with a ISqlFKMatchModel
+
+SESSION:
+- MeetingSession: enable extended error codes in database for API return values
+- Metadata: added centralized getter/setters
+- MeetingInformationDialog: created dialog to fill metadata about location/days/associations etc
+- MeetingSession: use QStandardPaths to locate settings file path
+
+SHIFTS:
+- ShiftsModel: removed in favour of on-demand ShiftSQLModel
+- Shifts: jobshifts name NOT NULL
+- ShiftBusyDlg: move query into ShiftBusyModel, do not depend on JobsModel
+- ShiftGraph: change job shift by double clicking job line uses now ShiftCombo.
+
+ROLLINGSTOCK:
+- RollingstockModel: removed in favour of on-demand RollingstockSQLModel
+- RSModelsModel: removed in favour of on-demand RSModelsSQLModel
+- RSOwnersModel: removed in favour of on-demand RSOwnersSQLModel
+- RS models: added 'suffix' column, UNIQUE(name,suffix)
+- RSCouplingInterface: do not depend on RollingstockModel and do not manage coupled/uncoupled models
+
+- MergeModelsDialog/MergeOwnersDialog: do not depend on external models
+
+ERROR CHECKER:
+- RsErrorTreeModel: do not depend on external models
+
+JOBS:
+- jobsegmets: added UNIQUE(jobId,num)
+- stops: added UNIQUE(jobId, arrival) and UNIQUE(jobId, departure)
+
+- JobPathEditor: removed Shift QComboBox in favour of CustomCompletionLineEdit with ShiftComboModel
+- TrainGraphics: jobName() utility moved to JobCategoryName (in utils)
+- JobPassesModel: store JobCategory to avoid using JobsModel
+- RSListOnDemandModel: added on-demand model for EditStopDialog to list coupled/uncoupled rs
+- RSProxyModel: do not depend on JobsModel
+- TrainAssetModel: made on-demand
+
+GRAPH:
+- GraphManager: handle settings change internally
+
+ODT EXPORT:
+- ODT Export: give precedence to database metadata for header/footer
+- JobWriter, SessionRSWriter, StationWriter: use less lists, instead write data directly into QXmlStreamWriter as soon as it is selected
+
+STATIONS:
+- StationsModel: used simplified() QString for user input, emit error messagebox if name already exist or station cannot be removed.
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.17.0:
+
+- Started replacing QOds and QuaZip libs with bare libzip
+- OdtDocument: use libzip instead of QuaZip
+- OdtDocument: instead of creating the mimetype file in the temp directory now it's added directly to the zip archive
+
+- SessionRSExport: export ODT sheet for session rs position at start/end
+- SessionStartEndRSViewer: new window to view all RS position at start or end of session order by station/owner, managed by ViewManager
+- Mainwindow: deleted unused action JobPlanner and added RsErrorsWidget action to toolbar and also SessionStartEndRSViewer
+
+- SettingsDialog: added Sheet Export page, with header and footer settings, in future they will be session-specific
+- ShiftGraph: added option to change job shift by double clicking job line and a dialog shows.
+- JobPathEditor: (StopDelegate) replaced line type simbols with lightning svg icon (Electric: normal icon, Diesel lines: lightning with a diaglonal red bar), loading icon "app_path/icons/lightning.svg"
+- Removed unused UI fine app/importrsdialog.ui replaced by RSImportWizard
+
+- StationsModel: added columns DefaulPlatformFreight and DefaulPlatformPassenger, with DefaulPlatformDelegate that uses PlatformSpinBox (simple subclass)
+- StopModel: when setting stop's station the platform resets to station's default platform for that job category
+
+- TrainGraphics: removed enum Category replaced by JobCategory enum in utils headers
+- MeetingSession: use new class JobCategoryName as context for translating categories
+- Settings: job category colors are now accessed by index instead of one getter/setters for each category
+- SettingsDialog: job category colors are now loop-based and grouped in Passenger/Non Passenger in a scroll area
+- StopModel: fixed bug when deleting all job stops it didn't delete last remained segment causing errors and hiding the job in graph (it was trying to delete segment before deleting stop, failing)
+
+- RS Error Checker: from settings now you can choose to start it when opening a file and when a job is edited
+
+- Added common FileFormats class in 'utils/file_format_names.h' to translate file formats in file dialogs
+- New Experimental RS Importer
+- NSIS installer -> new registered file format *.ttt
+
+- RS import: deleted old models and pages, created new ondemand models (data is loaded in small chunks),
+ it can import from ODS and in future also from other sessions,
+ you can filter out owners and models, assign custom names and numbers and match existing models/owners,
+ additionally it detects duplicates in models/owners/RS and impose user to fix them by setting a custom name
+- MeetingSession: check if imported_rs_* tables are empty when opening database, if not give user chance to recover data (probably app crashed) and resume RS importation
+- MeetingSession: new metadata table, now stores FormatVersion when creating a new session and checks it when opening
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.16.0:
+
+NAME CHANGE: removed all occurences on the word FREMO because it might be covered by copyright:
+- App NAME: FremoTimeTable -> TrainTimetable
+- Project TARTGET: fremo_db_5 -> TrainTimetable
+- class FremoSession -> MeetingSession
+- class FremoAppSettings -> TrainTimetableSettings
+- singleton getter macro FremoSettings -> AppSettings
+- typedef for 'fremo_id' -> 'db_id'
+- Removed the 'Fremo' alternative format in Save/Open/New dialogs (extension .fremo)
+- Translations: fremo5_it.ts -> traintimetable_it.ts
+- Changed strings when exporting (printing/printworker.cpp, shifts/shiftgraph/shiftgrapheditor.cpp)
+
+Other Changes;
+- REMOVED INCOMPLETE 'de' AND 'es' translations for now, they will be reintroduced when needed
+
+- StationFreeRSViewer: added sort by 'Job A' and 'Job B' column to model,
+ it can be useful if searching for all RS uncoupled by the same job if multiple jobs uncouple at same time
+
+- Mainwindow: changed title behaviour, now we explicitly set 'applicationDisplayName' and it is shown automatically in the title bar,
+ We don't set the title explicitly (otherwise we end with double 'Train Timetable', one set by us and one by Qt)
+ Removed 'Mainwindow' title from designer mainwindow.ui file.
+ We just set the 'windowFilePath' and reset it in 'closeSession()', this way it shows the stripped filename
+- Mainwindow: removed member QString curFile and added to MeetingSession a member QString fileName
+- Mainwindow: added menu File -> Properties to show file properties, in particular the full file path (not shown anymore in the title bar)
+- info.h added build date constant
+- Mainwindow: about dialog shows build date
+- Started using string constants in info.h instead of repeating app display name
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.15.0:
+
+- RSProxyModel: not anymore subclassing QSortFilterProxyModel with RollingstockModel as source
+- RSProxyModel: loaded by RSCoupleDialog, detect Free RS in station, Unused RS, First used RS, possible wrong operations to remove.
+- RSProxyModel: use background colors to immidiately recognize errors or informations
+- RSCouplingInterface: when checking for a possible next couple operation to delete check only operations of current job
+- EditStopDialog: don't ask for leaving an Engine coupled right after closing uncoupled dialog, instead ask only when Accepting EditStopDialog
+- EditStopDialog: temp remove possibility to Cancel dialog
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.15.0:
+
+- JobPathEditor: prevent context menu while editing stop
+- RollingstockModel: experimental use of cached iterators
+- RSModelsModel: shifted RS SubType by one: Now Electric is 1 (previously zero) and Zero is Invalid
+- RSCouplingInterface: warn if coupling an Electric engine in a non-electric line
+- StationSheetExport: bigger font point size in P4 style for highlighted Arrival/Departure
+- EditStopDialog: FIXED BUG, wrong query for selecting max train speed, was removing uncoupled rollingstock of all jobs and stops instead of considering only stops before the current one and of course only of the current job
+- StopsModel, EditStopDialog: check Arrival, set minimum accepted value to previous departure plus 1 minute
+- EditStopDialog: use originalArrival for getting train speed instead of UI QTimeEdit value because the latter may be changed by the user and might be after the next stop
+- EditStopDialog: renamed onAccepted() slot to normal function saveDataToModel(), directly called in done() instead of connecting to slot
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.14.0:
+
+- StopModel: removed hack aboutToEditCoupling(), startStopEditing() now is called directly from RSCouplingInterface whit bonus thath if from EditStopDialog user opens coupling dialog but doesn't modify couplings, the JobPathEditor is not marked as edited
+- StopModel: reload of stops on revertChanged() is now done internally
+- StopModel: when editing arrival/departure/platform/transit/description now station is also marked for update
+- StopModel: removed q_fixPlatform, more correct platform fix in setStation_intenal and just reusing q_setPlatform
+- EditStopDialog: recalculate platform on station change
+- StationsModel: don't allow negative numberf for platform/depot count. Ensure station has at least 1 platform (main or depot but at least 1)
+- SearchEngine: more efficent, store also JobCategory instead of asking twice per row the category from JobsModel (which involves a QHash lookup)
+- StationFreeRSViewer: added widget/model to view free RS at a given time in a given station
+- StationFreeRSViewer: model can sort by column (but only ascending order)
+- ViewManager: fix bug, didn't connect RS removed signal
+- StationsModel: fixed possible memory leak, when removing line from all stations it didn't delete JobStops. (Luckily they should be already deleted by then because of FK constraint)
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.13.1:
+
+- StopsModel: fix BUG last station not setting rw_node in 'setStation()' (q_getRwNode query called before setting s.stationId -> now uses 'stId' function argument directly)
+- StopModel: warn if Last stop is set Transit or TransitLineChange in the database
+- StopModel: fix BUG if removing Last stop of job and the previous one was transit now becoming the new Last stop it also unsets the 'transit' flag
+- FremoSession: if disconnecting from database returns SQLITE_BUSY (db is still used somewhere) re-prepare queries and abort closing
+- FremoSession: new enum DB_Error to get spcific open/new/close db errors
+- Mainwindow: show messagebox when try to close database while busy
+- Main: wait 10 secs for threapool to finish before closing
+- SearchEngine: new experimental Async Mode
+- Rs Errors Checker: ported from raw QThread to QThreadPool
+- Rs Errors: deprecated and removed RsErrorsModel in favor of RsErrorTreeModel
+- Rs Errors: fixed and update translations
+- Rs Errors: added check for 'Rs not uncoupled at the end of the job' also when coupling twice.
+- Rs error check: added possibility to check only specific RS instead of check all of them
+- JobPathEditor: moved rsToUpdate to StopModel
+- JobPathEditor: when saving or discarding send rsToUpdate to ViewManager and to background error checker for specific check.
+- RSJobViewer: removed call to adjustSize() in updatePlan()
+- RSJobViewer: moved data to new model RsPlanModel
+- RSJobViewer: do not update plan if only info (model, number, owner) changed ('updateInfo()')
+- RSJobViewer: added context menu with 'Show in JobEditor' action
+- RsErrorsWidget: fixed context menu coordinates (mapToGlobal() must be called from viewport)
+- Added file utils/worker_event_types.h as a centralized place for custom QEvent::Type enumeration
+- rs_models: added 'sub_type' column for engine type (Steam, Diesel, Electric)
+- RSModelsModel: added SubType column with delegate
+- ShiftGraph: removed QGraphicsSimpleText items for hour labels. Now they are drawn directly in scene 'drawBackground()', more efficent
+- ShiftGraph: removed hack to manually update the view on graph option change. It's enough to call QGraphicsScene::invalidate(sceneRect) passing the whole scene rect.
+- myMessageHandler: custom message handler now guard QTextStream with a QMutexLocker, seems to fix crashes in QDebug destructor
+- JobPathEditor: moved stationToUpdate logic to StopModel; Now involved StationJobView are updated when saving/discarding job changes
+- StationJobView: using new StationPlanModel, added possibility to show JobEditor selecting the stop.
+- ShiftViewer: using new model ShiftJobModel, added possibility to show job in JobEditor.
+- SearchEngine: fixed bug not resetting query results in only partial result shown
+- JobPathEditor: fixed in Mainwindow that JobPathEditor was enabled when opening a file
+- JobPathEditor: fixed still enabled after current job was removed
+- JobPathEditor: when Job has less than 2 stops but user doesn't want to remove it we give a second chance, not closing still editing
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.13.0:
+
+- StopModel: auto insert transit is now faster because instead of calling 'begingInsertRows()' and 'setStation()' for each transit
+ we now do a single cumulative insert and then use 'setStation_intenal()' that skips dataChanged() and other stuff
+- FremoSession: use database::enable_foreign_keys() instead of executing PRAGMA in openDB() and createNewDB()
+- New Feature: initial experimental implementation of background checker for rollingstock
+- Fix: workaround QT-BUG, QDockWidget doesn't dock anymore if closed whie floating, install event filter and re-dock it manually when closed. See mainwindow.cpp
+- Mainwindow: added RS Errors dock in Top/Bottom areas, allow Job Dock to use all vertical space when RS Errors dock is docked at bottom.
+- BUG FIX ShiftModel: setData() and data() checked idx.column() >= 0 -> return false (Always executed). Changed to idx.column() > 0 (without equal)
+- ShiftModel: print warning when query returns error code
+- SQLite: print warning if library is not thread-safe
+- RSCouplingInterface: fix QMessageBox No/Yes inverted actions
+- RSCouplingInterface: fix query select only one next RS operation and of the same job
+- ViewManager: possible to highlight a specific stop on JobPathEditor (both on current or on newly opened)
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.12.1:
+
+- StopModel: prevent setting transit if there are coupling operations, inform user with QMessageBox
+- Moded ColorView from Settings to Utils subdirectory
+- Updated translations
+- Changed all models to use enum NCols in columnCount
+- StationManager: size 550x500
+- StationsModel: user can choose color for main platforms per each station, chosing white (#FFFFFF) gets back to defult color, added ColorDelegate
+- FremoSettings: auto insert transits defaults to false (disabled)
+- JobSheetExport, StationSheetExport: transit are in italic
+- StopModel: removes old transits before adding new ones
+- StationSheetExport: use 'Dep' instead of 'Depot' because it's shorter
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.11.0:
+
+- ShiftSheetExport: added header with {text \tab \tab Page Number}, mirrored for left pages {Page Number \tab \tab text}
+- ShiftSheetExport: added footer with text
+- StopModel: fix transit was always set to TransitLineChange when using context menu
+- ViewManager: fix Show Next/Prev Segment when there are 2 segment in the same line
+- StopModel: added backup/restore with new tables (old_jobsegments, old_stops, old_coupling)
+- JobPathEditor: moved category/jobId/shiftId logic to StopModel
+- JobPathEditor: is now EditPermission free
+- StationManager: is now EditPermission free
+- ShiftManager: is now EditPermission free
+- ViewManager: is now EditPermission free
+- Deleted EditPermission and EditorManager
+- RollingstockManager: smaller margins, removed button box
+- RollingstockModel: fix bug, not able to create first rollingstock
+- Fix Crash on closing: JobPathEditor tried to check a shift after database was closed
+- Fix JobPathEditor: if job has less than 2 stops (+ AddHere) and chooses to save changes but not to delete job now changes are committed to db
+- Feature: now is possible to edit multiple things at the same time, like adding rollingstock while editing a job
+- JobManager: can now create new job and delete selected job
+- JobPathEditor: translate 'Line %1'
+- Moved TrainGraphics::Category enum to independent JobCategory enum in utils/types.h so there is no need to include the whole traingraphics.h (removed include from fremosession.h)
+- StopModel: default stop time is now category specific, if it's 0 minutes the stop is trasformed into Transit
+- EditStopDialog: fixed bug not changing station if edited from there
+- TrainGraphics: removed unused query q_selectStops
+- ViewManager: removed slot onStAdded, when a station is created it isn't in any line so no need to redraw jobs
+- StationsModel: new signal 'stationNameChanged()' to separate name/short name changes from platform/depots changed, updated ViewManager, JobsModel, ShiftGraphHolder and StationLayer to use this signal instead of 'stationModified()'
+- GraphManager: redraw names in StationLayer if current line is modified
+- StationObj: JobStop stores stopId to avoid duplicates in shadow stops
+- LinesModel: update line when station is added or removed
+- LineStationsModel, StationLinesModel: update line if user changed station km
+- LinesModel: stations platform now have z value of -1 so they stay below jobs and labels
+- TrainGraphics, StationObj::JobStop: labels now have z value of +1 so they stay above jobs and platforms
+- SQLViewer: replaced QStandardItemModel with custom SQLResultModel.
+- SQLConsole: new feature, timed query execute every interval and results blink in red to notify update
+- StopModel: transit are set to 0 minutes stop time, and when switching back from transit stop time becomes default for that job category but at least 1 minute
+- StopModel: fixed bug trying to destroy segment before setting to NULL references to it -> Foreign Key prevented deletion.
+- StopModel: fixed bug resetting otherSegment without resetting also other_rw_node -> removed query q_resetStopNextSeg in favour of setNextSeg()
+- StopModel: fixed bug resetting next line may cause two separate segment adjacent of the same line because it break the loop too early.
+- StopModel: fixed bug transit type was inverted on saving
+- JobPathEditor: rename slot 'onViewMenu' to 'onContextMenu'
+- StopEditor/StopDelegate: now transit are editable too, only arrival box is shown
+- EditStopDialog: on transits disable Departure box and Edit Coupled/uncoupled RS.
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.10.0:
+
+- Changed data() function on many item models to return also on Qt::EditRole so when user edits a cell it's previous value is shown instead of clearing cell contents
+- RSImportWizard: experimental import wizard for rollingstock, almost working
+- JobSheetExport: smaller font on job_stops and wider column A of job_summary
+- Fixed crash: when no line selected in graph StationLayer tried to retrive station names of a null LineObj* pointer. This prevented the creation of new files because new files have no line yet
+- StationLayer: ensure stations names are update when changing line by explicitly calling update()
+- LinesModel: now lines can be non-electric or electric
+- LinesModel: prevent line removal only if there are jobs on that line
+- JobsModel: fix job removal, must delete stops before segments not after
+- DB: added UNIQUE constraint to railways table on (lineId, stationId)
+- DB, StopsModel: now stops table registers also curent and next railway node to prevent user removing railway nodes (uses foreign key restrict)
+- SQLite version 3.29
+- JobsModel: fix foreign key preventing changing jobId -> added ON UPDATE CASCADE to stops and jobsegmets
+- StationLinesModel, LineStationsModel: use query error codes to check if there is already that station in that line to avoid duplicates instead of a separate query
+- TrainGraphics: fixed bug, when setting a new id old stop labels in stations weren't erased so you ended up with multiple labels
+- ShiftGraph: center job name, fix job line height not constant
+- Removed old RSImportDialog in favour of the new RSImportWizard
+- Updated translations
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.9.1:
+
+- EditStopDialog: Major refactor of coupling/uncoupling
+- EditStopDialog: Added logic to remove errors of RS coupld/uncoupled twice
+- EditStopDialog: Removed queries but not q_GetTrainSpeed
+- EditStopDialog: removed button to show all RS, now it updates automatically
+- Sheets: Now odt sheets are translatable
+- StationLayer: fixed bug not refreshing on when station name changed
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.9.0:
+
+- ShiftGraph: shift name labels centered horizontally and automatically add the word Shift -> 'Shift %name%' in ShiftGraph and Shift Sheet
+
+- Job Sheet, Station Sheet, ShiftSheet: column Binario -> Bin, to save space
+
+- Station Sheet: arrival column in bold for Arrival row, Departure in bold for Departure row
+
+- Removed files 'rsnumber.h' and '.cpp', now RS numbers are fixed to XXX-X
+- Added new RsNumberDelegate with custom spinbox (it is enabled only for non Engines)
+
+- Rs delgates moved to /rollingstock/manager/delegates/
+- Rs Dialogs moved to /rollingstock/manager/dialogs/ (MergeOwners and MergeModels)
+
+- Job Sheet: in last stop show asset before stop so reapeat previous row. This is because last stop should uncouple all rollingstock so asset after stop is always empty
+
+- Removed Fremosession::getLineId and FremoSession::getLineName, use LinesModel directly (removed also query from FremoSession)
+
+- Fixed BUG: when chosing to delete a job (because it has less than 2 stop, First + Last) it crashed because of recursion in JobPathEditor::clearJob()
+
+- Job Graph: stations label locked on top like HourPane
+
+- RS: added new types 'Freight Wagon' and 'Coach' instead of generic 'Wagon'. Warning: Engine type was changed from '1' to '0'
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.8.1:
+
+- Transit are now stored and loaded correctly
+- Fix BUG: StationJobView and Station Sheet used iterator even after erase()
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.8.0:
+
+- EditStopDialog: now Passes/Crosses views have label on top.
+- EditStopDialog: removed q_getLineSpeed (now LinesModel from StopsModel is used instead) and q_selectPasses
+- EditStopDialog: moved passes/crosses to a custom model
+
+- GraphManager: fixed bug, if a job was selected and user selected another job with searchbox bot jobs get selected, now only the one from searchbox is selected
+
+- StationJobView: prevent cell editing
+- StationJobView: fixed platform column was skipped on repeated Departure rows
+- StationJobView: don't repeat Arrival on Departure rows
+- StationJobView: reuse 'insertStop()' also for the first time a stop is inserted (removes duplicate code)
+
+- Station Sheet ODT: fixed, it was putting '0' in description column
+- Station Sheet ODT: fixed extension is now ODT instead of ODS
+
+- Station Sheet ODT and StationJobView: fixed BUG, sort repeated rows by Departure
+
+- Job Sheet ODT: don't duplicate Arrival/Departure on first and last stops
+
+- RollingstockModel, RSModelsModel, RSOwnersModel: 'removeRS' and 'removeRSAtRow' now based on 'removeRS_internal' (removed duplicate code)
+
+- RSJobViewer: fixed arrival and departure displayed in minutes (internal integer) instead of 'HH:mm'
+
+- Removed action JobPlanner (deprecated) and added separator before View->Editor toggle
+
+- Updated translations
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.7.2:
+
+- SearchEngine: can search multiple job categories that start with user input
+
+- SearchEngine: fixed error when searching by both category and number
+
+- Forward decleared ViewManager, EditorManager, SearchEngine in fremosession.h
+
+- StationJobView: removed unimplemented Station Graph page and unused PixViewer class, rearranged the layout
+
+- StationJobView: moved in StationManager (/stations/manager) subfolder
+
+- Merge Models: new functionality, with settings options to remove by default the merged model
+
+- Merge Owners: new functionality, with settings options to remove by default the merged owner
+
+- SettingsDialog: better layout
+
+- RS Type: now properly editable through 'Engine/Wagon' combo delegate
+
+- JobPathEditor: when saving (Commit changes pressing 'Ok') resets JobId to the old value if the new one is already in use
+
+- FremoSession: removed 'removeJob' function, use JobsModel directly instead
+
+- DB Fix: 'jobsegmets' table, added FK 'jobId' to jobs(id)
+
+- Logging: log current date and time at start with format 'dd/MM/yyyy HH:mm' instead of day names
+
+- FremoSession: moved some functions and queries to RollingstockModel (like RS_lastKnownPos)
+
+- FremoSession: removed 'getStID' (with q_getStId query) and 'getStName' functions, use StationsModel directly instead
+
+- FremoSettings: added 'HourLineStartOffset' so that hour line start a bit before the first platform of the first station
+
+- FremoSettings: added ShiftGraph options to SettingsDialog
+
+- FremoSettings: removed sheets border settings (unused since switching to ODT instead of ODS)
+
+- Fixed BUG: bug that prevented adding a station to a line if the station had the same ID as a line ID to which it was already added
+ (for instance St 1 in Line 1, cannot add St to other lines)
+
+- Fixed BUG: updating Job Id could lead to crash because JobsModel::lookupTable was not updated with the new iterator from JobsModel::m_data
+
+- Fixed BUG: adding a new stop to job on the same line caused the call to 'resetStopsLine' but there wasn't no 'nextLine' so a warning was printed
+
+- Fixed BUG: TrainAssetModel spelling error table 'rs_model' instead of 'rs_models'
+
+- Fixed BUG: changing shift graph settings while shift graph was open caused glitches (now explicitly call QGraphicsView::update)
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.7.1:
+
+- Removed old FremoSession API for printing/exporting sheets
+
+- Removed unused FlowLayout class (app/flowLayout.cpp)
+
+- Implemented searchbox: SearchEngine can match by category, number or both
+
+- JobPathEditor: trainIdSpin color works also when the widget is not in focus
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.7.0:
+
+- Moved 'platformName()' from FremoSession to utils/platform_utils.h in 'utils' namespace
+
+- Commented 'GraphicsScene::mousePressEvent' in graph/graphicsScene.cpp
+
+- Initial job selection refactor (not finished yet)
+
+- Fixed crash: JobPathEditor read-only mode when clicking job number spinbox and then clicking out, anywhere else
+
+- JobPathEditor in editing mode hides 'Save Job Sheet' button. It doesn't make sense while editing
+
+- JobSheetExport and ShiftSheetExport and StationSheetExport, new API and implementation to write sheets in ODT format rather then ODS format used previously
+ ODT is better for formatting and table columns when there are more tables in the same page. Also better page break handling
+
+- ShiftViewer: minimum width to 600 so user doesn't need to resize
+
+- JobManager: minimum width to 750 and height to 300 so user doesn't need to resize
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.6.0:
+
+- Removed old unused files (mydelegate, importrsdialog, linespage2, jobwizard) -> Smaller executable
+
+- New Feature: (toggable in settings) When adding a stop in JobPathEditor before editing the newly added stop,
+ the editor goes back to the second-last stop (penultima fermata) (former Last stop but user added another)
+ and lets the user choose which line is taken.
+
+- New Experimental feature (not finished yet): StopModel auto adds transits between two stops
+
+- New Experimental feature: 'Show Next/Prev Job Segment' allows to easily navigate between railway lines
+ keeping the job path selected
+
+- Line ComboBox in mainwindow toolbar now changes if current line is programmatically changed
+ (bidirectional connection with GraphManager)
+
+- Fix EditPermission: don't allow multiple 'endEditing()' calls
+
+- Fix JobPathEditor: endEditing() not called in the right place caused infinite loop and read-only mode errors
+
+- Fix LinesModel and StationsModel: it was impossible to change name (it was ignored)
+
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.4.0:
+
+- EditStopDialog2 renamed to EditStopDialog
+
+- LinesModel moved to QMap-based implementation. Lines are now automatically sorted by name
+
+- StationsModel moved to QMap-based implementation. Stations are now automatically sorted by name
+
+- Fix RollingstockModel: switch case in 'setData()' added break
+
+
+----------------------------------------------------------------------------------------------------------------------------------------
+Version 5.3.1:
+
+- New RS Import Dialog: not yet implemented, deprecate old importrsdialog
+
+- New searchbox module: not yet implemented
+
+- Implemented TrainAssetModel
+
+- EditStopDialog2: use TrainAssetModel instead of direct query to show train asset in 'Info' page.
+ also distinguish before/after stop. Hides 'Before' on First and hides 'After' on Last
+
+- Fix SelectionPage: in printing sub module added a break in format switch case
+
+- Fix JobsModel: removing a job now also remove couplings
+
+- JobPathEditor: in read-only mode show context menu but disable actions that are not permitted
+
+- RollingstockModel: when removing a model or owner, it removes all associated RS.
+
+- ShiftGraphHolder: auto refresh station names when edited, use short-name, fallback to full name
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..111f08a
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,158 @@
+cmake_minimum_required(VERSION 3.5)
+include(CMakeDependentOption)
+
+project(TrainTimetable VERSION 5.25.0 LANGUAGES CXX)
+
+option(UPDATE_TS "Update translations" OFF)
+option(UPDATE_TS_KEEP_OBSOLETE "Keep obsolete entries when updating translations" ON)
+option(BUILD_DOXYGEN "Build Doxygen documentation" OFF)
+
+if (WIN32)
+ option(RUN_WINDEPLOYQT "Run windeployqt after executable is installed" ON)
+endif()
+
+## Defines ##
+
+set(DB_FORMAT_VERSION 7)
+
+set(APP_PRODUCT_NAME "TrainTimeTable")
+set(APP_DISPLAY_NAME "Train Timetable")
+set(APP_COMPANY_NAME "Train Software")
+
+set(PROJECT_HOMEPAGE_URL "www.pollofrittomachebuono.altervista.org")
+set(APP_HELP_URL ${APP_ABOUT_URL})
+set(APP_UPDATE_URL ${APP_ABOUT_URL})
+
+set(PROJECT_DESCRIPTION "${APP_DISPLAY_NAME} lets you create and manage model railway sessions")
+
+set(APP_ICON ${CMAKE_SOURCE_DIR}/files/icons/icon.ico)
+
+# Create main application target
+set(TRAINTIMETABLE_TARGET "train-timetable")
+
+## defines end ##
+
+# NSIS Installer
+if(WIN32)
+ configure_file(packaging/windows/NSIS/constants.nsh.in ${CMAKE_BINARY_DIR}/NSIS/constants.nsh @ONLY)
+ configure_file(packaging/windows/NSIS/installer.nsi ${CMAKE_BINARY_DIR}/NSIS/installer.nsi COPYONLY)
+endif()
+
+add_custom_target(NSIS
+ DEPENDS ${TRAINTIMETABLE_TARGET}
+ #COMMAND TODO add NSIS compiler
+ SOURCES
+ packaging/windows/NSIS/constants.nsh.in
+ packaging/windows/NSIS/installer.nsi
+ packaging/windows/resources.rc.in
+ VERBATIM)
+
+## CUSTOM CONFIGURATION ##
+
+option(CONFIG_GLOBAL_TRY_CATCH "Global try/catch at main()" OFF)
+option(CONFIG_NO_DEBUG_CALL_TRACE "Disable scope call trace messages" OFF)
+option(CONFIG_PRINT_DBG_MSG "Debug messages (some)" ON)
+option(CONFIG_ENABLE_BACKGROUND_MANAGER "Enable background task manager" ON)
+cmake_dependent_option(CONFIG_ENABLE_RS_CHECKER "Enable rollingstock checker" ON "CONFIG_ENABLE_BACKGROUND_MANAGER" OFF)
+cmake_dependent_option(CONFIG_SEARCHBOX_MODE_ASYNC "Use thread to search for jobs" ON "CONFIG_ENABLE_BACKGROUND_MANAGER" OFF)
+option(CONFIG_ENABLE_AUTO_TIME_RECALC "Automatic recalculation of travel times based on rollingstock speed, experimental" OFF)
+option(CONFIG_ENABLE_USER_QUERY "Enable SQL console" OFF)
+
+if(CONFIG_GLOBAL_TRY_CATCH)
+ set(TRAINTIMETABLE_DEFINITIONS ${TRAINTIMETABLE_DEFINITIONS} -DGLOBAL_TRY_CATCH)
+endif()
+
+if(CONFIG_NO_DEBUG_CALL_TRACE)
+ set(TRAINTIMETABLE_DEFINITIONS ${TRAINTIMETABLE_DEFINITIONS} -DNO_DEBUG_CALL_TRACE)
+endif()
+
+if(CONFIG_PRINT_DBG_MSG)
+ set(TRAINTIMETABLE_DEFINITIONS ${TRAINTIMETABLE_DEFINITIONS} -DPRINT_DBG_MSG)
+endif()
+
+if(CONFIG_ENABLE_BACKGROUND_MANAGER)
+ set(TRAINTIMETABLE_DEFINITIONS ${TRAINTIMETABLE_DEFINITIONS} -DENABLE_BACKGROUND_MANAGER)
+endif()
+
+if(CONFIG_ENABLE_RS_CHECKER)
+ set(TRAINTIMETABLE_DEFINITIONS ${TRAINTIMETABLE_DEFINITIONS} -DENABLE_RS_CHECKER)
+endif()
+
+if(CONFIG_SEARCHBOX_MODE_ASYNC)
+ set(TRAINTIMETABLE_DEFINITIONS ${TRAINTIMETABLE_DEFINITIONS} -DSEARCHBOX_MODE_ASYNC)
+endif()
+
+if(CONFIG_ENABLE_AUTO_TIME_RECALC)
+ set(TRAINTIMETABLE_DEFINITIONS ${TRAINTIMETABLE_DEFINITIONS} -DENABLE_AUTO_TIME_RECALC)
+endif()
+
+if(CONFIG_ENABLE_USER_QUERY)
+ set(TRAINTIMETABLE_DEFINITIONS ${TRAINTIMETABLE_DEFINITIONS} -DENABLE_USER_QUERY)
+endif()
+
+## Config end ##
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+#set(CMAKE_CXX_EXTENSIONS OFF)
+
+set(CMAKE_AUTOUIC ON)
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+
+set(TRAINTIMETABLE_DEFINITIONS ${TRAINTIMETABLE_DEFINITIONS} -DAPPVERSION="${PROJECT_VERSION}" -DQT_DEPRECATED_WARNINGS)
+
+list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
+
+find_package(Qt5 REQUIRED
+ COMPONENTS
+ Core
+ Gui
+ Widgets
+ Svg
+ PrintSupport
+ LinguistTools)
+
+find_package(SQLite3)
+find_package(ZLIB)
+
+# Locate libzip
+if(NOT ZIP_INCLUDE_DIR OR NOT ZIP_LIBRARY)
+ set(ZIP_INCLUDE_DIR NOTFOUND CACHE PATH "Path to libzip include directory, contains 'zip.h' file.")
+ set(ZIP_LIBRARY NOTFOUND CACHE FILEPATH "Path to libzip library (shared DLL usually libzip.dll).")
+ message(FATAL_ERROR "libzip NOT FOUND (set ZIP_INCLUDE_DIR and ZIP_LIBRARY)")
+endif()
+
+if(BUILD_DOXYGEN)
+ find_package(Doxygen)
+endif()
+
+
+#Locate windeployqt
+if(WIN32 AND RUN_WINDEPLOYQT AND NOT WINDEPLOYQT_EXE)
+ set(WINDEPLOYQT_EXE_TMP NOTFOUND)
+ message("Searching windeployqt executable")
+ if(QT_QMAKE_EXECUTABLE)
+ get_filename_component(WINDEPLOYQT_DIR ${QT_QMAKE_EXECUTABLE} DIRECTORY)
+ set(WINDEPLOYQT_EXE_TMP "${WINDEPLOYQT_DIR}/windeployqt.exe")
+ endif()
+ if(NOT EXISTS ${WINDEPLOYQT_EXE_TMP} AND Qt5_DIR)
+ get_filename_component(WINDEPLOYQT_EXE_TMP "${Qt5_DIR}/../../../bin/windeployqt.exe" REALPATH)
+ endif()
+
+ if(EXISTS ${WINDEPLOYQT_EXE_TMP})
+ message("Found ${WINDEPLOYQT_EXE_TMP}")
+ else()
+ message("windeployqt NOT FOUND")
+ set(WINDEPLOYQT_EXE_TMP NOTFOUND)
+ endif()
+ set(WINDEPLOYQT_EXE ${WINDEPLOYQT_EXE_TMP} CACHE FILEPATH "windeployqt executable file path.")
+ unset(WINDEPLOYQT_EXE_TMP)
+ unset(WINDEPLOYQT_DIR)
+endif()
+
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+add_subdirectory(src)
+add_subdirectory(packaging)
+
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..d8511fa
--- /dev/null
+++ b/TODO
@@ -0,0 +1,329 @@
+
+axel.thobaben@siemens.com
+
+
+TODOS:
+
+BASSA PRIORITA':
+- per chi ha più monitor possibilità di dividere la finestra e ogni monitor può vedere una linea diversa o anche tutti la stessa linea ma in punti diversi, inoltre aggiungere slider zoom per il grafico servizi
+- Funzione nuova: stazioni spunta presenziata/non presenziata: (default true, presenziata), se non è presenziata un treno non può partire/terminare da quella stazione e nei fogli stazione non compare nelle colonne 'Arriva da'/'Parte per' bensì mostra quella prima e quella dopo
+- Aggiungi logo nella copertina dei turni
+- Aggiungi testo impostabile nella copertina turni
+- Files: esetensione personalizzata, icona, file associations
+- Fogli Esportati: Fai scegliere lingua direttamente all'utente (di default è la stessa scelta per il programma)
+
+-------------------------------------
+fare PER ULTIMO
+- Concetto di treno successivo/precedente
+ Aggiungi nel database le relative colonne in 'jobs'
+ Quando si crea un treno in JobPathEditor l'utente può specificare un treno precedente
+ (Quello corrente diventa il suo successivo)
+ - Devono essere della stessa categoria
+ - Deve partire dalla stessa stazione dove finisce il precedente
+
+ Chiedi all'utente se vuole importare i rotabili del treno precedente (Tutto/solo carri/solo loco)
+ es: regionale che fa navetta ha sempre la stessa composizione che viene ricopiata
+
+ I rotabili sono occupati anche da quando finisce un servizio all' inizio del successivo
+ quindi non possono essere agganciati da altri treni
+
+ COUPLING OPERATION:
+ - 0 = Sgancia rotabile
+ - 1 = Aggancia rotabile
+ - 2 = Importato da treno precedente (il rotabile è già in composizione)
+ - 3 = Lascia in composizione per treno successivo
+
+ Queste distizioni servono per capire se alla fine di un servizio un rotabile è libero (op = 0 sganciato)
+ o è occupato in attesa del treno successivo (op = 3)
+
+ Inoltre sono utili per annullare azioni precedenti (Undo)
+ cioè se l'utente aveva impostato un treno precedente e poi lo toglie i rotabili che erano stati importati
+ vengono tolti automaticamente e sul treno precedente segni che i rotabili vengono liberati (op = 0 sganciato)
+
+ Di default sgancia tutti i rotabili alla fine servizio
+ viene campbiato da solo quando si specifica un treno precedente e quindi quello precedente setta i rs a op = 3
+
+ Nel JobPathEditor e nelle Stampe:
+ La prima stazione indica eventuale treno precedente
+ L'ultima stazione indica eventuale treno successivo
+------------------------------------------------------------------------------------------
+
+PRIORITA' MEDIA:
+- Usare QString::simplified() per inserire dati (es. nome stazioni, turni, linee, ecc)
+- Linee: mettere opzione binario singolo e impedisce incroci in linea, FIND_INCROCI_1.sql, FIND_PRECEDENZE_IN_LINEA.sql, però limitare solo a linee contrassegnate come binario singolo e anche ricalcolare quando un servizio viene modificato
+- Categorie Treno: ogni categoria IMPOSTABILE un range NUMERI TRENO massimo/minimo, (Default nelle impostazioni per nuove sessioni, precedenza a valori specifici database)
+ JobPathEditor: quando cambio categoria dalla combobox -> cambia anche il numero treno in base ai range impostati, quando cambio manualmente numero trno fuori dal range avverti e l'utente sceglie se mantenere o annullare la modifica
+- Fogli esportati: inoltre grandezza font impostabile
+- Sgancio avviene con orario di arrivo e Aggancio avviene con orario di partenza, eventualmente avvisa se si aggancia un carro sganciato da un altro treno solo un minuto prima: potrebbe ritardare anche solo se il macchinista non sgancia il carro proprio subito arrivato in stazione ma aspetta un po'
+- Tempo fermata default diverso per ogni categoria impostabile e si salva nel database
+- JobPathEditor: mostra numero progressivo della fermata a partire da 1 sul bordo sinistro dello StopDelegate
+
+------------------------------------------------------------------------------------------
+
+PRIORITA' ALTA:
+- VirtualBox: IE8 in lingua inglese prende male gli orari nei QTimeEdit: 00:00 viene visto come 12:00 AM
+- BIG TODO: cambiando turno se si sta editando fermata non si chiude da sola
+- Ricalcola tempi SEMPRE quando cambiano i rotabili (non chiedere)
+- BIG BIG TODO: quando da EditStopDialog si agganciano rotabili occupati si viene chiesto se rimuovere eventuali altre operazioni anche su altri treni, se poi si annullano le modifiche al servizio queste operzioni non vengono ricostruite perchè non facevano parte di old_couplings, INSERIRLE PRIMA DI RIMUOVERLE o PERMETTERE DI RIMUOVERE OPERAZIONI SOLO DEL PROPRIO SERVIZIO???
+- Quando si tenta di eliminare una stazione o un rotabile che sono utilizzati avvisa l'utente con un messaggio altrimenti sembra che il programma non prenda il comando.
+- Se su una stazione transitano solo senza fermarsi allora può essere tolta da una linea (avvisa, chiedi conferma -> elimina automaticamente tutti i transiti a meno che non siano TransitLineChange e infine toglie stazione dalla linea) -> così una volta tolta la stazione da tutte le linee si può eliminare
+- Piano Giornaliero Stazione: cambia nome pulsante da 'Foglio' in 'Salva Foglio'
+- PDF scegli se pagina unica grande o più fogli A4 (Metti la voce in impostazioni)
+- Precedenze le vede solo il treno che si ferma non quello che supera
+
+- Aggiungi pulsante 'Elimina tutti i rotabili' con conferma e 'Elimina tutti i servizi' e 'Importa layout (stazioni/linee)' e 'Importa Modelli'
+
+- DOXYGEN: generate documentation for every function
+
+
+- Use libsodium for public key license encrypt
+
+
+-------------------------------------------------------------------------------------------
+RISOLTI:
+
+FIXED - ODT export scrivi in grassetto -Ag e +Sg, togli + e - rimane solo Ag: e Sg: inoltre a volte -Sg: non va a capo dopo rotabili agganciati
+
+FIXED - Importa Rotabili: fai scegliere Carro/Locomotiva, e velocità di default per quando non trova il modello e lo deve creare da zero
+
+DONE - QFileDialog opens on Program Files Folder - which is read-only so change default to standard paths documents
+
+DONE - QFileDialog opens on Program Files Folder - which is read-only so change default to standard paths documents
+
+DONE - RSCoupleDialog: aggiungi in basso legenda dei colori
+
+FIXED - La prima fermata la mette sempre che parte da binario 1 dep, perche??? forse perche appena si crea il treno viene impostato a MERCI
+
+FIXED - Categorie treni non tradotte
+
+FIXED - StopEditor, se si imposta manualmente la partenza può essere anche precedente o uguale all'arrivo
+
+DONE - Performance: rendere disattivabile l'avvio automatico del RS Error Checker e il lancio selettivo automatico quando viene modificato un servizio
+
+DONE - Rs Error: lanciare in automatico all'apertura del database
+
+DONE - Fogli esportati: imposta scritte pie pagina e intestazione (Default nelle impostazioni usate per nuove sessioni e sessioni dove non è impostato, precedenza ad impostazioni specifica nel database)
+
+DONE - Rotabili Sessione: rendi ordinabile per propprietario o stazione (sempre ordine alfabetico), rendi stampabile (Foglio odt)
+
+FIXED - BUG: rimuovendo tutte le fermate da un job non elimina il segmento, questo porta a segmenti duplicati
+
+DONE - Stazioni: binario di default arrivo per A (Merci, Postale, LIS) e un altro per B (tutte le altre categorie), quando aggiung una nuova fermata, in base alla categoria sceglie il binario di default (se non impostato sceglie 1) COSA succede quando cambio categoria???
+
+DONE - Grafico Turno: cliccando su un treno esce finestra per cambiare turno senza uscire dalla finestra del grafico (cioè senza passare per JobEditor)
+
+DONE - Linee elettrificate/non elettrificate nel JobPathEditor mettere icona fulmine
+DONE - Errore rotabili da aprire dalla toolbar
+
+DONE - Rimuovere windowTitle dai file del designer perchè occupano solo memoria nelle stringhe del programma
+
+ULTIMI:
+DONE - JobPathEditor: non permettere context menu o EditStopDialog mentre sta editando stop con item delegate
+DONE - Foglio stazioni: scritte in grasetto anche piu grandi
+DONE - EditStopDialog: agganciati devono vedersi solo rotabili liberi in quella stazione
+FIXED - EditStopDialog: BUG applicare orari senza Ricalcola riesce a mettere arrivo a 00:00 che è prima della fermata precedente!
+
+FIXED - file 1.db servizio 40000 fermata Castel di Piave tra gli sganciati non dovrebbe mostrare carro G
+---------------------
+
+DONE - StationsManager: colonna colore stazione metti nome.
+
+DONE - Carri vanno con trattino anche nei fogli stazione e turni.
+- Berlin Sans fb demi-> nome stazione, intestazione e piè pagina
+- Adobe Heiti -> parola Stazione sottolineato (vedi file CASTEL_DI_PIAVE_orario_stazione per i font e le misure esatte)
+
+
+DONE - Rotabili: da StationsManager interrogare una stazione e vedere che rotabili ci sono in quell'orario (e magari ti suggerisce anche il prossimo orario in cui succede una manovra)
+DONE - Aggancio Rotabili: ti permette di agganciare solo rotabili liberi in quella stazione.
+
+DONE - 1) Completare foreign keys per eliminare EditPermission system
+DONE - 2) Completare importazione rotabili
+DONE - 3) Stazioni colore binari principali impostabile diverso per stazione
+DONE - 5) Quando elimini la linea corrente che succede? GraphManager non viene informato!!!
+DONE - 6) Quando agganci locomotiva elettrica in linea non elettrificata mostra avviso
+DONE - 9) Eliminato EditPermission, permetti di eliminare job da JobManager
+FIXED - 11) Ricaica grafico turni sposta linee -> controlla
+
+FIXED - 17) StationsManager: stazioni e linee modificate -> non si aggiorna il grafico
+FIXED - 18) StopModel: transiti devono avere 0 minuti di fermata, le altre almeno 1 minuto
+FIXED - 19) JobPathEditor: non funziona scegli stazione in AddStop quando trasforma in transito la stazione precedente perde il focus tastiera
+FIXED - 21) EditStopDialog: le modifiche al campo stazione non hanno effetto
+
+FIXED - 22) Errore: non elimina segmento se fai VE - Mestre - Mira - Padova e poi da EditStopDialog cambi padova in Mestre
+
+Cambiamenti da fare:
+
+DONE - Nome colonne JobEditor sparisce (?), controllare
+
+DONE - No colonna 'Modello' su tabella Vagoni; si modifica solo nome e numero
+
+DONE - Foglio Job: Mostra assi iniziali del treno SOPRA la Tabella
+
+DONE - Togli prima colonna ("A1") vuota dai fogli
+
+DONE - Bordi celle fogli spessore/colore impostabile
+
+DONE - Controlla 'Opzioni': sembra che non legga i valori di default e quindi mette zero su tutto :-(
+
+DONE - Tasto 'Modifica' RSManager diventa 'Vedi Servizi'
+
+
+DONE - Traduci UI 'Settings'
+
+DONE - Regola spessore linee dei binari e ore del grafico
+
+DONE - Arrivo/Partenza foglio Job sopra tabella
+
+DONE - Più finestre in contemporanea per visualizzare il piano stazione e i rotabili liberi
+
+DONE - Dividere servizi in turni: Un turno equivale ad un libretto macchinista ovvero
+ più servizi a scelta ma che non siano in contemporanea (Si guida un treno alla volta)
+
+DONE - Un grafico separato per ogni linea
+
+
+NUOVI
+
+DONE - TURNO: vedere il grafico con solo treni del turno vedi file:///C:/Filippo/Qt_project/fremo_db_3/files/esempio_grafico_turno.odg
+
+DONE - Migliora pagina orari su EditStopDialog
+
+DONE - JobEditor: Modifica a cascata gli orari
+
+
+NUOVISSIMI
+
+DONE - Elimina categorie 'Alta Velocità' e 'Rapido', inserisci categori 'Postale' e 'Locomotiva in spostamento (LIS)'(Sarebbero i rimandi)
+DONE - Treno che ferma in stazione si vede la fermata su tutti i grafici
+ esempio: treno da Adria ferma a Mestre, anche dal grafico linea Mestre-Padova_AV si vede il binario occupato dal treno da Adria con relativo nome treno
+
+DONE - Di default treno ferma su binario 1 (principale, non deposito)
+
+
+
+DONE - Tempo di fermata di default del treno: non dipende ne da linea ne da stazone, esclusivamente dalla categoria del servizio
+ Quando aggiungi una fermata su JobPathEditor inserisce già il tempo di fermata impostato (poi eventualmente correggi a mano gli orari, ma si modificano a cascata)
+ Tempi (Impostabili): -Merci/Postale: 10min
+ -Tutti i passegeri: 2min
+ -Diretto: transito
+
+- Migliora gestione transiti
+
+DONE - Quando aggiungi fermata su JobPathEditor la considera come LastStop quindi non ti fa impostare direttamente la partenza
+ Possibile soluz: le considera tutte come NormalStop e quando dai l'Ok finale tramuta l'ultima in LastStop
+
+DONE - JobPathEditor calcola già tempo di arrivo in base alla distanza e velocità, poi eventualmente correggi a mano gli orari, ma si modificano a cascata
+
+DONE (FIXED) - Settings non applica spessori linea
+
+----------
+ TASKs
+----------
+
+DONE - Finestra Impostazioni ingrandibile
+
+DONE - Numero treni nel Job Graph Font più piccolo per ridurre sovrapposizioni
+
+DONE PRIORITA' MASSIMA (blocca tutte le atre modifice riguardanti rs)
+- Rotabili separa modello/numero di serie/proprietario:
+ Tabella: rs_models
+ Colonne: ID, Name(String), MaxSpeed, Axes, Engine/Wagon (Nome è composto da lettere e numeri)
+ Contiene le informazioni sui modelli dei rotabili, va riportata in RollingStockManager in un QTabWidget
+
+ Tabella: rs_owners
+ Colonne: owner_id, name(String)
+ Vengono aggiunti da soli quando inseriti dalla pagina 'rollingstock'
+
+ Tabella: rollingstock
+ COLONNE ID, model_id, numero_di_serie(INT), owner_id
+ Su colonna 'owner' propone suggerimenti da 'rs_owners'
+ Il numero di serie è soggetto a formattazione in gruppi sui carri/carrozze (viene applicato in una funzione es getRSName(fremo_id rsId)
+ (Di solito 5 o 4 cifre separate in 3/4 + trattino + ultima cifra)
+
+
+DONE - Le linee delle ore devono finire un po' oltre (più a sinistra) del primo binario della prima stazione
+ Altrimenti il primo binario sembra una linea di separazione
+
+PRIORITA' (BUG)
+DONE - A volte non va menù modifica su JobPathEditor, probabilmente collegato a setReadOnly e il sistema requestEditing
+
+
+DONE - Stazioni Nome abbreviati per grafico servizio e grafico turni, sulle stampe e JobPathEditor nome intero
+ Aggiungere una colonna nel database e nel StationsModel (l'utente inserisce sia nome intero che abbrev quando crea la stazione)
+
+
+PRIORITA' ALTA
+DONE - Stampa Fogli Stazione
+ Colonne: Arrivo, Proviene da, Num Treno, Binario, Partenza, Parte per, Incrocia, Precedenza, Rotabili, Note
+ Colonne più larghe, tutti le celle vanno centrate
+ Transiti e fermate < 1 minuto hanno 1 sola riga; tutto il resto 2 righe separate
+ -Riga di arrivo: colonna partenza vuota
+ -Riga di partenza: colonna arrivo vuota (Così il foglio è più ordinato)
+
+PRIORITA' MEDIA - BASSA
+DONE - Stampa Turno
+ La prima pagina è una copertina (vedi file)
+ Contiene le date Inizio/Fine del fremo meeting, un testo arbitrario, la località dove si svolge il meeting
+ e un logo dell'associazione - infine in basso c'è il nome del turno
+ Questi dati sono comuni a tutti i turni,
+ L'utente le deve inserire in una finestra apposita e vanno salvati nel database
+ Immagine va convertita in un formato stabilito (es PNG, perchè ha alpha) e diventa SQL BINARY BLOB
+ Così se il file viene aperto in un altro pc l'immagine c'è comunque
+
+PRIORITA' ALTA
+- Stampa Servizio (che occupa una pagina intera nella stampa turno)
+ Traduci 'Job' in 'Treno' anzichè 'Servizio' - va centrato in alto (riga 1)
+ Partenza/Arrivo sulla stessa riga (Destra e sinistra) (riga 2)
+ Locomotiva/Assi sulla stessa riga (riga 3)
+ Riga vuota per separare (riga 4)
+ Header Tabella (riga 5)
+
+ Il campo Locomotiva contiene i nomi di tutte le locomotive agganciate nella prima stazione
+ Attenzione: forse è necessario unire celle (span) perchè la tabella sotto ha tante colonne
+
+ Colonna Rotabili: non usare +/-, Scrivi 'Ag' una volta sola e a seguire tutti i rotabili da agganciare
+ poi a capo 'Sg' e tutti i rotabili da sganciare (ovviamente solo se ce ne sono)
+
+ Vedi file: foglio servizio c'è seconda tabella composizione treno (solo colonne 'Da' e 'Rotabili')
+ in sostanza ripete le info Rotabili della prima tabella
+
+ Nell'ultima riga (Stazione di destinazione) la colonna Rotabili mostra un resoconto di ciò che è rimasto in composizione
+ cioè anzichè Ag/Sg mostra tutti i rotabili agganciati, stessa cosa nella seconda tabella
+
+
+
+DONE - Controlla 'StationLinesModel' e 'LineStationsModel' approfonditamente sembrano buggati
+ soprattutto non si salva, i km automatici quando si aggiunge un a stazione sono sballati
+DONE - 'LineStationsModel' non controlla se una stazione è già nella linea prima di aggiungerla
+
+- DONE Rifare da capo il sistema del requestEditing, magari:
+
+ EditingPass p = mgr->requestEditing(JobEditing, AddOrRemove)
+ if(p.canEdit())
+ {
+ ...
+ }
+
+//Quando va fuori scope chiama da solo 'endEditing()' altrimenti lo fai manualmente
+
+------------
+ MODELLI
+------------
+
+***** IMPORTANTE ******
+
+----------------------------------------
+
+DONE - Grafico Turni: nome turno centrato e scritto per esteso 'Turno A' (Aggiunge da solo 'Turno %1' anche nei fogli turno)
+
+DONE - Foglio stazioni: arrivo e partenza in grassetto
+
+DONE - Carri utilizza ultimi 4 numeri 'XXX-X' e fa il padding degli zeri (li aggiunge da solo) es 95 -> 009-5 585 -> 058-5 4458 -> 445-8
+
+FIXED - Crash: Versione 5.8.0: apri desktop/1.db; M40000 aggancia D343 e carro S su Fabiago, sganciali a Cornuda; Stampa piano giornaliero Cornuda -> crashare
+
+DONE - Foglio servizio: tabella composizione ultima stazione mostra prima di sganciare i rotabili (in futuro cambierà il meccanismo con treni successivi/precedenti)
+
+DONE - Colonna Binario su foglio -> 'Bin' per recuperare spazio
+
+DONE - nomi stazioni barra fissa quando si scrolla in basso coe per le ore
diff --git a/cmake/FindSQLite3.cmake.old b/cmake/FindSQLite3.cmake.old
new file mode 100644
index 0000000..10a28ae
--- /dev/null
+++ b/cmake/FindSQLite3.cmake.old
@@ -0,0 +1,66 @@
+# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
+# file Copyright.txt or https://cmake.org/licensing for details.
+
+#[=======================================================================[.rst:
+FindSQLite3
+-----------
+
+Find the SQLite libraries, v3
+
+IMPORTED targets
+^^^^^^^^^^^^^^^^
+
+This module defines the following :prop_tgt:`IMPORTED` target:
+
+``SQLite::SQLite3``
+
+Result variables
+^^^^^^^^^^^^^^^^
+
+This module will set the following variables if found:
+
+``SQLite3_INCLUDE_DIRS``
+ where to find sqlite3.h, etc.
+``SQLite3_LIBRARIES``
+ the libraries to link against to use SQLite3.
+``SQLite3_VERSION``
+ version of the SQLite3 library found
+``SQLite3_FOUND``
+ TRUE if found
+
+#]=======================================================================]
+
+# Look for the necessary header
+find_path(SQLite3_INCLUDE_DIR NAMES sqlite3.h)
+mark_as_advanced(SQLite3_INCLUDE_DIR)
+
+# Look for the necessary library
+find_library(SQLite3_LIBRARY NAMES sqlite3 sqlite)
+mark_as_advanced(SQLite3_LIBRARY)
+
+# Extract version information from the header file
+if(SQLite3_INCLUDE_DIR)
+ file(STRINGS ${SQLite3_INCLUDE_DIR}/sqlite3.h _ver_line
+ REGEX "^#define SQLITE_VERSION *\"[0-9]+\\.[0-9]+\\.[0-9]+\""
+ LIMIT_COUNT 1)
+ string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+"
+ SQLite3_VERSION "${_ver_line}")
+ unset(_ver_line)
+endif()
+
+include(${CMAKE_ROOT}/Modules/FindPackageHandleStandardArgs.cmake)
+find_package_handle_standard_args(SQLite3
+ REQUIRED_VARS SQLite3_INCLUDE_DIR SQLite3_LIBRARY
+ VERSION_VAR SQLite3_VERSION)
+
+# Create the imported target
+if(SQLite3_FOUND)
+ set(SQLite3_INCLUDE_DIRS ${SQLite3_INCLUDE_DIR})
+ set(SQLite3_LIBRARIES ${SQLite3_LIBRARY})
+ if(NOT TARGET SQLite::SQLite3)
+ add_library(SQLite::SQLite3 UNKNOWN IMPORTED)
+ set_target_properties(SQLite::SQLite3 PROPERTIES
+ IMPORTED_LOCATION "${SQLite3_LIBRARY}"
+ INTERFACE_INCLUDE_DIRECTORIES "${SQLite3_INCLUDE_DIR}")
+ endif()
+endif()
\ No newline at end of file
diff --git a/cmake/FindZip.cmake.old b/cmake/FindZip.cmake.old
new file mode 100644
index 0000000..a7e89d4
--- /dev/null
+++ b/cmake/FindZip.cmake.old
@@ -0,0 +1,63 @@
+# Searches for an installation of the zip library. On success, it sets the following variables:
+#
+# Zip_FOUND Set to true to indicate the zip library was found
+# Zip_INCLUDE_DIRS The directory containing the header file zip/zip.h
+# Zip_LIBRARIES The libraries needed to use the zip library
+#
+# To specify an additional directory to search, set Zip_ROOT.
+#
+# Author: Siddhartha Chaudhuri, 2009
+#
+
+# Look for the header, first in the user-specified location and then in the system locations
+SET(Zip_INCLUDE_DOC "The directory containing the header file zip/zip.h")
+FIND_PATH(Zip_INCLUDE_DIRS NAMES zip/zip.h PATHS ${Zip_ROOT} ${Zip_ROOT}/include DOC ${Zip_INCLUDE_DOC} NO_DEFAULT_PATH)
+IF(NOT Zip_INCLUDE_DIRS) # now look in system locations
+ FIND_PATH(Zip_INCLUDE_DIRS NAMES zip/zip.h DOC ${Zip_INCLUDE_DOC})
+ENDIF(NOT Zip_INCLUDE_DIRS)
+
+SET(Zip_FOUND FALSE)
+
+IF(Zip_INCLUDE_DIRS)
+ SET(Zip_LIBRARY_DIRS ${Zip_INCLUDE_DIRS})
+
+ IF("${Zip_LIBRARY_DIRS}" MATCHES "/include$")
+ # Strip off the trailing "/include" in the path.
+ GET_FILENAME_COMPONENT(Zip_LIBRARY_DIRS ${Zip_LIBRARY_DIRS} PATH)
+ ENDIF("${Zip_LIBRARY_DIRS}" MATCHES "/include$")
+
+ IF(EXISTS "${Zip_LIBRARY_DIRS}/lib")
+ SET(Zip_LIBRARY_DIRS ${Zip_LIBRARY_DIRS}/lib)
+ ENDIF(EXISTS "${Zip_LIBRARY_DIRS}/lib")
+
+ # Find Zip libraries
+ FIND_LIBRARY(Zip_DEBUG_LIBRARY NAMES zipd zip_d libzipd libzip_d
+ PATH_SUFFIXES Debug ${CMAKE_LIBRARY_ARCHITECTURE} ${CMAKE_LIBRARY_ARCHITECTURE}/Debug
+ PATHS ${Zip_LIBRARY_DIRS} NO_DEFAULT_PATH)
+ FIND_LIBRARY(Zip_RELEASE_LIBRARY NAMES zip libzip
+ PATH_SUFFIXES Release ${CMAKE_LIBRARY_ARCHITECTURE} ${CMAKE_LIBRARY_ARCHITECTURE}/Release
+ PATHS ${Zip_LIBRARY_DIRS} NO_DEFAULT_PATH)
+
+ SET(Zip_LIBRARIES )
+ IF(Zip_DEBUG_LIBRARY AND Zip_RELEASE_LIBRARY)
+ SET(Zip_LIBRARIES debug ${Zip_DEBUG_LIBRARY} optimized ${Zip_RELEASE_LIBRARY})
+ ELSEIF(Zip_DEBUG_LIBRARY)
+ SET(Zip_LIBRARIES ${Zip_DEBUG_LIBRARY})
+ ELSEIF(Zip_RELEASE_LIBRARY)
+ SET(Zip_LIBRARIES ${Zip_RELEASE_LIBRARY})
+ ENDIF(Zip_DEBUG_LIBRARY AND Zip_RELEASE_LIBRARY)
+
+ IF(Zip_LIBRARIES)
+ SET(Zip_FOUND TRUE)
+ ENDIF(Zip_LIBRARIES)
+ENDIF(Zip_INCLUDE_DIRS)
+
+IF(Zip_FOUND)
+ IF(NOT Zip_FIND_QUIETLY)
+ MESSAGE(STATUS "Found Zip: headers at ${Zip_INCLUDE_DIRS}, libraries at ${Zip_LIBRARY_DIRS}")
+ ENDIF(NOT Zip_FIND_QUIETLY)
+ELSE(Zip_FOUND)
+ IF(Zip_FIND_REQUIRED)
+ MESSAGE(FATAL_ERROR "Zip library not found")
+ ENDIF(Zip_FIND_REQUIRED)
+ENDIF(Zip_FOUND)
\ No newline at end of file
diff --git a/files/Prefazione_generale_all_orario_di_servizio.pdf b/files/Prefazione_generale_all_orario_di_servizio.pdf
new file mode 100644
index 0000000..d4e80e6
Binary files /dev/null and b/files/Prefazione_generale_all_orario_di_servizio.pdf differ
diff --git a/files/diagram/fremo_db_format_4.pdf b/files/diagram/fremo_db_format_4.pdf
new file mode 100644
index 0000000..aa2e404
Binary files /dev/null and b/files/diagram/fremo_db_format_4.pdf differ
diff --git a/files/diagram/fremo_db_format_4_2.pdf b/files/diagram/fremo_db_format_4_2.pdf
new file mode 100644
index 0000000..0d38de9
Binary files /dev/null and b/files/diagram/fremo_db_format_4_2.pdf differ
diff --git a/files/diagram/fremo_db_format_4_3.pdf b/files/diagram/fremo_db_format_4_3.pdf
new file mode 100644
index 0000000..e21145f
Binary files /dev/null and b/files/diagram/fremo_db_format_4_3.pdf differ
diff --git a/files/diagram/fremo_db_format_V_4_3.pdf b/files/diagram/fremo_db_format_V_4_3.pdf
new file mode 100644
index 0000000..bdaa1c6
Binary files /dev/null and b/files/diagram/fremo_db_format_V_4_3.pdf differ
diff --git a/files/diagram/old_format_diagram.pdf b/files/diagram/old_format_diagram.pdf
new file mode 100644
index 0000000..80daf3e
Binary files /dev/null and b/files/diagram/old_format_diagram.pdf differ
diff --git a/files/fs_carri_merci.pdf b/files/fs_carri_merci.pdf
new file mode 100644
index 0000000..6c0113d
Binary files /dev/null and b/files/fs_carri_merci.pdf differ
diff --git a/files/icons/bullet-train.svg b/files/icons/bullet-train.svg
new file mode 100644
index 0000000..4bbf07c
--- /dev/null
+++ b/files/icons/bullet-train.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/files/icons/examples/GIMP/gimp-2000_16x16.png b/files/icons/examples/GIMP/gimp-2000_16x16.png
new file mode 100644
index 0000000..930ed80
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2000_16x16.png differ
diff --git a/files/icons/examples/GIMP/gimp-2000_32x32.png b/files/icons/examples/GIMP/gimp-2000_32x32.png
new file mode 100644
index 0000000..00123f4
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2000_32x32.png differ
diff --git a/files/icons/examples/GIMP/gimp-2001_16x16.png b/files/icons/examples/GIMP/gimp-2001_16x16.png
new file mode 100644
index 0000000..930ed80
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2001_16x16.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_jumbo_48x48.png b/files/icons/examples/GIMP/gimp-2_shell_jumbo_48x48.png
new file mode 100644
index 0000000..93db386
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_jumbo_48x48.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_l_32x32.png b/files/icons/examples/GIMP/gimp-2_shell_l_32x32.png
new file mode 100644
index 0000000..3ab9bf4
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_l_32x32.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_open_jumbo_48x48.png b/files/icons/examples/GIMP/gimp-2_shell_open_jumbo_48x48.png
new file mode 100644
index 0000000..93db386
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_open_jumbo_48x48.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_open_l_32x32.png b/files/icons/examples/GIMP/gimp-2_shell_open_l_32x32.png
new file mode 100644
index 0000000..3ab9bf4
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_open_l_32x32.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_open_s_16x16.png b/files/icons/examples/GIMP/gimp-2_shell_open_s_16x16.png
new file mode 100644
index 0000000..183a7e5
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_open_s_16x16.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_open_sh_32x32.png b/files/icons/examples/GIMP/gimp-2_shell_open_sh_32x32.png
new file mode 100644
index 0000000..3ab9bf4
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_open_sh_32x32.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_open_xl_48x48.png b/files/icons/examples/GIMP/gimp-2_shell_open_xl_48x48.png
new file mode 100644
index 0000000..8919408
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_open_xl_48x48.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_s_16x16.png b/files/icons/examples/GIMP/gimp-2_shell_s_16x16.png
new file mode 100644
index 0000000..183a7e5
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_s_16x16.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_sel_jumbo_48x48.png b/files/icons/examples/GIMP/gimp-2_shell_sel_jumbo_48x48.png
new file mode 100644
index 0000000..93db386
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_sel_jumbo_48x48.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_sel_l_32x32.png b/files/icons/examples/GIMP/gimp-2_shell_sel_l_32x32.png
new file mode 100644
index 0000000..8eb284a
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_sel_l_32x32.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_sel_s_16x16.png b/files/icons/examples/GIMP/gimp-2_shell_sel_s_16x16.png
new file mode 100644
index 0000000..6870770
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_sel_s_16x16.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_sel_sh_32x32.png b/files/icons/examples/GIMP/gimp-2_shell_sel_sh_32x32.png
new file mode 100644
index 0000000..8eb284a
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_sel_sh_32x32.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_sel_xl_48x48.png b/files/icons/examples/GIMP/gimp-2_shell_sel_xl_48x48.png
new file mode 100644
index 0000000..8919408
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_sel_xl_48x48.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_sh_32x32.png b/files/icons/examples/GIMP/gimp-2_shell_sh_32x32.png
new file mode 100644
index 0000000..3ab9bf4
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_sh_32x32.png differ
diff --git a/files/icons/examples/GIMP/gimp-2_shell_xl_48x48.png b/files/icons/examples/GIMP/gimp-2_shell_xl_48x48.png
new file mode 100644
index 0000000..8919408
Binary files /dev/null and b/files/icons/examples/GIMP/gimp-2_shell_xl_48x48.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw000_16x16.png b/files/icons/examples/Libreoffice/sdraw000_16x16.png
new file mode 100644
index 0000000..d184cfe
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw000_16x16.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw000_32x32.png b/files/icons/examples/Libreoffice/sdraw000_32x32.png
new file mode 100644
index 0000000..dcec226
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw000_32x32.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_jumbo_48x48.png b/files/icons/examples/Libreoffice/sdraw_shell_jumbo_48x48.png
new file mode 100644
index 0000000..3189bf0
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_jumbo_48x48.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_l_32x32.png b/files/icons/examples/Libreoffice/sdraw_shell_l_32x32.png
new file mode 100644
index 0000000..dcec226
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_l_32x32.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_open_jumbo_48x48.png b/files/icons/examples/Libreoffice/sdraw_shell_open_jumbo_48x48.png
new file mode 100644
index 0000000..3189bf0
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_open_jumbo_48x48.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_open_l_32x32.png b/files/icons/examples/Libreoffice/sdraw_shell_open_l_32x32.png
new file mode 100644
index 0000000..dcec226
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_open_l_32x32.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_open_s_16x16.png b/files/icons/examples/Libreoffice/sdraw_shell_open_s_16x16.png
new file mode 100644
index 0000000..d184cfe
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_open_s_16x16.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_open_sh_32x32.png b/files/icons/examples/Libreoffice/sdraw_shell_open_sh_32x32.png
new file mode 100644
index 0000000..dcec226
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_open_sh_32x32.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_open_xl_48x48.png b/files/icons/examples/Libreoffice/sdraw_shell_open_xl_48x48.png
new file mode 100644
index 0000000..ead48ab
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_open_xl_48x48.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_s_16x16.png b/files/icons/examples/Libreoffice/sdraw_shell_s_16x16.png
new file mode 100644
index 0000000..d184cfe
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_s_16x16.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_sel_jumbo_48x48.png b/files/icons/examples/Libreoffice/sdraw_shell_sel_jumbo_48x48.png
new file mode 100644
index 0000000..3189bf0
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_sel_jumbo_48x48.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_sel_l_32x32.png b/files/icons/examples/Libreoffice/sdraw_shell_sel_l_32x32.png
new file mode 100644
index 0000000..e1ec367
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_sel_l_32x32.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_sel_s_16x16.png b/files/icons/examples/Libreoffice/sdraw_shell_sel_s_16x16.png
new file mode 100644
index 0000000..4935de9
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_sel_s_16x16.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_sel_sh_32x32.png b/files/icons/examples/Libreoffice/sdraw_shell_sel_sh_32x32.png
new file mode 100644
index 0000000..e1ec367
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_sel_sh_32x32.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_sel_xl_48x48.png b/files/icons/examples/Libreoffice/sdraw_shell_sel_xl_48x48.png
new file mode 100644
index 0000000..ead48ab
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_sel_xl_48x48.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_sh_32x32.png b/files/icons/examples/Libreoffice/sdraw_shell_sh_32x32.png
new file mode 100644
index 0000000..dcec226
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_sh_32x32.png differ
diff --git a/files/icons/examples/Libreoffice/sdraw_shell_xl_48x48.png b/files/icons/examples/Libreoffice/sdraw_shell_xl_48x48.png
new file mode 100644
index 0000000..ead48ab
Binary files /dev/null and b/files/icons/examples/Libreoffice/sdraw_shell_xl_48x48.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_jumbo_48x48.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_jumbo_48x48.png
new file mode 100644
index 0000000..9aa05ca
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_jumbo_48x48.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_l_32x32.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_l_32x32.png
new file mode 100644
index 0000000..e32dfcd
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_l_32x32.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_open_jumbo_48x48.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_open_jumbo_48x48.png
new file mode 100644
index 0000000..9aa05ca
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_open_jumbo_48x48.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_open_l_32x32.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_open_l_32x32.png
new file mode 100644
index 0000000..e32dfcd
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_open_l_32x32.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_open_s_16x16.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_open_s_16x16.png
new file mode 100644
index 0000000..ebd79b9
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_open_s_16x16.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_open_sh_32x32.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_open_sh_32x32.png
new file mode 100644
index 0000000..e32dfcd
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_open_sh_32x32.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_open_xl_48x48.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_open_xl_48x48.png
new file mode 100644
index 0000000..ab7619d
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_open_xl_48x48.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_s_16x16.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_s_16x16.png
new file mode 100644
index 0000000..ebd79b9
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_s_16x16.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_jumbo_48x48.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_jumbo_48x48.png
new file mode 100644
index 0000000..9aa05ca
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_jumbo_48x48.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_l_32x32.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_l_32x32.png
new file mode 100644
index 0000000..0729d8f
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_l_32x32.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_s_16x16.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_s_16x16.png
new file mode 100644
index 0000000..a5b4db2
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_s_16x16.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_sh_32x32.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_sh_32x32.png
new file mode 100644
index 0000000..0729d8f
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_sh_32x32.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_xl_48x48.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_xl_48x48.png
new file mode 100644
index 0000000..ab7619d
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_sel_xl_48x48.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_sh_32x32.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_sh_32x32.png
new file mode 100644
index 0000000..e32dfcd
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_sh_32x32.png differ
diff --git a/files/icons/examples/QtCreator/MaintenanceTool_shell_xl_48x48.png b/files/icons/examples/QtCreator/MaintenanceTool_shell_xl_48x48.png
new file mode 100644
index 0000000..ab7619d
Binary files /dev/null and b/files/icons/examples/QtCreator/MaintenanceTool_shell_xl_48x48.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator000_16x16.png b/files/icons/examples/QtCreator/qtcreator000_16x16.png
new file mode 100644
index 0000000..e455b24
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator000_16x16.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator000_32x32.png b/files/icons/examples/QtCreator/qtcreator000_32x32.png
new file mode 100644
index 0000000..be34319
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator000_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator001_16x16.png b/files/icons/examples/QtCreator/qtcreator001_16x16.png
new file mode 100644
index 0000000..9280728
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator001_16x16.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator001_32x32.png b/files/icons/examples/QtCreator/qtcreator001_32x32.png
new file mode 100644
index 0000000..97ea639
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator001_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator002_16x16.png b/files/icons/examples/QtCreator/qtcreator002_16x16.png
new file mode 100644
index 0000000..3df4fb5
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator002_16x16.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator002_32x32.png b/files/icons/examples/QtCreator/qtcreator002_32x32.png
new file mode 100644
index 0000000..d43fd1a
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator002_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator003_16x16.png b/files/icons/examples/QtCreator/qtcreator003_16x16.png
new file mode 100644
index 0000000..b0bc537
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator003_16x16.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator003_32x32.png b/files/icons/examples/QtCreator/qtcreator003_32x32.png
new file mode 100644
index 0000000..5f1d047
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator003_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator004_16x16.png b/files/icons/examples/QtCreator/qtcreator004_16x16.png
new file mode 100644
index 0000000..25edbb8
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator004_16x16.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator004_32x32.png b/files/icons/examples/QtCreator/qtcreator004_32x32.png
new file mode 100644
index 0000000..1559ca2
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator004_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator005_16x16.png b/files/icons/examples/QtCreator/qtcreator005_16x16.png
new file mode 100644
index 0000000..9028273
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator005_16x16.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator005_32x32.png b/files/icons/examples/QtCreator/qtcreator005_32x32.png
new file mode 100644
index 0000000..4d927ef
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator005_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator006_16x16.png b/files/icons/examples/QtCreator/qtcreator006_16x16.png
new file mode 100644
index 0000000..bfa4e69
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator006_16x16.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator006_32x32.png b/files/icons/examples/QtCreator/qtcreator006_32x32.png
new file mode 100644
index 0000000..6da5670
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator006_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator007_16x16.png b/files/icons/examples/QtCreator/qtcreator007_16x16.png
new file mode 100644
index 0000000..5e23733
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator007_16x16.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator007_32x32.png b/files/icons/examples/QtCreator/qtcreator007_32x32.png
new file mode 100644
index 0000000..174cf44
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator007_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_jumbo_48x48.png b/files/icons/examples/QtCreator/qtcreator_shell_jumbo_48x48.png
new file mode 100644
index 0000000..ec9a5d8
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_jumbo_48x48.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_l_32x32.png b/files/icons/examples/QtCreator/qtcreator_shell_l_32x32.png
new file mode 100644
index 0000000..5eb9248
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_l_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_open_jumbo_48x48.png b/files/icons/examples/QtCreator/qtcreator_shell_open_jumbo_48x48.png
new file mode 100644
index 0000000..ec9a5d8
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_open_jumbo_48x48.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_open_l_32x32.png b/files/icons/examples/QtCreator/qtcreator_shell_open_l_32x32.png
new file mode 100644
index 0000000..5eb9248
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_open_l_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_open_s_16x16.png b/files/icons/examples/QtCreator/qtcreator_shell_open_s_16x16.png
new file mode 100644
index 0000000..1e7e87a
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_open_s_16x16.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_open_sh_32x32.png b/files/icons/examples/QtCreator/qtcreator_shell_open_sh_32x32.png
new file mode 100644
index 0000000..5eb9248
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_open_sh_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_open_xl_48x48.png b/files/icons/examples/QtCreator/qtcreator_shell_open_xl_48x48.png
new file mode 100644
index 0000000..4196517
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_open_xl_48x48.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_s_16x16.png b/files/icons/examples/QtCreator/qtcreator_shell_s_16x16.png
new file mode 100644
index 0000000..1e7e87a
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_s_16x16.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_sel_jumbo_48x48.png b/files/icons/examples/QtCreator/qtcreator_shell_sel_jumbo_48x48.png
new file mode 100644
index 0000000..ec9a5d8
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_sel_jumbo_48x48.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_sel_l_32x32.png b/files/icons/examples/QtCreator/qtcreator_shell_sel_l_32x32.png
new file mode 100644
index 0000000..7ac0a66
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_sel_l_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_sel_s_16x16.png b/files/icons/examples/QtCreator/qtcreator_shell_sel_s_16x16.png
new file mode 100644
index 0000000..dfd220f
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_sel_s_16x16.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_sel_sh_32x32.png b/files/icons/examples/QtCreator/qtcreator_shell_sel_sh_32x32.png
new file mode 100644
index 0000000..7ac0a66
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_sel_sh_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_sel_xl_48x48.png b/files/icons/examples/QtCreator/qtcreator_shell_sel_xl_48x48.png
new file mode 100644
index 0000000..4196517
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_sel_xl_48x48.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_sh_32x32.png b/files/icons/examples/QtCreator/qtcreator_shell_sh_32x32.png
new file mode 100644
index 0000000..5eb9248
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_sh_32x32.png differ
diff --git a/files/icons/examples/QtCreator/qtcreator_shell_xl_48x48.png b/files/icons/examples/QtCreator/qtcreator_shell_xl_48x48.png
new file mode 100644
index 0000000..4196517
Binary files /dev/null and b/files/icons/examples/QtCreator/qtcreator_shell_xl_48x48.png differ
diff --git a/files/icons/examples/firefox/firefox000_16x16.png b/files/icons/examples/firefox/firefox000_16x16.png
new file mode 100644
index 0000000..bd05655
Binary files /dev/null and b/files/icons/examples/firefox/firefox000_16x16.png differ
diff --git a/files/icons/examples/firefox/firefox000_32x32.png b/files/icons/examples/firefox/firefox000_32x32.png
new file mode 100644
index 0000000..f895145
Binary files /dev/null and b/files/icons/examples/firefox/firefox000_32x32.png differ
diff --git a/files/icons/examples/firefox/firefox001_16x16.png b/files/icons/examples/firefox/firefox001_16x16.png
new file mode 100644
index 0000000..3fc3bd9
Binary files /dev/null and b/files/icons/examples/firefox/firefox001_16x16.png differ
diff --git a/files/icons/examples/firefox/firefox001_32x32.png b/files/icons/examples/firefox/firefox001_32x32.png
new file mode 100644
index 0000000..ca4ca3b
Binary files /dev/null and b/files/icons/examples/firefox/firefox001_32x32.png differ
diff --git a/files/icons/examples/firefox/firefox002_16x16.png b/files/icons/examples/firefox/firefox002_16x16.png
new file mode 100644
index 0000000..78001eb
Binary files /dev/null and b/files/icons/examples/firefox/firefox002_16x16.png differ
diff --git a/files/icons/examples/firefox/firefox002_32x32.png b/files/icons/examples/firefox/firefox002_32x32.png
new file mode 100644
index 0000000..68c3a11
Binary files /dev/null and b/files/icons/examples/firefox/firefox002_32x32.png differ
diff --git a/files/icons/examples/firefox/firefox003_16x16.png b/files/icons/examples/firefox/firefox003_16x16.png
new file mode 100644
index 0000000..11e7fc2
Binary files /dev/null and b/files/icons/examples/firefox/firefox003_16x16.png differ
diff --git a/files/icons/examples/firefox/firefox003_32x32.png b/files/icons/examples/firefox/firefox003_32x32.png
new file mode 100644
index 0000000..5e284b9
Binary files /dev/null and b/files/icons/examples/firefox/firefox003_32x32.png differ
diff --git a/files/icons/examples/firefox/firefox004_16x16.png b/files/icons/examples/firefox/firefox004_16x16.png
new file mode 100644
index 0000000..dcd7286
Binary files /dev/null and b/files/icons/examples/firefox/firefox004_16x16.png differ
diff --git a/files/icons/examples/firefox/firefox004_32x32.png b/files/icons/examples/firefox/firefox004_32x32.png
new file mode 100644
index 0000000..8977ebc
Binary files /dev/null and b/files/icons/examples/firefox/firefox004_32x32.png differ
diff --git a/files/icons/examples/firefox/firefox005_16x16.png b/files/icons/examples/firefox/firefox005_16x16.png
new file mode 100644
index 0000000..bd05655
Binary files /dev/null and b/files/icons/examples/firefox/firefox005_16x16.png differ
diff --git a/files/icons/examples/firefox/firefox005_32x32.png b/files/icons/examples/firefox/firefox005_32x32.png
new file mode 100644
index 0000000..f895145
Binary files /dev/null and b/files/icons/examples/firefox/firefox005_32x32.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_jumbo_48x48.png b/files/icons/examples/firefox/firefox_shell_jumbo_48x48.png
new file mode 100644
index 0000000..85ab6d0
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_jumbo_48x48.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_l_32x32.png b/files/icons/examples/firefox/firefox_shell_l_32x32.png
new file mode 100644
index 0000000..f5def28
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_l_32x32.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_open_jumbo_48x48.png b/files/icons/examples/firefox/firefox_shell_open_jumbo_48x48.png
new file mode 100644
index 0000000..85ab6d0
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_open_jumbo_48x48.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_open_l_32x32.png b/files/icons/examples/firefox/firefox_shell_open_l_32x32.png
new file mode 100644
index 0000000..f5def28
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_open_l_32x32.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_open_s_16x16.png b/files/icons/examples/firefox/firefox_shell_open_s_16x16.png
new file mode 100644
index 0000000..c8db915
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_open_s_16x16.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_open_sh_32x32.png b/files/icons/examples/firefox/firefox_shell_open_sh_32x32.png
new file mode 100644
index 0000000..f5def28
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_open_sh_32x32.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_open_xl_48x48.png b/files/icons/examples/firefox/firefox_shell_open_xl_48x48.png
new file mode 100644
index 0000000..a09aeee
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_open_xl_48x48.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_s_16x16.png b/files/icons/examples/firefox/firefox_shell_s_16x16.png
new file mode 100644
index 0000000..c8db915
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_s_16x16.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_sel_jumbo_48x48.png b/files/icons/examples/firefox/firefox_shell_sel_jumbo_48x48.png
new file mode 100644
index 0000000..85ab6d0
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_sel_jumbo_48x48.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_sel_l_32x32.png b/files/icons/examples/firefox/firefox_shell_sel_l_32x32.png
new file mode 100644
index 0000000..30086a3
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_sel_l_32x32.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_sel_s_16x16.png b/files/icons/examples/firefox/firefox_shell_sel_s_16x16.png
new file mode 100644
index 0000000..220e27a
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_sel_s_16x16.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_sel_sh_32x32.png b/files/icons/examples/firefox/firefox_shell_sel_sh_32x32.png
new file mode 100644
index 0000000..596e30d
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_sel_sh_32x32.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_sel_xl_48x48.png b/files/icons/examples/firefox/firefox_shell_sel_xl_48x48.png
new file mode 100644
index 0000000..a09aeee
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_sel_xl_48x48.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_sh_32x32.png b/files/icons/examples/firefox/firefox_shell_sh_32x32.png
new file mode 100644
index 0000000..f5def28
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_sh_32x32.png differ
diff --git a/files/icons/examples/firefox/firefox_shell_xl_48x48.png b/files/icons/examples/firefox/firefox_shell_xl_48x48.png
new file mode 100644
index 0000000..a09aeee
Binary files /dev/null and b/files/icons/examples/firefox/firefox_shell_xl_48x48.png differ
diff --git a/files/icons/icon.ico b/files/icons/icon.ico
new file mode 100644
index 0000000..5c6faf5
Binary files /dev/null and b/files/icons/icon.ico differ
diff --git a/files/icons/icon.svg b/files/icons/icon.svg
new file mode 100644
index 0000000..a12ef21
--- /dev/null
+++ b/files/icons/icon.svg
@@ -0,0 +1,108 @@
+
+
+
+
diff --git a/files/icons/icon.xcf b/files/icons/icon.xcf
new file mode 100644
index 0000000..e18b047
Binary files /dev/null and b/files/icons/icon.xcf differ
diff --git a/files/icons/icon16x16.png b/files/icons/icon16x16.png
new file mode 100644
index 0000000..d19e5c1
Binary files /dev/null and b/files/icons/icon16x16.png differ
diff --git a/files/icons/icon16x16.xcf b/files/icons/icon16x16.xcf
new file mode 100644
index 0000000..964b36c
Binary files /dev/null and b/files/icons/icon16x16.xcf differ
diff --git a/files/icons/icon32x32.png b/files/icons/icon32x32.png
new file mode 100644
index 0000000..fc61bf7
Binary files /dev/null and b/files/icons/icon32x32.png differ
diff --git a/files/icons/icon32x32.xcf b/files/icons/icon32x32.xcf
new file mode 100644
index 0000000..b1367d8
Binary files /dev/null and b/files/icons/icon32x32.xcf differ
diff --git a/files/icons/icon48x48.png b/files/icons/icon48x48.png
new file mode 100644
index 0000000..471f516
Binary files /dev/null and b/files/icons/icon48x48.png differ
diff --git a/files/icons/icon48x48.xcf b/files/icons/icon48x48.xcf
new file mode 100644
index 0000000..23a9426
Binary files /dev/null and b/files/icons/icon48x48.xcf differ
diff --git a/files/icons/icon64x64.png b/files/icons/icon64x64.png
new file mode 100644
index 0000000..a7ff759
Binary files /dev/null and b/files/icons/icon64x64.png differ
diff --git a/files/icons/icon64x64.xcf b/files/icons/icon64x64.xcf
new file mode 100644
index 0000000..ada6704
Binary files /dev/null and b/files/icons/icon64x64.xcf differ
diff --git a/files/icons/icona_stilizzata128x128.xcf b/files/icons/icona_stilizzata128x128.xcf
new file mode 100644
index 0000000..9d71ee1
Binary files /dev/null and b/files/icons/icona_stilizzata128x128.xcf differ
diff --git a/files/icons/icona_stilizzata16x16.xcf b/files/icons/icona_stilizzata16x16.xcf
new file mode 100644
index 0000000..fdadc78
Binary files /dev/null and b/files/icons/icona_stilizzata16x16.xcf differ
diff --git a/files/icons/icona_stilizzata256x256.ico b/files/icons/icona_stilizzata256x256.ico
new file mode 100644
index 0000000..9429fd6
Binary files /dev/null and b/files/icons/icona_stilizzata256x256.ico differ
diff --git a/files/icons/icona_stilizzata256x256.png b/files/icons/icona_stilizzata256x256.png
new file mode 100644
index 0000000..baffb0b
Binary files /dev/null and b/files/icons/icona_stilizzata256x256.png differ
diff --git a/files/icons/icona_stilizzata256x256.xcf b/files/icons/icona_stilizzata256x256.xcf
new file mode 100644
index 0000000..ef58adf
Binary files /dev/null and b/files/icons/icona_stilizzata256x256.xcf differ
diff --git a/files/icons/icona_stilizzata32x32.xcf b/files/icons/icona_stilizzata32x32.xcf
new file mode 100644
index 0000000..354cda2
Binary files /dev/null and b/files/icons/icona_stilizzata32x32.xcf differ
diff --git a/files/icons/icona_stilizzata64x64.xcf b/files/icons/icona_stilizzata64x64.xcf
new file mode 100644
index 0000000..9aaa46b
Binary files /dev/null and b/files/icons/icona_stilizzata64x64.xcf differ
diff --git a/files/icons/icona_stilizzata96x96.xcf b/files/icons/icona_stilizzata96x96.xcf
new file mode 100644
index 0000000..0d7ac28
Binary files /dev/null and b/files/icons/icona_stilizzata96x96.xcf differ
diff --git a/files/icons/lightning/disegno_originale.svg b/files/icons/lightning/disegno_originale.svg
new file mode 100644
index 0000000..769738c
--- /dev/null
+++ b/files/icons/lightning/disegno_originale.svg
@@ -0,0 +1,65 @@
+
+
+
+
diff --git a/files/icons/lightning/lightning.svg b/files/icons/lightning/lightning.svg
new file mode 100644
index 0000000..42fc690
--- /dev/null
+++ b/files/icons/lightning/lightning.svg
@@ -0,0 +1,35 @@
+
+
diff --git a/files/icons/train.png b/files/icons/train.png
new file mode 100644
index 0000000..381acce
Binary files /dev/null and b/files/icons/train.png differ
diff --git a/files/icons/train/README.txt b/files/icons/train/README.txt
new file mode 100644
index 0000000..9cf36c0
--- /dev/null
+++ b/files/icons/train/README.txt
@@ -0,0 +1,12 @@
+Icon source: https://www.flaticon.com/free-icon/train_2855711
+See: https://support.flaticon.com/hc/en-us/articles/207248209-How-I-must-insert-the-attribution-
+
+ATTRIBUTION to 'Smashicons'
+
+Apps/games:
+
+Place the attribution on the app's credits page and on the description page on the app store.
+
+Websites:
+
+Insert the attribution on the page where the icon is shown. This can be placed next to the image or on the footer of the website.
\ No newline at end of file
diff --git a/files/icons/train/train.svg b/files/icons/train/train.svg
new file mode 100644
index 0000000..a9c2a88
--- /dev/null
+++ b/files/icons/train/train.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/files/icons/train/train_128.png b/files/icons/train/train_128.png
new file mode 100644
index 0000000..2c8d0f0
Binary files /dev/null and b/files/icons/train/train_128.png differ
diff --git a/files/icons/train/train_16.png b/files/icons/train/train_16.png
new file mode 100644
index 0000000..d4bc68b
Binary files /dev/null and b/files/icons/train/train_16.png differ
diff --git a/files/icons/train/train_24.png b/files/icons/train/train_24.png
new file mode 100644
index 0000000..5e413cc
Binary files /dev/null and b/files/icons/train/train_24.png differ
diff --git a/files/icons/train/train_256.png b/files/icons/train/train_256.png
new file mode 100644
index 0000000..7f0f4e8
Binary files /dev/null and b/files/icons/train/train_256.png differ
diff --git a/files/icons/train/train_32.png b/files/icons/train/train_32.png
new file mode 100644
index 0000000..2eb5511
Binary files /dev/null and b/files/icons/train/train_32.png differ
diff --git a/files/icons/train/train_512.png b/files/icons/train/train_512.png
new file mode 100644
index 0000000..3b4ecfc
Binary files /dev/null and b/files/icons/train/train_512.png differ
diff --git a/files/icons/train/train_64.png b/files/icons/train/train_64.png
new file mode 100644
index 0000000..0e647a6
Binary files /dev/null and b/files/icons/train/train_64.png differ
diff --git a/files/odt_format/OD_Essentials.pdf b/files/odt_format/OD_Essentials.pdf
new file mode 100644
index 0000000..263c2de
Binary files /dev/null and b/files/odt_format/OD_Essentials.pdf differ
diff --git a/files/odt_format/OpenDocument-v1.2-part1.pdf b/files/odt_format/OpenDocument-v1.2-part1.pdf
new file mode 100644
index 0000000..7721966
Binary files /dev/null and b/files/odt_format/OpenDocument-v1.2-part1.pdf differ
diff --git a/files/rolling_stock/CARRI SOCI AFS FREMO.ods b/files/rolling_stock/CARRI SOCI AFS FREMO.ods
new file mode 100644
index 0000000..fc6d55c
Binary files /dev/null and b/files/rolling_stock/CARRI SOCI AFS FREMO.ods differ
diff --git a/files/rolling_stock/CARRI SOCI AFS FREMO_ROTTO.ods b/files/rolling_stock/CARRI SOCI AFS FREMO_ROTTO.ods
new file mode 100644
index 0000000..32f58db
Binary files /dev/null and b/files/rolling_stock/CARRI SOCI AFS FREMO_ROTTO.ods differ
diff --git a/files/rolling_stock/CARRI SOCI AFS.ods b/files/rolling_stock/CARRI SOCI AFS.ods
new file mode 100644
index 0000000..c0c7088
Binary files /dev/null and b/files/rolling_stock/CARRI SOCI AFS.ods differ
diff --git a/files/rolling_stock/Duplicati.ods b/files/rolling_stock/Duplicati.ods
new file mode 100644
index 0000000..6457de2
Binary files /dev/null and b/files/rolling_stock/Duplicati.ods differ
diff --git a/files/rolling_stock/MOLTI_CARRI.ods b/files/rolling_stock/MOLTI_CARRI.ods
new file mode 100644
index 0000000..4083d40
Binary files /dev/null and b/files/rolling_stock/MOLTI_CARRI.ods differ
diff --git a/files/shifts/Prefix_La_Serenissima_A.ott b/files/shifts/Prefix_La_Serenissima_A.ott
new file mode 100644
index 0000000..26a1401
Binary files /dev/null and b/files/shifts/Prefix_La_Serenissima_A.ott differ
diff --git a/files/shifts/esempio_foglio_turno.odt b/files/shifts/esempio_foglio_turno.odt
new file mode 100644
index 0000000..9587cde
Binary files /dev/null and b/files/shifts/esempio_foglio_turno.odt differ
diff --git a/files/shifts/esempio_grafico_turno.odg b/files/shifts/esempio_grafico_turno.odg
new file mode 100644
index 0000000..7be1c67
Binary files /dev/null and b/files/shifts/esempio_grafico_turno.odg differ
diff --git a/files/stations/CASTEL_DI_PIAVE_orario_stazione.odt b/files/stations/CASTEL_DI_PIAVE_orario_stazione.odt
new file mode 100644
index 0000000..214eace
Binary files /dev/null and b/files/stations/CASTEL_DI_PIAVE_orario_stazione.odt differ
diff --git a/files/stations/CORNUDA.odt b/files/stations/CORNUDA.odt
new file mode 100644
index 0000000..c9184d3
Binary files /dev/null and b/files/stations/CORNUDA.odt differ
diff --git a/packaging/CMakeLists.txt b/packaging/CMakeLists.txt
new file mode 100644
index 0000000..e69de29
diff --git a/packaging/windows/NSIS/constants.nsh.in b/packaging/windows/NSIS/constants.nsh.in
new file mode 100644
index 0000000..46c6923
--- /dev/null
+++ b/packaging/windows/NSIS/constants.nsh.in
@@ -0,0 +1,28 @@
+;Definitions
+
+!define APP_NAME "@APP_DISPLAY_NAME@"
+!define APP_PRODUCT "@TRAINTIMETABLE_TARGET@"
+!define COMPANY_NAME "@APP_COMPANY_NAME@"
+!define DESCRIPTION "@PROJECT_DESCRIPTION@"
+
+# These three must be integers
+!define VERSIONMAJOR @PROJECT_VERSION_MAJOR@
+!define VERSIONMINOR @PROJECT_VERSION_MINOR@
+!define VERSIONBUILD @PROJECT_VERSION_PATCH@
+
+# These will be displayed by the "Click here for support information" link in "Add/Remove Programs"
+# It is possible to use "mailto:" links in here to open the email client
+!define HELPURL "@APP_HELP_URL@" # "Support Information" link
+!define UPDATEURL "@APP_UPDATE_URL@" # "Product Updates" link
+!define ABOUTURL "@PROJECT_HOMEPAGE_URL@" # "Publisher" link
+
+# This is the size (in kB) of all the files copied into "Program Files"
+!define INSTALLSIZE 8000
+
+!define TRAIN_TIMETABLE_PATH "@CMAKE_BINARY_DIR@\debug\"
+!define TRAIN_TIMETABLE_EXTRA "@CMAKE_SOURCE_DIR@\files"
+!define TRAIN_TIMETABLE_EXE "@TRAINTIMETABLE_TARGET@.exe"
+!define TRAIN_TIMETABLE_SETTINGS "traintimetable_settings.ini"
+
+!define TRAIN_TIMETABLE_LICENSE "${NSISDIR}\Docs\Modern UI\License.txt" ;TODO
+!define TRAIN_TIMETABLE_README "${NSISDIR}\Docs\Modern UI\License.txt" ;TODO
diff --git a/packaging/windows/NSIS/installer.nsi b/packaging/windows/NSIS/installer.nsi
new file mode 100644
index 0000000..0b8d2b2
--- /dev/null
+++ b/packaging/windows/NSIS/installer.nsi
@@ -0,0 +1,389 @@
+; Setup TrainTimetable and register variables for .ttt file associations
+
+;--------------------------------
+;Include Modern UI and FileFunc and LogicLib
+
+ !include "MUI2.nsh"
+
+
+!include "FileFunc.nsh"
+!insertmacro RefreshShellIcons
+!insertmacro un.RefreshShellIcons
+
+!include LogicLib.nsh
+
+;--------------------------------
+;Definitions
+
+!include "constants.nsh"
+
+;--------------------------------
+;General
+
+;Name and file
+Name "${COMPANY_NAME} - ${APP_NAME}"
+OutFile "${APP_PRODUCT}-${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}-setup.exe"
+Unicode True
+SetCompressor /SOLID /FINAL lzma
+
+;Default installation folder
+InstallDir "$PROGRAMFILES64\${COMPANY_NAME}\${APP_NAME}" ; x86_64 64-bit
+
+;Get installation folder from registry if available
+InstallDirRegKey HKCU "Software\${COMPANY_NAME} ${APP_NAME}" ""
+
+;Request application privileges for Windows Vista
+RequestExecutionLevel admin ;Require admin rights on NT6+ (When UAC is turned on)
+
+; Prevent blurry text on High DPI screen due to Windows stretching the window
+ManifestDPIAware True
+
+;--------------------------------
+;Interface Settings
+
+!define MUI_ABORTWARNING
+
+!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\orange-install.ico"
+!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\orange-uninstall.ico"
+
+;--------------------------------
+;Pages
+
+#!define MUI_WELCOMEPAGE_TITLE "$(welcome_title)"
+#!define MUI_WELCOMEPAGE_TEXT "$(welcome_text)"
+
+!insertmacro MUI_PAGE_WELCOME
+!insertmacro MUI_PAGE_LICENSE ${TRAIN_TIMETABLE_LICENSE}
+!insertmacro MUI_PAGE_COMPONENTS
+!insertmacro MUI_PAGE_DIRECTORY
+!insertmacro MUI_PAGE_INSTFILES
+
+!define MUI_FINISHPAGE_LINK "$(visit_site)"
+!define MUI_FINISHPAGE_LINK_LOCATION ${ABOUTURL}
+
+!define MUI_FINISHPAGE_RUN "$INSTDIR\${TRAIN_TIMETABLE_EXE}"
+!define MUI_FINISHPAGE_NOREBOOTSUPPORT
+
+!define MUI_FINISHPAGE_SHOWREADME ${TRAIN_TIMETABLE_README}
+!define MUI_FINISHPAGE_SHOWREADME_TEXT "$(show_readme_label)"
+
+!insertmacro MUI_PAGE_FINISH
+
+;Uninstaller Pages
+!insertmacro MUI_UNPAGE_CONFIRM
+!insertmacro MUI_UNPAGE_INSTFILES
+
+;--------------------------------
+;Languages
+
+ !insertmacro MUI_LANGUAGE "English" ; The first language is the default language
+ !insertmacro MUI_LANGUAGE "Italian"
+
+;--------------------------------
+;Localized messages
+
+LangString welcome_title ${LANG_ENGLISH} "Welcome to the ${APP_NAME} ${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD} Setup Wizard"
+LangString welcome_title ${LANG_ITALIAN} "Benvenuto nel Wizard di installazione di ${APP_NAME} ${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}"
+
+LangString welcome_text ${LANG_ENGLISH} "This wizard will guide you through the installation of ${APP_NAME}"
+LangString welcome_text ${LANG_ITALIAN} "Questa procedura ti guiderà nell'installazione di ${APP_NAME}"
+
+LangString visit_site ${LANG_ENGLISH} "Visit the ${APP_NAME} site for the latest news, FAQs and support"
+LangString visit_site ${LANG_ITALIAN} "Visita il sito di ${APP_NAME} per le ultime notizie, domande frequanti e ricevere supporto"
+
+LangString show_readme_label ${LANG_ENGLISH} "Show release notes"
+LangString show_readme_label ${LANG_ITALIAN} "Mostra note di rilascio"
+
+LangString DESC_MainProgram ${LANG_ENGLISH} "Main application and settings files"
+LangString DESC_MainProgram ${LANG_ITALIAN} "Applicazione principale e file di configurazione"
+
+LangString DESC_SM_Shortcut ${LANG_ENGLISH} "Create shortcuts to Start Menu. This makes easier to start Train Timetable"
+LangString DESC_SM_Shortcut ${LANG_ITALIAN} "Crea collegamenti al Menu Start. Questo rende pi${U+00FA} facile l'avvio dell'applicazione" #${U+00FA} = ù (U accentata minuscola)
+
+LangString DESC_FileAss ${LANG_ENGLISH} "Setup file associations to display an icon for Train Timetable Session files and be able to open the application by just double clicking on the file"
+LangString DESC_FileAss ${LANG_ITALIAN} "Configura le associazioni dei file per mostrare un'icona sui file Train Timetable Session e per aprire l'applicazione semplicemente facendo doppio click sul file"
+
+LangString keep_logs_message ${LANG_ENGLISH} "Keep logs files? This is useful if you need to send them"
+LangString keep_logs_message ${LANG_ITALIAN} "Mantenere i file di log? E' utile se si intende inviarli"
+
+LangString unist_previous_msg ${LANG_ENGLISH} "Uninstall previous version?"
+LangString unist_previous_msg ${LANG_ITALIAN} "Disinstallare la versione precedente?"
+
+LangString unist_failed_msg ${LANG_ENGLISH} "Failed to uninstall, continue anyway?"
+LangString unist_failed_msg ${LANG_ITALIAN} "Impossibile disinstallare la versione precedente, procedere comunque?"
+
+;--------------------------------
+;Version info
+
+VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductName" "${APP_NAME}"
+VIAddVersionKey /LANG=${LANG_ENGLISH} "Comments" "Visit ${ABOUTURL}"
+VIAddVersionKey /LANG=${LANG_ENGLISH} "CompanyName" "${COMPANY_NAME}"
+VIAddVersionKey /LANG=${LANG_ENGLISH} "LegalTrademarks" "${APP_NAME} is a trademark of ${COMPANY_NAME}"
+VIAddVersionKey /LANG=${LANG_ENGLISH} "LegalCopyright" "© ${COMPANY_NAME}"
+VIAddVersionKey /LANG=${LANG_ENGLISH} "FileDescription" "${APP_NAME} Installer"
+VIAddVersionKey /LANG=${LANG_ENGLISH} "FileVersion" "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}"
+
+VIAddVersionKey /LANG=${LANG_ITALIAN} "ProductName" "${APP_NAME}"
+VIAddVersionKey /LANG=${LANG_ITALIAN} "Comments" "Visita ${ABOUTURL}"
+VIAddVersionKey /LANG=${LANG_ITALIAN} "CompanyName" "${COMPANY_NAME}"
+VIAddVersionKey /LANG=${LANG_ITALIAN} "LegalTrademarks" "${APP_NAME} è un marchio di ${COMPANY_NAME}"
+VIAddVersionKey /LANG=${LANG_ITALIAN} "LegalCopyright" "© ${COMPANY_NAME}"
+VIAddVersionKey /LANG=${LANG_ITALIAN} "FileDescription" "${APP_NAME} Installer"
+VIAddVersionKey /LANG=${LANG_ITALIAN} "FileVersion" "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}"
+
+VIFileVersion ${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}.0
+VIProductVersion "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}.0"
+
+;--------------------------------
+;Reserve Files
+
+ ;If you are using solid compression, files that are required before
+ ;the actual installation should be stored first in the data block,
+ ;because this will make your installer start faster.
+
+ !insertmacro MUI_RESERVEFILE_LANGDLL
+
+;--------------------------------
+;Installer Sections
+
+Section "Application" main_program
+ SetShellVarContext current
+
+ SetOutPath $INSTDIR
+
+ File ${TRAIN_TIMETABLE_PATH}\${TRAIN_TIMETABLE_EXE}
+ File ${TRAIN_TIMETABLE_EXTRA}\icons\icon.ico
+
+ File ${TRAIN_TIMETABLE_PATH}\*.dll
+
+ SetOutPath $INSTDIR\platforms
+ File ${TRAIN_TIMETABLE_PATH}\platforms\*.dll
+
+ SetOutPath $INSTDIR\printsupport
+ File ${TRAIN_TIMETABLE_PATH}\printsupport\*.dll
+
+ SetOutPath $INSTDIR\styles
+ File ${TRAIN_TIMETABLE_PATH}\styles\*.dll
+
+ SetOutPath $INSTDIR\imageformats
+ File ${TRAIN_TIMETABLE_PATH}\imageformats\*.dll
+
+ SetOutPath $INSTDIR\icons
+ File ${TRAIN_TIMETABLE_EXTRA}\icons\lightning\lightning.svg
+
+ SetOutPath $INSTDIR\translations
+ File ${TRAIN_TIMETABLE_PATH}\translations\*.qm
+
+ SetOutPath "$LOCALAPPDATA\${COMPANY_NAME}\${APP_NAME}"
+ ; Create empty settings file
+ FileOpen $0 $OUTDIR\${TRAIN_TIMETABLE_SETTINGS} w
+ FileClose $0
+
+ ; Set application language to the language chosen int the installer (default English)
+ ; Convert LANG_ID to local language code
+ StrCpy $0 "en_US"
+ ${Switch} $LANGUAGE
+ ${Case} ${LANG_ITALIAN}
+ StrCpy $0 "it_IT"
+ ${Break}
+ ${Default}
+ ${Break}
+ ${EndSwitch}
+ WriteINIStr $OUTDIR\${TRAIN_TIMETABLE_SETTINGS} "General" "language" $0
+
+ # Uninstaller - See function un.onInit and section "uninstall" for configuration
+ WriteUninstaller "$INSTDIR\uninstall.exe"
+
+ # Registry information for add/remove programs
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "DisplayName" "${COMPANY_NAME} - ${APP_NAME} - ${DESCRIPTION}"
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "InstallLocation" "$\"$INSTDIR$\""
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "DisplayIcon" "$\"$INSTDIR\icon.ico$\""
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "Publisher" "$\"${COMPANY_NAME}$\""
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "HelpLink" "$\"${HELPURL}$\""
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "URLUpdateInfo" "$\"${UPDATEURL}$\""
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "URLInfoAbout" "$\"${ABOUTURL}$\""
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "DisplayVersion" "$\"${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}$\""
+ WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "VersionMajor" ${VERSIONMAJOR}
+ WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "VersionMinor" ${VERSIONMINOR}
+ # There is no option for modifying or repairing the install
+ WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "NoModify" 1
+ WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "NoRepair" 1
+ # Set the INSTALLSIZE constant (!defined at the top of this script) so Add/Remove Programs can accurately report the size
+ WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "EstimatedSize" ${INSTALLSIZE}
+
+ WriteRegStr HKCU "Software\${COMPANY_NAME} ${APP_NAME}" "" $INSTDIR
+ WriteRegDWORD HKCU "Software\${COMPANY_NAME} ${APP_NAME}" "VersionMajor" "${VERSIONMAJOR}"
+ WriteRegDWORD HKCU "Software\${COMPANY_NAME} ${APP_NAME}" "VersionMinor" "${VERSIONMINOR}"
+ WriteRegDWORD HKCU "Software\${COMPANY_NAME} ${APP_NAME}" "VersionRevision" "77"
+ WriteRegDWORD HKCU "Software\${COMPANY_NAME} ${APP_NAME}" "VersionBuild" "${VERSIONBUILD}"
+
+SectionEnd
+
+; Open a section to create shortcuts to start menu
+Section "Start Menu Shortcuts" sm_shorcuts
+ # Start Menu
+ SetShellVarContext current
+ CreateDirectory "$SMPROGRAMS\${COMPANY_NAME}"
+ CreateShortCut "$SMPROGRAMS\${COMPANY_NAME}\${APP_NAME}.lnk" "$INSTDIR\${TRAIN_TIMETABLE_EXE}" "" "$INSTDIR\icon.ico"
+SectionEnd
+
+; Open a section to register file type
+Section "File associations" file_ass
+ SetShellVarContext current
+ SetOutPath $INSTDIR
+
+ WriteRegStr HKCU "Software\Classes\.ttt" "" "Train_Timetable.session"
+ WriteRegStr HKCU "Software\Classes\.ttt" "PerceivedType" "document"
+ WriteRegStr HKCU "Software\Classes\Train_Timetable.session" "" "Train Timetable Session File"
+ WriteRegStr HKCU "Software\Classes\Train_Timetable.session\DefaultIcon" "" "$INSTDIR\${TRAIN_TIMETABLE_EXE},0"
+ WriteRegStr HKCU "Software\Classes\Train_Timetable.session\shell\open\command" "" '$INSTDIR\${TRAIN_TIMETABLE_EXE} "%1"'
+
+ DetailPrint $INSTDIR
+
+ ;Refresh icon cache
+ ${RefreshShellIcons}
+SectionEnd
+
+;--------------------------------
+;Installer Functions
+
+!macro UninstallExisting exitcode uninstcommand
+Push `${uninstcommand}`
+Call UninstallExisting
+Pop ${exitcode}
+!macroend
+Function UninstallExisting
+Exch $1 ; uninstcommand
+Push $2 ; Uninstaller
+Push $3 ; Len
+StrCpy $3 ""
+StrCpy $2 $1 1
+StrCmp $2 '"' qloop sloop
+sloop:
+ StrCpy $2 $1 1 $3
+ IntOp $3 $3 + 1
+ StrCmp $2 "" +2
+ StrCmp $2 ' ' 0 sloop
+ IntOp $3 $3 - 1
+ Goto run
+qloop:
+ StrCmp $3 "" 0 +2
+ StrCpy $1 $1 "" 1 ; Remove initial quote
+ IntOp $3 $3 + 1
+ StrCpy $2 $1 1 $3
+ StrCmp $2 "" +2
+ StrCmp $2 '"' 0 qloop
+run:
+ StrCpy $2 $1 $3 ; Path to uninstaller
+ StrCpy $1 161 ; ERROR_BAD_PATHNAME
+ GetFullPathName $3 "$2\.." ; $InstDir
+ IfFileExists "$2" 0 +4
+ ExecWait '"$2" /S _?=$3' $1 ; This assumes the existing uninstaller is a NSIS uninstaller, other uninstallers don't support /S nor _?=
+ IntCmp $1 0 "" +2 +2 ; Don't delete the installer if it was aborted
+ Delete "$2" ; Delete the uninstaller
+ RMDir "$3" ; Try to delete $InstDir
+ RMDir "$3\.." ; (Optional) Try to delete the parent of $InstDir
+Pop $3
+Pop $2
+Exch $1 ; exitcode
+FunctionEnd
+
+Function .onInit
+
+ !insertmacro MUI_LANGDLL_DISPLAY
+
+ ReadRegStr $0 HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}" "UninstallString"
+ ${If} $0 != ""
+ ${AndIf} ${Cmd} `MessageBox MB_YESNO|MB_ICONQUESTION "$(unist_previous_msg)" /SD IDYES IDYES`
+ !insertmacro UninstallExisting $0 $0
+ ${If} $0 <> 0
+ MessageBox MB_YESNO|MB_ICONSTOP "$(unist_failed_msg)" /SD IDYES IDYES +2
+ Abort
+ ${EndIf}
+ ${EndIf}
+FunctionEnd
+
+;--------------------------------
+;Descriptions
+
+ ;Assign language strings to sections
+ !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
+ !insertmacro MUI_DESCRIPTION_TEXT ${main_program} $(DESC_MainProgram)
+ !insertmacro MUI_DESCRIPTION_TEXT ${sm_shorcuts} $(DESC_SM_Shortcut)
+ !insertmacro MUI_DESCRIPTION_TEXT ${file_ass} $(DESC_FileAss)
+ !insertmacro MUI_FUNCTION_DESCRIPTION_END
+
+;--------------------------------
+;Uninstaller Section
+
+Section un.main_program
+
+ # Remove Start Menu launcher
+ Delete "$SMPROGRAMS\${COMPANY_NAME}\${APP_NAME}.lnk"
+ # Try to remove the Start Menu folder - this will only happen if it is empty
+ RMDir "$SMPROGRAMS\${COMPANY_NAME}"
+
+ # Remove files
+ Delete $INSTDIR\${TRAIN_TIMETABLE_EXE}
+ Delete $INSTDIR\icon.ico
+
+ Delete $INSTDIR\icons\lightning.svg
+ RMDir $INSTDIR\icons
+
+ Delete $INSTDIR\translations\*.qm
+ RMDir $INSTDIR\translations
+
+ # Ask user if they want to delete or keep log files. If they choose to keep them AppData folder is not removed
+ MessageBox MB_YESNO "$(keep_logs_message)" IDYES delete_settings
+ RMDir /r "$LOCALAPPDATA\${COMPANY_NAME}\${APP_NAME}\logs"
+
+delete_settings:
+ Delete "$LOCALAPPDATA\${COMPANY_NAME}\${APP_NAME}\${TRAIN_TIMETABLE_SETTINGS}"
+ RMDir "$LOCALAPPDATA\${COMPANY_NAME}\${APP_NAME}"
+ RMDir "$LOCALAPPDATA\${COMPANY_NAME}"
+
+ Delete $INSTDIR\*.dll
+
+ RMDir /r $INSTDIR\logs
+
+ Delete $INSTDIR\platforms\*.dll
+ RMDir /r $INSTDIR\platforms
+
+ Delete $INSTDIR\printsupport\*.dll
+ RMDir /r $INSTDIR\printsupport
+
+ Delete $INSTDIR\styles\*.dll
+ RMDir /r $INSTDIR\styles
+
+ Delete $INSTDIR\imageformats\*.dll
+ RMDir /r $INSTDIR\imageformats
+
+ # Remove uninstaller information from the registry
+ DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANY_NAME} ${APP_NAME}"
+ DeleteRegKey HKCU "Software\${COMPANY_NAME} ${APP_NAME}"
+
+ ; Unregister file associations in uninstall.exe
+ !macro AssocDeleteFileExtAndProgId _hkey _dotext _pid
+ ReadRegStr $R0 ${_hkey} "Software\Classes\${_dotext}" ""
+ StrCmp $R0 "${_pid}" 0 +2
+ DeleteRegKey ${_hkey} "Software\Classes\${_dotext}"
+
+ DeleteRegKey ${_hkey} "Software\Classes\${_pid}"
+ !macroend
+
+ !insertmacro AssocDeleteFileExtAndProgId HKCU ".ttt" "Train_Timetable.session"
+
+ DetailPrint $INSTDIR
+ DetailPrint $OUTDIR
+
+ ;Refresh icon cache
+ ${un.RefreshShellIcons}
+
+ # Always delete uninstaller as the last action
+ Delete $INSTDIR\uninstall.exe
+
+ # Try to remove the install directory - this will only happen if it is empty
+ RMDir $INSTDIR
+SectionEnd
diff --git a/packaging/windows/resources.rc.in b/packaging/windows/resources.rc.in
new file mode 100644
index 0000000..e643d36
--- /dev/null
+++ b/packaging/windows/resources.rc.in
@@ -0,0 +1,37 @@
+#include
+
+IDI_ICON1 ICON DISCARDABLE "${APP_ICON}"
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0
+ PRODUCTVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_DLL
+ FILESUBTYPE 0x0L
+ BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904b0"
+ BEGIN
+ VALUE "CompanyName", "${APP_COMPANY_NAME}\0"
+ VALUE "FileDescription", "${APP_DISPLAY_NAME}\0"
+ VALUE "FileVersion", "${PROJECT_VERSION}\0"
+ VALUE "LegalCopyright", "${APP_COMPANY_NAME}\0"
+ VALUE "OriginalFilename", "${TRAINTIMETABLE_TARGET}.exe\0"
+ VALUE "ProductName", "${APP_PRODUCT_NAME}\0"
+ VALUE "ProductVersion", "${PROJECT_VERSION}\0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x0409, 1200
+ END
+ END
+/* End of Version info */
+
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..6682fee
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,288 @@
+
+#Set Win32 resources
+if (WIN32)
+ configure_file(../packaging/windows/resources.rc.in ${CMAKE_BINARY_DIR}/resources/resources.rc)
+ set(TRAINTIMETABLE_RESOURCES
+ ${TRAINTIMETABLE_RESOURCES}
+ ${CMAKE_BINARY_DIR}/resources/resources.rc
+ )
+endif()
+
+add_subdirectory(app)
+add_subdirectory(backgroundmanager)
+add_subdirectory(db_metadata)
+add_subdirectory(graph)
+add_subdirectory(jobs)
+add_subdirectory(lines)
+add_subdirectory(odt_export)
+add_subdirectory(printing)
+add_subdirectory(rollingstock)
+add_subdirectory(searchbox)
+add_subdirectory(settings)
+add_subdirectory(shifts)
+add_subdirectory(sqlconsole)
+add_subdirectory(sqlite3pp)
+add_subdirectory(stations)
+add_subdirectory(translations)
+add_subdirectory(utils)
+add_subdirectory(viewmanager)
+
+# Set TrainTimetable info template file
+set(TRAINTIMETABLE_SOURCES
+ ${TRAINTIMETABLE_SOURCES}
+ app/info.h.in
+ )
+configure_file(app/info.h.in ${CMAKE_BINARY_DIR}/include/info.h)
+
+# Add executable
+add_executable(${TRAINTIMETABLE_TARGET} WIN32
+ ${TRAINTIMETABLE_SOURCES}
+ ${TRAINTIMETABLE_UI_FILES}
+ ${TRAINTIMETABLE_RESOURCES}
+ )
+
+# Set compiler options
+if(MSVC)
+ target_compile_options(
+ ${TRAINTIMETABLE_TARGET}
+ PRIVATE
+ /WX
+ /wd4267
+ /wd4244
+ /experimental:external
+ /external:anglebrackets
+ /external:W0
+ "$<$:/O2>"
+ "$<$:/MP>"
+ )
+else()
+ target_compile_options(
+ ${TRAINTIMETABLE_TARGET}
+ PRIVATE
+ "$<$:-O2>"
+ #-Werror
+ -Wuninitialized
+ -pedantic-errors
+ -Wall
+ -Wextra
+ -Wno-unused-parameter
+ -Wshadow
+ )
+endif()
+
+if(UNIX AND NOT APPLE)
+ target_link_options(
+ ${TRAINTIMETABLE_TARGET}
+ PRIVATE
+ -rdynamic
+ )
+endif()
+
+# Set include directories
+target_include_directories(
+ ${TRAINTIMETABLE_TARGET}
+ PRIVATE
+ ${SQLite3_INCLUDE_DIRS}
+ ${ZIP_INCLUDE_DIR}
+ ${CMAKE_BINARY_DIR}/include #For template files
+ )
+
+# Set link libraries
+target_link_libraries(
+ ${TRAINTIMETABLE_TARGET}
+ PRIVATE
+ Qt5::Core
+ Qt5::Gui
+ Qt5::Widgets
+ Qt5::Svg
+ Qt5::PrintSupport
+ ${SQLite3_LIBRARIES}
+ ${ZLIB_LIBRARIES}
+ ${ZIP_LIBRARY}
+ )
+
+if (WIN32)
+ target_link_libraries(
+ ${TRAINTIMETABLE_TARGET}
+ PRIVATE
+ DbgHelp
+ )
+endif()
+
+if(UNIX AND NOT APPLE)
+ install(TARGETS ${TRAINTIMETABLE_TARGET} RUNTIME DESTINATION bin)
+endif()
+
+## Enable Crashpad if found
+#if (GoogleCrashpad_FOUND)
+# set(OLIVE_DEFINITIONS ${OLIVE_DEFINITIONS} USE_CRASHPAD)
+
+# target_include_directories(
+# ${OLIVE_TARGET}
+# PRIVATE
+# ${CRASHPAD_INCLUDE_DIRS}
+# )
+
+# target_link_libraries(
+# ${OLIVE_TARGET}
+# PRIVATE
+# ${CRASHPAD_LIBRARIES}
+# )
+
+# set(OLIVE_CRASH_TARGET "olive-crashhandler")
+
+# set(OLIVE_CRASH_SOURCES
+# dialog/crashhandler/crashhandler.h
+# dialog/crashhandler/crashhandler.cpp
+# dialog/crashhandler/crashhandlermain.cpp
+# )
+
+# if (WIN32)
+# add_executable(
+# ${OLIVE_CRASH_TARGET}
+# WIN32
+# ${OLIVE_CRASH_SOURCES}
+# )
+# else()
+# add_executable(
+# ${OLIVE_CRASH_TARGET}
+# ${OLIVE_CRASH_SOURCES}
+# )
+# endif()
+
+# target_include_directories(
+# ${OLIVE_CRASH_TARGET}
+# PRIVATE
+# ${CRASHPAD_INCLUDE_DIRS}
+# )
+
+# target_link_libraries(
+# ${OLIVE_CRASH_TARGET}
+# PRIVATE
+# Qt5::Core
+# Qt5::Gui
+# Qt5::Widgets
+# Qt5::Network
+# ${CRASHPAD_LIBRARIES}
+# )
+
+# set(CRASHPAD_HANDLER "crashpad_handler${CMAKE_EXECUTABLE_SUFFIX}")
+# set(MINIDUMP_STACKWALK "minidump_stackwalk${CMAKE_EXECUTABLE_SUFFIX}")
+
+# if(UNIX AND NOT APPLE)
+# install(TARGETS ${OLIVE_CRASH_TARGET} RUNTIME DESTINATION bin)
+# install(PROGRAMS ${CRASHPAD_LIBRARY_DIRS}/${CRASHPAD_HANDLER} DESTINATION bin)
+# install(PROGRAMS ${BREAKPAD_BIN_DIR}/${MINIDUMP_STACKWALK} DESTINATION bin)
+# endif()
+
+# if(APPLE)
+# # Move crash handler executables inside Mac app bundle
+# add_custom_command(TARGET ${OLIVE_CRASH_TARGET} POST_BUILD
+# COMMAND ${CMAKE_COMMAND} -E copy_if_different ${OLIVE_CRASH_TARGET} $
+# COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CRASHPAD_LIBRARY_DIRS}/${CRASHPAD_HANDLER} $
+# COMMAND ${CMAKE_COMMAND} -E copy_if_different ${BREAKPAD_BIN_DIR}/${MINIDUMP_STACKWALK} $
+# )
+# endif()
+#endif()
+
+# Set compiler definitions
+target_compile_definitions(${TRAINTIMETABLE_TARGET} PRIVATE ${TRAINTIMETABLE_DEFINITIONS})
+
+if(DOXYGEN_FOUND)
+ set(DOXYGEN_PROJECT_NAME ${APP_DISPLAY_NAME})
+ set(DOXYGEN_PROJECT_LOGO ${APP_ICON})
+ set(DOXYGEN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/docs")
+ set(DOXYGEN_EXTRACT_ALL "YES")
+ set(DOXYGEN_EXTRACT_PRIVATE "YES")
+ doxygen_add_docs(docs ALL ${TRAINTIMETABLE_SOURCES})
+endif()
+
+## Update/Release translations ##
+
+#(Run this target before installing installing and every time you update translations)
+add_custom_target(RELEASE_TRANSLATIONS ALL
+ COMMENT "Running translations it_IT...")
+
+if(UPDATE_TS)
+ # Run 'lupdate' to parse C++ and UI files and extract translatable strings
+ if(UPDATE_TS_KEEP_OBSOLETE)
+ add_custom_command(TARGET RELEASE_TRANSLATIONS
+ POST_BUILD
+ COMMAND ${Qt5_LUPDATE_EXECUTABLE} ARGS ${CMAKE_SOURCE_DIR}/src -ts ${TRAINTIMETABLE_TS_FILES}
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src
+ COMMENT "Updating translations"
+ VERBATIM)
+ else()
+ add_custom_command(TARGET RELEASE_TRANSLATIONS
+ POST_BUILD
+ COMMAND ${Qt5_LUPDATE_EXECUTABLE} ARGS ${CMAKE_SOURCE_DIR}/src -ts ${TRAINTIMETABLE_TS_FILES} -no-obsolete
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src
+ COMMENT "Updating translations"
+ VERBATIM)
+ endif()
+
+endif()
+
+# For each .ts file release a .qm file
+foreach(TS_FILE ${TRAINTIMETABLE_TS_FILES})
+ get_filename_component(QM_FILE_NAME ${TS_FILE} NAME_WLE)
+ set(QM_FILE "${CMAKE_BINARY_DIR}/src/translations/${QM_FILE_NAME}.qm")
+ message("Generating translation: ${QM_FILE}")
+ add_custom_command(TARGET RELEASE_TRANSLATIONS
+ POST_BUILD
+ COMMAND ${Qt5_LRELEASE_EXECUTABLE} ARGS ${CMAKE_CURRENT_SOURCE_DIR}/${TS_FILE} -qm ${QM_FILE}
+ COMMENT "Translating ${QM_FILE_NAME}"
+ VERBATIM)
+endforeach()
+
+## Update/Release translations end ##
+
+## Install and Deploy ##
+
+# Copy executable
+install(TARGETS ${TRAINTIMETABLE_TARGET}
+ RUNTIME
+ DESTINATION ${CMAKE_INSTALL_PREFIX})
+
+# Copy SVG icon
+install(FILES ${CMAKE_SOURCE_DIR}/files/icons/lightning/lightning.svg
+ DESTINATION ${CMAKE_INSTALL_PREFIX}/icons)
+
+# For each .ts file install corrensponding .qm file
+foreach(TS_FILE ${TRAINTIMETABLE_TS_FILES})
+ get_filename_component(QM_FILE_NAME ${TS_FILE} NAME_WLE)
+ install(FILES
+ "${CMAKE_BINARY_DIR}/src/translations/${QM_FILE_NAME}.qm"
+ DESTINATION ${CMAKE_INSTALL_PREFIX}/translations OPTIONAL)
+endforeach()
+
+if(WIN32)
+ # Copy SQlite3 DLL
+ install(PROGRAMS ${SQLite3_LIBRARIES} DESTINATION ${CMAKE_INSTALL_PREFIX})
+
+ # Copy ZLib DLL
+ install(PROGRAMS ${ZLIB_LIBRARIES} DESTINATION ${CMAKE_INSTALL_PREFIX})
+
+ # Copy libzip DLL
+ install(PROGRAMS ${ZIP_LIBRARY} DESTINATION ${CMAKE_INSTALL_PREFIX})
+
+ if(RUN_WINDEPLOYQT)
+ if(NOT WINDEPLOYQT_EXE)
+ message(FATAL_ERROR "In order to run windeployqt you must first set th exe path in WINDEPLOYQT_EXE")
+ endif()
+
+ install(CODE "
+ message(STATUS \"Running windeployqt ${WINDEPLOYQT_EXE}\")
+ execute_process(COMMAND ${WINDEPLOYQT_EXE} ${CMAKE_INSTALL_PREFIX}
+ WORKING_DIRECTORY ${CMAKE_INSTALL_PREFIX}
+ OUTPUT_VARIABLE WINDEPLOYQT_EXE_RESULT
+ ERROR_VARIABLE WINDEPLOYQT_EXE_RESULT)
+
+ message(STATUS \${WINDEPLOYQT_EXE_RESULT})
+
+ message(STATUS \"windeployqt Done.\")
+ ")
+ endif()
+endif()
+
+## Install end ##
diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
new file mode 100644
index 0000000..2e8c46f
--- /dev/null
+++ b/src/app/CMakeLists.txt
@@ -0,0 +1,19 @@
+set(TRAINTIMETABLE_SOURCES
+ ${TRAINTIMETABLE_SOURCES}
+ app/main.cpp
+ app/mainwindow.h
+ app/mainwindow.cpp
+ app/propertiesdialog.h
+ app/propertiesdialog.cpp
+ app/scopedebug.h
+ app/scopedebug.cpp
+ app/session.h
+ app/session.cpp
+ PARENT_SCOPE
+)
+
+set(TRAINTIMETABLE_UI_FILES
+ ${TRAINTIMETABLE_UI_FILES}
+ app/mainwindow.ui
+ PARENT_SCOPE
+)
diff --git a/src/app/info.h.in b/src/app/info.h.in
new file mode 100644
index 0000000..6768a2c
--- /dev/null
+++ b/src/app/info.h.in
@@ -0,0 +1,17 @@
+#ifndef INFO_H
+#define INFO_H
+
+#include
+
+static const QString AppVersion = QStringLiteral("${PROJECT_VERSION}");
+static const QString AppCompany = QStringLiteral("${APP_COMPANY_NAME}");
+static const QString AppProduct = QStringLiteral("${APP_PRODUCT_NAME}");
+static const QString AppDisplayName = QStringLiteral("${APP_DISPLAY_NAME}");
+
+static const QString FormatVersionStr = QStringLiteral("${DB_FORMAT_VERSION}");
+
+static const int FormatVersion = ${DB_FORMAT_VERSION};
+
+static const QString AppBuildDate = QStringLiteral(__DATE__);
+
+#endif // INFO_H
diff --git a/src/app/main.cpp b/src/app/main.cpp
new file mode 100644
index 0000000..9cf5f37
--- /dev/null
+++ b/src/app/main.cpp
@@ -0,0 +1,203 @@
+#include "mainwindow.h"
+#include
+#include "app/session.h"
+
+#include
+
+#include
+#include
+
+#include
+
+#include
+
+#include
+#include
+
+#include
+
+#include "info.h"
+
+#include
+
+#include
+
+#include
+
+Q_GLOBAL_STATIC(QFile, gLogFile)
+static QtMessageHandler defaultHandler;
+static QMutex logMutex;
+
+void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
+{
+ QMutexLocker lock(&logMutex);
+
+ QString str;
+ //const QString fmt = QStringLiteral("%1: %2 (%3:%4, %5)\n");
+ static const QString fmt = QStringLiteral("%1: %2\n");
+ switch (type) {
+ case QtDebugMsg:
+ str = fmt.arg(QStringLiteral("Debug")).arg(msg); //.arg(context.file).arg(context.line).arg(context.function);
+ break;
+ case QtInfoMsg:
+ str = fmt.arg(QStringLiteral("Info")).arg(msg); //.arg(context.file).arg(context.line).arg(context.function);
+ break;
+ case QtWarningMsg:
+ str = fmt.arg(QStringLiteral("Warning")).arg(msg); //.arg(context.file).arg(context.line).arg(context.function);
+ break;
+ case QtCriticalMsg:
+ str = fmt.arg(QStringLiteral("Critical")).arg(msg); //.arg(context.file).arg(context.line).arg(context.function);
+ break;
+ case QtFatalMsg:
+ str = fmt.arg(QStringLiteral("Fatal")).arg(msg); //.arg(context.file).arg(context.line).arg(context.function);
+ }
+
+ QTextStream s(gLogFile());
+ s << str;
+
+ defaultHandler(type, context, msg);
+}
+
+void loadTranslations()
+{
+ QLocale locale = AppSettings.getLanguage();
+
+ qDebug() << "Locale:" << locale << locale.uiLanguages();
+
+ QString path = qApp->applicationDirPath() + QStringLiteral("/translations");
+ qDebug() << path;
+
+ QTranslator *translatorQt = new QTranslator(qApp);
+ if(translatorQt->load(locale,
+ QStringLiteral("qt"), QStringLiteral("_"),
+ path))
+ {
+ qDebug() << "Loading Qt translations";
+ qApp->installTranslator(translatorQt);
+ }
+
+ QTranslator *translator = new QTranslator(qApp);
+ if(translator->load(locale,
+ QStringLiteral("traintimetable"), QStringLiteral("_"),
+ path))
+ {
+ qDebug() << "Loading UI translations";
+ qApp->installTranslator(translator);
+ }
+}
+
+void setupLogger()
+{
+ //const QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
+ QString path = MeetingSession::appDataPath;
+ if(qApp->arguments().contains("--test"))
+ path = qApp->applicationDirPath(); //If testing use exe folder instead of AppData: see MeetingSession
+
+ QFile *logFile = gLogFile();
+
+ logFile->setFileName(path + QStringLiteral("/logs/traintimetable_log.log"));
+ logFile->open(QFile::WriteOnly | QFile::Append | QFile::Text);
+ if(!logFile->isOpen()) //FIXME: if logFile gets too big, ask user to truncate it
+ {
+ QDir dir(path);
+ dir.mkdir("logs");
+ logFile->open(QFile::WriteOnly | QFile::Append | QFile::Text);
+ }
+ if(logFile->isOpen())
+ {
+ defaultHandler = qInstallMessageHandler(myMessageOutput);
+ }
+ else {
+ qDebug() << "Cannot open Log file:" << logFile->fileName() << "Error:" << logFile->errorString();
+ }
+}
+
+int main(int argc, char *argv[])
+{
+#ifdef GLOBAL_TRY_CATCH
+ try{
+#endif
+
+ QApplication app(argc, argv);
+ QApplication::setOrganizationName(AppCompany);
+ //QApplication::setApplicationName(AppProduct);
+ QApplication::setApplicationDisplayName(AppDisplayName);
+ QApplication::setApplicationVersion(AppVersion);
+
+ MeetingSession::locateAppdata();
+
+ setupLogger();
+
+ qDebug() << QApplication::applicationDisplayName()
+ << "Version:" << QApplication::applicationVersion()
+ << "Built:" << AppBuildDate;
+ qDebug() << "Qt:" << QT_VERSION_STR;
+ qDebug() << "Sqlite:" << sqlite3_libversion() << " DB Format: V" << FormatVersion;
+ qDebug() << QDateTime::currentDateTime().toString("dd/MM/yyyy HH:mm");
+
+ //Check SQLite thread safety
+ int val = sqlite3_threadsafe();
+ if(val != 1)
+ {
+ //Not thread safe
+ qWarning() << "SQLite Library was not compiled with SQLITE_THREADSAFE=1. This may cause crashes of this application.";
+ }
+
+ MeetingSession meetingSession;
+ loadTranslations();
+
+ MainWindow w;
+ w.showNormal();
+ w.resize(800, 600);
+ w.showMaximized();
+
+ if(argc > 1) //FIXME: better handling if there are extra arguments
+ {
+ QString fileName = app.arguments().at(1);
+ qDebug() << "Trying to load:" << fileName;
+ if(QFile(fileName).exists())
+ {
+ w.loadFile(app.arguments().at(1));
+ }
+ }
+
+ qDebug() << "Running...";
+
+ int ret = app.exec();
+ QThreadPool::globalInstance()->waitForDone(1000);
+ DB_Error err = Session->closeDB();
+
+ if(err == DB_Error::DbBusyWhenClosing || QThreadPool::globalInstance()->activeThreadCount() > 0)
+ {
+ qWarning() << "Error: Application closing while threadpool still running or database busy!";
+ QThreadPool::globalInstance()->waitForDone(10000);
+ Session->closeDB();
+ }
+
+ return ret;
+
+
+#ifdef GLOBAL_TRY_CATCH
+ }catch(const char* str)
+ {
+ qDebug() << "Exception:" << str;
+ throw;
+ }catch(const std::string& str)
+ {
+ qDebug() << "Exception:" << str.c_str();
+ throw;
+ }catch(sqlite3pp::database_error& e)
+ {
+ qDebug() << "Exception:" << e.what();
+ throw;
+ }catch(std::exception& e)
+ {
+ qDebug() << "Exception:" << e.what();
+ throw;
+ }catch(...)
+ {
+ qDebug() << "Caught generic exception";
+ throw;
+ }
+#endif
+}
diff --git a/src/app/mainwindow.cpp b/src/app/mainwindow.cpp
new file mode 100644
index 0000000..7c21de2
--- /dev/null
+++ b/src/app/mainwindow.cpp
@@ -0,0 +1,920 @@
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+
+#include "app/session.h"
+
+#include "viewmanager/viewmanager.h"
+
+#include
+#include
+#include "utils/file_format_names.h"
+
+#include "jobs/jobeditor/jobpatheditor.h"
+#include
+
+#include
+#include
+#include
+
+#include
+
+#include "settings/settingsdialog.h"
+
+#include "graph/graphmanager.h"
+#include "graph/graphicsview.h"
+#include
+
+#include "db_metadata/meetinginformationdialog.h"
+
+#include "lines/linestorage.h"
+#include "lines/linesmatchmodel.h"
+
+#include "printing/printwizard.h"
+
+#ifdef ENABLE_USER_QUERY
+#include "sqlconsole/sqlconsole.h"
+#endif
+
+#include
+
+#include "utils/sqldelegate/customcompletionlineedit.h"
+#include "searchbox/searchresultmodel.h"
+
+#ifdef ENABLE_BACKGROUND_MANAGER
+#include "backgroundmanager/backgroundmanager.h"
+#endif
+
+#ifdef ENABLE_RS_CHECKER
+#include "rollingstock/rs_checker/rserrorswidget.h"
+#endif
+
+#include "propertiesdialog.h"
+#include "info.h"
+
+#include
+
+#include //HACK: TODO remove
+
+#include "app/scopedebug.h"
+
+MainWindow::MainWindow(QWidget *parent) :
+ QMainWindow(parent),
+ ui(new Ui::MainWindow),
+ jobEditor(nullptr),
+ #ifdef ENABLE_RS_CHECKER
+ rsErrorsWidget(nullptr),
+ rsErrDock(nullptr),
+ #endif
+ view(nullptr),
+ jobDock(nullptr),
+ searchEdit(nullptr),
+ lineComboSearch(nullptr),
+ welcomeLabel(nullptr),
+ recentFileActs{nullptr},
+ m_mode(CentralWidgetMode::StartPageMode)
+{
+ ui->setupUi(this);
+ ui->actionAbout->setText(tr("About %1").arg(qApp->applicationDisplayName()));
+
+ auto viewMgr = Session->getViewManager();
+ viewMgr->m_mainWidget = this;
+
+ auto graphMgr = viewMgr->getGraphMgr();
+ connect(graphMgr, &GraphManager::jobSelected, this, &MainWindow::onJobSelected);
+
+ view = graphMgr->getView();
+ view->setObjectName("GraphicsView");
+ view->setParent(this);
+
+ auto linesMatchModel = new LinesMatchModel(Session->m_Db, true, this);
+ linesMatchModel->setHasEmptyRow(false); //Do not allow empty view (no line selected)
+ lineComboSearch = new CustomCompletionLineEdit(linesMatchModel, this);
+ lineComboSearch->setPlaceholderText(tr("Choose line"));
+ lineComboSearch->setToolTip(tr("Choose a railway line"));
+ lineComboSearch->setMinimumWidth(200);
+ lineComboSearch->setMinimumHeight(25);
+ lineComboSearch->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+
+ QAction *sep = ui->mainToolBar->insertSeparator(ui->actionPrev_Job_Segment);
+ ui->mainToolBar->insertWidget(sep, lineComboSearch);
+
+ connect(lineComboSearch, &CustomCompletionLineEdit::dataIdChanged, graphMgr, &GraphManager::setCurrentLine);
+ connect(graphMgr, &GraphManager::currentLineChanged, lineComboSearch, &CustomCompletionLineEdit::setData_slot);
+
+ auto lineStorage = Session->mLineStorage;
+ connect(lineStorage, &LineStorage::lineAdded, this, &MainWindow::checkLineNumber);
+ connect(lineStorage, &LineStorage::lineRemoved, this, &MainWindow::checkLineNumber);
+
+ //Welcome label
+ welcomeLabel = new QLabel(this);
+ welcomeLabel->setTextFormat(Qt::RichText);
+ welcomeLabel->setAlignment(Qt::AlignCenter);
+ welcomeLabel->setFont(QFont("Arial", 15));
+ welcomeLabel->setObjectName("WelcomeLabel");
+
+ //JobPathEditor dock
+ jobEditor = new JobPathEditor(this);
+ viewMgr->jobEditor = jobEditor;
+ jobDock = new QDockWidget("JobEditor", this);
+ jobDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
+ jobDock->setWidget(jobEditor);
+ jobDock->installEventFilter(this); //NOTE: see MainWindow::eventFilter() below
+
+ addDockWidget(Qt::RightDockWidgetArea, jobDock);
+ ui->menuView->addAction(jobDock->toggleViewAction());
+ connect(jobDock->toggleViewAction(), &QAction::triggered, jobEditor, &JobPathEditor::show);
+
+#ifdef ENABLE_RS_CHECKER
+ //RS Errors dock
+ rsErrorsWidget = new RsErrorsWidget(this);
+ rsErrDock = new QDockWidget(rsErrorsWidget->windowTitle(), this);
+ rsErrDock->setAllowedAreas(Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea);
+ rsErrDock->setWidget(rsErrorsWidget);
+ rsErrDock->installEventFilter(this); //NOTE: see eventFilter() below
+
+ addDockWidget(Qt::BottomDockWidgetArea, rsErrDock);
+ ui->menuView->addAction(rsErrDock->toggleViewAction());
+ ui->mainToolBar->addAction(rsErrDock->toggleViewAction());
+#endif
+
+ //Allow JobPathEditor to use all vertical space when RsErrorWidget dock is at bottom
+ setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea);
+ setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea);
+
+ //Search Box
+ SearchResultModel *searchModel = new SearchResultModel(Session->m_Db, this);
+ searchEdit = new CustomCompletionLineEdit(searchModel, this);
+ searchEdit->setMinimumWidth(300);
+ searchEdit->setMinimumHeight(25);
+ searchEdit->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ searchEdit->setPlaceholderText(tr("Find"));
+ searchEdit->setClearButtonEnabled(true);
+ connect(searchEdit, &CustomCompletionLineEdit::dataIdChanged, this, &MainWindow::onJobSearchItemSelected);
+ connect(searchModel, &SearchResultModel::resultsReady, this, &MainWindow::onJobSearchResultsReady);
+
+ QWidget* spacer = new QWidget();
+ spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ ui->mainToolBar->addWidget(spacer);
+ ui->mainToolBar->addWidget(searchEdit);
+
+ setup_actions();
+ setCentralWidgetMode(CentralWidgetMode::StartPageMode);
+
+ QMenu *recentFilesMenu = new QMenu(this);
+ for(int i = 0; i < MaxRecentFiles; i++)
+ {
+ recentFileActs[i] = new QAction(this);
+ recentFileActs[i]->setVisible(false);
+ connect(recentFileActs[i], &QAction::triggered,
+ this, &MainWindow::onOpenRecent);
+
+ recentFilesMenu->addAction(recentFileActs[i]);
+ }
+
+ updateRecentFileActions();
+
+ ui->actionOpen_Recent->setMenu(recentFilesMenu);
+}
+
+MainWindow::~MainWindow()
+{
+ Session->getViewManager()->m_mainWidget = nullptr;
+ delete ui;
+}
+
+void MainWindow::setup_actions()
+{
+ databaseActionGroup = new QActionGroup(this);
+
+ databaseActionGroup->addAction(ui->actionAddJob);
+ databaseActionGroup->addAction(ui->actionRemoveJob);
+
+ databaseActionGroup->addAction(ui->actionStations);
+ databaseActionGroup->addAction(ui->actionRollingstockManager);
+ databaseActionGroup->addAction(ui->actionJob_Shifts);
+ databaseActionGroup->addAction(ui->action_JobsMgr);
+ databaseActionGroup->addAction(ui->actionRS_Session_Viewer);
+ databaseActionGroup->addAction(ui->actionMeeting_Information);
+
+ databaseActionGroup->addAction(ui->actionQuery);
+
+ databaseActionGroup->addAction(ui->actionClose);
+ databaseActionGroup->addAction(ui->actionPrint);
+
+ databaseActionGroup->addAction(ui->actionSave);
+ databaseActionGroup->addAction(ui->actionSaveCopy_As);
+
+ databaseActionGroup->addAction(ui->actionExport_PDF);
+ databaseActionGroup->addAction(ui->actionExport_Svg);
+
+ databaseActionGroup->addAction(ui->actionPrev_Job_Segment);
+ databaseActionGroup->addAction(ui->actionNext_Job_Segment);
+
+ connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::onOpen);
+ connect(ui->actionNew, &QAction::triggered, this, &MainWindow::onNew);
+ connect(ui->actionClose, &QAction::triggered, this, &MainWindow::onCloseSession);
+ connect(ui->actionSave, &QAction::triggered, this, &MainWindow::onSave);
+ connect(ui->actionSaveCopy_As, &QAction::triggered, this, &MainWindow::onSaveCopyAs);
+
+ connect(ui->actionPrint, &QAction::triggered, this, &MainWindow::onPrint);
+ connect(ui->actionExport_PDF, &QAction::triggered, this, &MainWindow::onPrintPDF);
+ connect(ui->actionExport_Svg, &QAction::triggered, this, &MainWindow::onExportSvg);
+ connect(ui->actionProperties, &QAction::triggered, this, &MainWindow::onProperties);
+
+ connect(ui->actionStations, &QAction::triggered, this, &MainWindow::onStationManager);
+ connect(ui->actionRollingstockManager, &QAction::triggered, this, &MainWindow::onRollingStockManager);
+ connect(ui->actionJob_Shifts, &QAction::triggered, this, &MainWindow::onShiftManager);
+ connect(ui->action_JobsMgr, &QAction::triggered, this, &MainWindow::onJobsManager);
+ connect(ui->actionRS_Session_Viewer, &QAction::triggered, this, &MainWindow::onSessionRSViewer);
+ connect(ui->actionMeeting_Information, &QAction::triggered, this, &MainWindow::onMeetingInformation);
+
+ connect(ui->actionAddJob, &QAction::triggered, this, &MainWindow::onAddJob);
+ connect(ui->actionRemoveJob, &QAction::triggered, this, &MainWindow::onRemoveJob);
+
+ connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::about);
+ connect(ui->actionAbout_Qt, &QAction::triggered, qApp, &QApplication::aboutQt);
+
+#ifdef ENABLE_USER_QUERY
+ connect(ui->actionQuery, &QAction::triggered, this, &MainWindow::onExecQuery);
+#else
+ ui->actionQuery->setVisible(false);
+ ui->actionQuery->setEnabled(false);
+#endif
+
+ connect(ui->actionSettings, &QAction::triggered, this, &MainWindow::onOpenSettings);
+
+ connect(ui->actionExit, &QAction::triggered, this, &MainWindow::close);
+
+ connect(ui->actionNext_Job_Segment, &QAction::triggered, this, []()
+ {
+ Session->getViewManager()->requestJobShowPrevNextSegment(false);
+ });
+ connect(ui->actionPrev_Job_Segment, &QAction::triggered, this, []()
+ {
+ Session->getViewManager()->requestJobShowPrevNextSegment(true);
+ });
+}
+
+void MainWindow::about()
+{
+ QMessageBox::information(this,
+ tr("About"),
+ tr("%1 application makes easier to deal with timetables and trains\n"
+ "Beta version: %2\n"
+ "Built: %3")
+ .arg(qApp->applicationDisplayName())
+ .arg(qApp->applicationVersion())
+ .arg(QDate::fromString(AppBuildDate, QLatin1String("MMM dd yyyy")).toString("dd/MM/yyyy")));
+}
+
+void MainWindow::onOpen()
+{
+ DEBUG_ENTRY;
+
+#ifdef SEARCHBOX_MODE_ASYNC
+ Session->getBackgroundManager()->abortTrivialTasks();
+#endif
+
+#ifdef ENABLE_BACKGROUND_MANAGER
+ if(Session->getBackgroundManager()->isRunning())
+ {
+ int ret = QMessageBox::warning(this,
+ tr("Backgroung Task"),
+ tr("Background task for checking rollingstock errors is still running.\n"
+ "Do you want to cancel it?"),
+ QMessageBox::Yes, QMessageBox::No,
+ QMessageBox::Yes);
+ if(ret == QMessageBox::Yes)
+ Session->getBackgroundManager()->abortAllTasks();
+ else
+ return;
+ }
+#endif
+
+ QFileDialog dlg(this, tr("Open Session"));
+ dlg.setFileMode(QFileDialog::ExistingFile);
+ dlg.setAcceptMode(QFileDialog::AcceptOpen);
+ dlg.setDirectory(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
+
+ QStringList filters;
+ filters << FileFormats::tr(FileFormats::tttFormat);
+ filters << FileFormats::tr(FileFormats::sqliteFormat);
+ filters << FileFormats::tr(FileFormats::allFiles);
+ dlg.setNameFilters(filters);
+
+ if(dlg.exec() != QDialog::Accepted)
+ return;
+
+ QString fileName = dlg.selectedUrls().value(0).toLocalFile();
+
+ if(fileName.isEmpty())
+ return;
+
+ QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
+
+ if(!QThreadPool::globalInstance()->waitForDone(2000))
+ {
+ QMessageBox::warning(this,
+ tr("Background Tasks"),
+ tr("Some background tasks are still running.\n"
+ "The file was not opened. Try again."));
+ QApplication::restoreOverrideCursor();
+ return;
+ }
+
+ QApplication::restoreOverrideCursor();
+
+ loadFile(fileName);
+}
+
+void MainWindow::loadFile(const QString& fileName)
+{
+ DEBUG_ENTRY;
+ if(fileName.isEmpty())
+ return;
+
+ qDebug() << "Loading:" << fileName;
+
+ QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
+
+ DB_Error err = Session->openDB(fileName, false);
+
+ QApplication::restoreOverrideCursor();
+
+ if(err == DB_Error::FormatTooOld)
+ {
+ int but = QMessageBox::warning(this, tr("Version is old"),
+ tr("This file was created by an older version of Train Timetable.\n"
+ "Opening it without conversion might not work and even crash the application.\n"
+ "Do you want to open it anyway?"),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
+ if(but == QMessageBox::Yes)
+ err = Session->openDB(fileName, true);
+ }
+ else if(err == DB_Error::FormatTooNew)
+ {
+ if(err == DB_Error::FormatTooOld)
+ {
+ int but = QMessageBox::warning(this, tr("Version is too new"),
+ tr("This file was created by a newer version of Train Timetable.\n"
+ "You should update the application first. Opening this file might not work or even crash.\n"
+ "Do you want to open it anyway?"),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
+ if(but == QMessageBox::Yes)
+ err = Session->openDB(fileName, true);
+ }
+ }
+
+ if(err == DB_Error::DbBusyWhenClosing)
+ showCloseWarning();
+
+ if(err != DB_Error::NoError)
+ return;
+
+ setCurrentFile(fileName);
+
+ //Fake we are coming from Start Page
+ //Otherwise we cannot show the first line
+ m_mode = CentralWidgetMode::StartPageMode;
+ checkLineNumber();
+
+
+ if(!Session->checkImportRSTablesEmpty())
+ {
+ //Probably the application crashed before finishing RS importation
+ //Give user choice to resume it or discard
+
+ QMessageBox msgBox(QMessageBox::Warning,
+ tr("RS Import"),
+ tr("There is some rollingstock import data left in this file. "
+ "Probably the application has crashed!
"
+ "Before deleting it would you like to resume importation?
"
+ "(Sorry for the crash, would you like to contact me and share information about it?)"),
+ QMessageBox::NoButton, this);
+ auto resumeBut = msgBox.addButton(tr("Resume importation"), QMessageBox::YesRole);
+ msgBox.addButton(tr("Just delete it"), QMessageBox::NoRole);
+ msgBox.setDefaultButton(resumeBut);
+ msgBox.setTextFormat(Qt::RichText);
+
+ msgBox.exec();
+
+ if(msgBox.clickedButton() == resumeBut)
+ {
+ Session->getViewManager()->resumeRSImportation();
+ }else{
+ Session->clearImportRSTables();
+ }
+ }
+}
+
+void MainWindow::setCurrentFile(const QString& fileName)
+{
+ DEBUG_ENTRY;
+
+ if(fileName.isEmpty())
+ {
+ setWindowFilePath(QString()); //Reset title bar
+ return;
+ }
+
+ //Qt automatically takes care of showing stripped filename in window title
+ setWindowFilePath(fileName);
+
+ QStringList files = AppSettings.getRecentFiles();
+ files.removeAll(fileName);
+ files.prepend(fileName);
+ while (files.size() > MaxRecentFiles)
+ files.removeLast();
+
+ AppSettings.setRecentFiles(files);
+
+ updateRecentFileActions();
+}
+
+QString MainWindow::strippedName(const QString &fullFileName, bool *ok)
+{
+ QFileInfo fi(fullFileName);
+ if(ok) *ok = fi.exists();
+ return fi.fileName();
+}
+
+void MainWindow::updateRecentFileActions()
+{
+ DEBUG_ENTRY;
+ QStringList files = AppSettings.getRecentFiles();
+
+ int numRecentFiles = qMin(files.size(), int(MaxRecentFiles));
+
+ for (int i = 0; i < numRecentFiles; i++)
+ {
+ bool ok = true;
+ QString name = strippedName(files[i], &ok);
+ if(name.isEmpty() || !ok)
+ {
+ files.removeAt(i);
+ i--;
+ numRecentFiles = qMin(files.size(), int(MaxRecentFiles));
+ }
+ else
+ {
+ QString text = tr("&%1 %2").arg(i + 1).arg(name);
+ recentFileActs[i]->setText(text);
+ recentFileActs[i]->setData(files[i]);
+ recentFileActs[i]->setToolTip(files[i]);
+ recentFileActs[i]->setVisible(true);
+ }
+ }
+ for (int j = numRecentFiles; j < MaxRecentFiles; ++j)
+ recentFileActs[j]->setVisible(false);
+
+ AppSettings.setRecentFiles(files);
+}
+
+void MainWindow::onOpenRecent()
+{
+ DEBUG_ENTRY;
+ QAction *act = qobject_cast(sender());
+ if(!act)
+ return;
+
+ loadFile(act->data().toString());
+}
+
+void MainWindow::onNew()
+{
+ DEBUG_ENTRY;
+
+#ifdef SEARCHBOX_MODE_ASYNC
+ Session->getBackgroundManager()->abortTrivialTasks();
+#endif
+
+#ifdef ENABLE_RS_CHECKER
+ if(Session->getBackgroundManager()->isRunning())
+ {
+ int ret = QMessageBox::warning(this,
+ tr("Backgroung Task"),
+ tr("Background task for checking rollingstock errors is still running.\n"
+ "Do you want to cancel it?"),
+ QMessageBox::Yes, QMessageBox::No,
+ QMessageBox::Yes);
+ if(ret == QMessageBox::Yes)
+ Session->getBackgroundManager()->abortAllTasks();
+ else
+ return;
+ }
+#endif
+
+ QFileDialog dlg(this, tr("Create new Session"));
+ dlg.setFileMode(QFileDialog::AnyFile);
+ dlg.setAcceptMode(QFileDialog::AcceptSave);
+ dlg.setDirectory(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
+
+ QStringList filters;
+ filters << FileFormats::tr(FileFormats::tttFormat);
+ filters << FileFormats::tr(FileFormats::sqliteFormat);
+ filters << FileFormats::tr(FileFormats::allFiles);
+ dlg.setNameFilters(filters);
+
+ if(dlg.exec() != QDialog::Accepted)
+ return;
+
+ QString fileName = dlg.selectedUrls().value(0).toLocalFile();
+
+ if(fileName.isEmpty())
+ return;
+
+ QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
+
+ if(!QThreadPool::globalInstance()->waitForDone(2000))
+ {
+ QMessageBox::warning(this,
+ tr("Background Tasks"),
+ tr("Some background tasks are still running.\n"
+ "The new file was not created. Try again."));
+ QApplication::restoreOverrideCursor();
+ return;
+ }
+
+ QFile f(fileName);
+ if(f.exists())
+ f.remove();
+
+ DB_Error err = Session->createNewDB(fileName);
+
+ QApplication::restoreOverrideCursor();
+
+ if(err == DB_Error::DbBusyWhenClosing)
+ showCloseWarning();
+
+ if(err != DB_Error::NoError)
+ return;
+
+ setCurrentFile(fileName);
+ checkLineNumber();
+}
+
+void MainWindow::onSave()
+{
+ if(!Session->getViewManager()->closeEditors())
+ return;
+
+ Session->releaseAllSavepoints();
+}
+
+void MainWindow::onSaveCopyAs()
+{
+ DEBUG_ENTRY;
+
+ if(!Session->getViewManager()->closeEditors())
+ return;
+
+ QFileDialog dlg(this, tr("Save Session Copy"));
+ dlg.setFileMode(QFileDialog::AnyFile);
+ dlg.setAcceptMode(QFileDialog::AcceptSave);
+ dlg.setDirectory(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
+
+ QStringList filters;
+ filters << FileFormats::tr(FileFormats::tttFormat);
+ filters << FileFormats::tr(FileFormats::sqliteFormat);
+ filters << FileFormats::tr(FileFormats::allFiles);
+ dlg.setNameFilters(filters);
+
+ if(dlg.exec() != QDialog::Accepted)
+ return;
+
+ QString fileName = dlg.selectedUrls().value(0).toLocalFile();
+
+ if(fileName.isEmpty())
+ return;
+
+ QFile f(fileName);
+ if(f.exists())
+ f.remove();
+
+ database backupDB(fileName.toUtf8(), SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
+
+ int rc = Session->m_Db.backup(backupDB, [](int pageCount, int remaining, int res)
+ {
+ Q_UNUSED(res)
+ qDebug() << pageCount << "/" << remaining;
+ });
+
+ if(rc != SQLITE_OK && rc != SQLITE_DONE)
+ {
+ QString errMsg = Session->m_Db.error_msg();
+ qDebug() << Session->m_Db.error_code() << errMsg;
+ QMessageBox::warning(this, tr("Error saving copy"), errMsg);
+ }
+}
+
+void MainWindow::closeEvent(QCloseEvent *e)
+{
+ if(closeSession())
+ e->accept();
+ else
+ e->ignore();
+}
+
+void MainWindow::showCloseWarning()
+{
+ QMessageBox::warning(this,
+ tr("Error while Closing"),
+ tr("There was an error while closing the database.\n"
+ "Make sure there aren't any background tasks running and try again."));
+}
+
+void MainWindow::setCentralWidgetMode(MainWindow::CentralWidgetMode mode)
+{
+ switch (mode)
+ {
+ case CentralWidgetMode::StartPageMode:
+ {
+ jobDock->hide();
+#ifdef ENABLE_RS_CHECKER
+ rsErrDock->hide();
+#endif
+ welcomeLabel->setText(tr("Open a file: File > Open
"
+ "Create new project: File > New
"));
+ statusBar()->showMessage(tr("Open file or create a new one"));
+
+ break;
+ }
+ case CentralWidgetMode::NoLinesWarningMode:
+ {
+ jobDock->show();
+#ifdef ENABLE_RS_CHECKER
+ rsErrDock->hide();
+#endif
+ welcomeLabel->setText(
+ tr("There are no lines in this session
"
+ ""
+ "
"
+ ""
+ "Start by creating the railway layout for this session: | "
+ "
"
+ ""
+ ""
+ ""
+ ""
+ "1. | "
+ "Create stations (Edit > Stations) | "
+ " "
+ ""
+ "2. | "
+ "Create railway lines (Edit > Stations > Lines Tab) | "
+ " "
+ ""
+ "3. | "
+ "Add stations to railway lines | "
+ " "
+ ""
+ " | "
+ "(Edit > Stations > Lines Tab > Edit Line) | "
+ " "
+ " "
+ " | "
+ "
"
+ "
"
+ "
"));
+ break;
+ }
+ case CentralWidgetMode::ViewSessionMode:
+ {
+ jobDock->show();
+#ifdef ENABLE_RS_CHECKER
+ rsErrDock->show();
+#endif
+ welcomeLabel->setText(QString());
+ break;
+ }
+ }
+
+ if(mode == CentralWidgetMode::ViewSessionMode)
+ {
+ if(centralWidget() != view)
+ {
+ takeCentralWidget(); //Remove ownership from welcomeLabel
+ setCentralWidget(view);
+ view->show();
+ welcomeLabel->hide();
+ }
+ }
+ else
+ {
+ if(centralWidget() != welcomeLabel)
+ {
+ takeCentralWidget(); //Remove ownership from QGraphicsView
+ setCentralWidget(welcomeLabel);
+ view->hide();
+ welcomeLabel->show();
+ }
+ }
+
+ enableDBActions(mode != CentralWidgetMode::StartPageMode);
+
+ m_mode = mode;
+}
+
+void MainWindow::onCloseSession()
+{
+ closeSession();
+}
+
+void MainWindow::onProperties()
+{
+ PropertiesDialog dlg(this);
+ dlg.exec();
+}
+
+void MainWindow::onMeetingInformation()
+{
+ MeetingInformationDialog dlg(this);
+ if(dlg.exec() == QDialog::Accepted)
+ dlg.saveData();
+}
+
+bool MainWindow::closeSession()
+{
+ DB_Error err = Session->closeDB();
+
+ if(err == DB_Error::DbBusyWhenClosing)
+ showCloseWarning();
+
+ if(err != DB_Error::NoError && err != DB_Error::DbNotOpen)
+ return false;
+
+ setCentralWidgetMode(CentralWidgetMode::StartPageMode);
+
+ //Reset filePath to refresh title
+ setCurrentFile(QString());
+
+ lineComboSearch->setData(0);
+
+ return true;
+}
+
+void MainWindow::enableDBActions(bool enable)
+{
+ databaseActionGroup->setEnabled(enable);
+ searchEdit->setEnabled(enable);
+ lineComboSearch->setEnabled(enable);
+ if(!enable)
+ jobEditor->setEnabled(false);
+
+#ifdef ENABLE_RS_CHECKER
+ rsErrorsWidget->setEnabled(enable);
+#endif
+}
+
+void MainWindow::onStationManager()
+{
+ Session->getViewManager()->showStationsManager();
+}
+
+void MainWindow::onRollingStockManager()
+{
+ Session->getViewManager()->showRSManager();
+}
+
+void MainWindow::onShiftManager()
+{
+ Session->getViewManager()->showShiftManager();
+}
+
+void MainWindow::onJobsManager()
+{
+ Session->getViewManager()->showJobsManager();
+}
+
+void MainWindow::onAddJob()
+{
+ Session->getViewManager()->requestJobCreation();
+}
+
+void MainWindow::onRemoveJob()
+{
+ DEBUG_ENTRY;
+ Session->getViewManager()->removeSelectedJob();
+}
+
+void MainWindow::onPrint()
+{
+ PrintWizard wizard(this);
+ wizard.setOutputType(Print::Native);
+ wizard.exec();
+}
+
+void MainWindow::onPrintPDF()
+{
+ PrintWizard wizard(this);
+ wizard.setOutputType(Print::Pdf);
+ wizard.exec();
+}
+
+void MainWindow::onExportSvg()
+{
+ PrintWizard wizard(this);
+ wizard.setOutputType(Print::Svg);
+ wizard.exec();
+}
+
+#ifdef ENABLE_USER_QUERY
+void MainWindow::onExecQuery()
+{
+ DEBUG_ENTRY;
+ SQLConsole *console = new SQLConsole(this);
+ console->setAttribute(Qt::WA_DeleteOnClose);
+ console->show();
+}
+#endif
+
+void MainWindow::onOpenSettings()
+{
+ DEBUG_ENTRY;
+ SettingsDialog dlg(this);
+ dlg.loadSettings();
+ dlg.exec();
+}
+
+void MainWindow::checkLineNumber()
+{
+ auto graphMgr = Session->getViewManager()->getGraphMgr();
+ db_id firstLineId = graphMgr->getFirstLineId();
+
+ if(firstLineId && m_mode != CentralWidgetMode::ViewSessionMode)
+ {
+ //First line was added or newly opened file -> Session has at least one line
+ ui->actionAddJob->setEnabled(true);
+ ui->actionAddJob->setToolTip(tr("Add train job"));
+ ui->actionRemoveJob->setEnabled(true);
+ setCentralWidgetMode(CentralWidgetMode::ViewSessionMode);
+
+ //Show first line (Alphabetically)
+ graphMgr->setCurrentLine(firstLineId);
+ }
+ else if(firstLineId == 0 && m_mode != CentralWidgetMode::NoLinesWarningMode)
+ {
+ //Last line removed -> Session has no line
+ //If there aren't lines prevent from creating jobs
+ ui->actionAddJob->setEnabled(false);
+ ui->actionAddJob->setToolTip(tr("You must create at least one line before adding job to this session"));
+ ui->actionRemoveJob->setEnabled(false);
+ setCentralWidgetMode(CentralWidgetMode::NoLinesWarningMode);
+ }
+}
+
+void MainWindow::onJobSelected(db_id jobId)
+{
+ const bool selected = jobId != 0;
+ ui->actionPrev_Job_Segment->setEnabled(selected);
+ ui->actionNext_Job_Segment->setEnabled(selected);
+}
+
+//QT-BUG: If user closes a floating dock widget, when shown again it cannot dock anymore
+//HACK: intercept dock close event and manually re-dock and hide so next time is shown it's docked
+//NOTE: calling directly 'QDockWidget::setFloating(false)' from inside 'eventFinter()' causes CRASH
+// so queue it. Cannot use 'QMetaObject::invokeMethod()' because it's not a slot.
+bool MainWindow::eventFilter(QObject *watched, QEvent *event)
+{
+ if(watched == jobDock && event->type() == QEvent::Close)
+ {
+ if(jobDock->isFloating())
+ {
+ QTimer::singleShot(0, jobDock, [this]()
+ {
+ jobDock->setFloating(false);
+ });
+ }
+ }
+#ifdef ENABLE_RS_CHECKER
+ else if(watched == rsErrDock && event->type() == QEvent::Close)
+ {
+ if(rsErrDock->isFloating())
+ {
+ QTimer::singleShot(0, rsErrDock, [this]()
+ {
+ rsErrDock->setFloating(false);
+ });
+ }
+ }
+#endif
+
+ return QMainWindow::eventFilter(watched, event);
+}
+
+void MainWindow::onSessionRSViewer()
+{
+ Session->getViewManager()->showSessionStartEndRSViewer();
+}
+
+void MainWindow::onJobSearchItemSelected(db_id jobId)
+{
+ searchEdit->clear(); //Clear text
+ Session->getViewManager()->requestJobSelection(jobId, true, true);
+}
+
+void MainWindow::onJobSearchResultsReady()
+{
+ searchEdit->resizeColumnToContents();
+ searchEdit->selectFirstIndexOrNone(true);
+}
diff --git a/src/app/mainwindow.h b/src/app/mainwindow.h
new file mode 100644
index 0000000..2d3de75
--- /dev/null
+++ b/src/app/mainwindow.h
@@ -0,0 +1,134 @@
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include
+
+#include "utils/types.h"
+
+namespace Ui {
+class MainWindow;
+}
+
+namespace sqlite3pp {
+class database;
+}
+
+using namespace sqlite3pp;
+
+class QGraphicsView;
+class QGraphicsScene;
+class JobPathEditor;
+class QDockWidget;
+class QLabel;
+class QActionGroup;
+class CustomCompletionLineEdit;
+
+#ifdef ENABLE_RS_CHECKER
+class RsErrorsWidget;
+#endif
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ explicit MainWindow(QWidget *parent = nullptr);
+ ~MainWindow();
+
+ void enableDBActions(bool enable);
+ void setCurrentFile(const QString &fileName);
+ void updateRecentFileActions();
+
+ QString strippedName(const QString &fullFileName, bool *ok = nullptr);
+ void loadFile(const QString &fileName);
+
+ bool closeSession();
+
+private slots:
+ void onStationManager();
+
+ void onNew();
+ void onOpen();
+ void onOpenRecent();
+ void onCloseSession();
+
+ void onProperties();
+ void onMeetingInformation();
+
+ void onRollingStockManager();
+
+ void onShiftManager();
+
+ void onJobsManager();
+
+ void onAddJob();
+ void onRemoveJob();
+
+ void about();
+ void onPrint();
+ void onPrintPDF();
+ void onExportSvg();
+
+#ifdef ENABLE_USER_QUERY
+ void onExecQuery();
+#endif
+
+ void onSaveCopyAs();
+ void onSave();
+ void onOpenSettings();
+
+ void checkLineNumber();
+
+protected:
+ void closeEvent(QCloseEvent *e) override;
+
+ bool eventFilter(QObject *watched, QEvent *event) override;
+
+private slots:
+ void onJobSelected(db_id jobId);
+
+ void onSessionRSViewer();
+
+ void onJobSearchItemSelected(db_id jobId);
+ void onJobSearchResultsReady();
+
+private:
+ void setup_actions();
+ void showCloseWarning();
+
+ enum class CentralWidgetMode
+ {
+ StartPageMode,
+ NoLinesWarningMode,
+ ViewSessionMode
+ };
+
+ void setCentralWidgetMode(CentralWidgetMode mode);
+
+private:
+ Ui::MainWindow *ui;
+
+ JobPathEditor *jobEditor;
+
+#ifdef ENABLE_RS_CHECKER
+ RsErrorsWidget *rsErrorsWidget;
+ QDockWidget *rsErrDock;
+#endif
+
+ QGraphicsView *view;
+ QDockWidget *jobDock;
+
+ CustomCompletionLineEdit *searchEdit;
+ CustomCompletionLineEdit *lineComboSearch;
+
+ QLabel *welcomeLabel;
+
+ QActionGroup *databaseActionGroup;
+
+ enum { MaxRecentFiles = 5 };
+ QAction* recentFileActs[MaxRecentFiles];
+
+ CentralWidgetMode m_mode;
+};
+
+#endif // MAINWINDOW_H
diff --git a/src/app/mainwindow.ui b/src/app/mainwindow.ui
new file mode 100644
index 0000000..30c4845
--- /dev/null
+++ b/src/app/mainwindow.ui
@@ -0,0 +1,244 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 400
+ 300
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+ TopToolBarArea
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+ Open
+
+
+
+
+ Save
+
+
+
+
+ Exit
+
+
+
+
+ New Job
+
+
+
+
+ Remove Job
+
+
+ Remove selected train job
+
+
+
+
+ Stations
+
+
+
+
+ Rollingstock
+
+
+
+
+ New
+
+
+
+
+ Close
+
+
+
+
+ About
+
+
+
+
+ About Qt
+
+
+
+
+ Print
+
+
+
+
+ Export PDF
+
+
+
+
+ Open Recent
+
+
+
+
+ Query
+
+
+
+
+ Save Copy As
+
+
+ Save a backup copy of the current session
+
+
+
+
+ Job Shifts
+
+
+
+
+ Export Svg
+
+
+
+
+ Jobs
+
+
+
+
+ Settings
+
+
+
+
+ false
+
+
+ Next Job Segment
+
+
+
+
+ false
+
+
+ Prev Job Segment
+
+
+
+
+ Properties
+
+
+
+
+ Session Rollingstock Summary
+
+
+ Show rollingstock position at start or end of session
+
+
+
+
+ Meeting Information
+
+
+
+
+
+
+
diff --git a/src/app/propertiesdialog.cpp b/src/app/propertiesdialog.cpp
new file mode 100644
index 0000000..830d99c
--- /dev/null
+++ b/src/app/propertiesdialog.cpp
@@ -0,0 +1,34 @@
+#include "propertiesdialog.h"
+
+#include "app/session.h"
+
+#include
+#include
+#include
+
+#include
+
+PropertiesDialog::PropertiesDialog(QWidget *parent) :
+ QDialog(parent)
+{
+ setWindowTitle(tr("File Properties"));
+
+ QGridLayout *lay = new QGridLayout(this);
+ lay->setAlignment(Qt::AlignTop | Qt::AlignLeft);
+ lay->setContentsMargins(9, 9, 9, 9);
+
+ pathLabel = new QLabel;
+ lay->addWidget(pathLabel, 0, 0);
+
+ pathReadOnlyEdit = new QLineEdit;
+ lay->addWidget(pathReadOnlyEdit, 0, 1);
+
+ pathLabel->setText(tr("File Path:"));
+ pathReadOnlyEdit->setText(QDir::toNativeSeparators(Session->fileName));
+ pathReadOnlyEdit->setPlaceholderText(tr("No opened file"));
+ pathReadOnlyEdit->setReadOnly(true);
+
+ //TODO: make pretty and maybe add other informations like metadata versions
+
+ setMinimumSize(200, 200);
+}
diff --git a/src/app/propertiesdialog.h b/src/app/propertiesdialog.h
new file mode 100644
index 0000000..555cb67
--- /dev/null
+++ b/src/app/propertiesdialog.h
@@ -0,0 +1,20 @@
+#ifndef PROPERTIESDIALOG_H
+#define PROPERTIESDIALOG_H
+
+#include
+
+class QLabel;
+class QLineEdit;
+
+class PropertiesDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ explicit PropertiesDialog(QWidget *parent = nullptr);
+
+private:
+ QLabel *pathLabel;
+ QLineEdit *pathReadOnlyEdit;
+};
+
+#endif // PROPERTIESDIALOG_H
diff --git a/src/app/scopedebug.cpp b/src/app/scopedebug.cpp
new file mode 100644
index 0000000..42a041c
--- /dev/null
+++ b/src/app/scopedebug.cpp
@@ -0,0 +1,34 @@
+#include "app/scopedebug.h"
+
+#ifndef NO_DEBUG_CALL_TRACE
+
+static uint thread_local stackLevel = 0;
+
+Scope::Scope(const char *fn, const char *s, const char *e) :
+ func(fn),
+ start(s),
+ end(e)
+{
+ qDebug().nospace().noquote()
+ << start << QByteArray(" ").repeated(stackLevel) << ">>> " << func << end;
+ stackLevel++;
+}
+
+Scope::~Scope()
+{
+ stackLevel--;
+ qDebug().nospace().noquote()
+ << start << QByteArray(" ").repeated(stackLevel) << "<<< " << func << end;
+}
+
+ScopeTimer::ScopeTimer(const char *fn, const char *s, const char *e) :Scope(fn, s, e)
+{
+ timer.start();
+}
+
+ScopeTimer::~ScopeTimer()
+{
+ qDebug() << "TOOK" << timer.elapsed() << "ms";
+}
+
+#endif
diff --git a/src/app/scopedebug.h b/src/app/scopedebug.h
new file mode 100644
index 0000000..f742209
--- /dev/null
+++ b/src/app/scopedebug.h
@@ -0,0 +1,57 @@
+#ifndef SCOPEDEBUG_H
+#define SCOPEDEBUG_H
+
+
+#define SHELL_RESET "\033[0m"
+
+#define SHELL_RED "\033[031m"
+#define SHELL_GREEN "\033[032m"
+#define SHELL_YELLOW "\033[033m"
+#define SHELL_BLUE "\033[034m"
+
+
+#include
+#include
+#include
+
+//#define NO_DEBUG_CALL_TRACE
+
+#ifndef NO_DEBUG_CALL_TRACE
+
+class Scope
+{
+public:
+ Scope(const char *fn, const char *s="", const char* e="");
+ ~Scope();
+
+ const char *func, *start, *end;
+};
+
+
+class ScopeTimer : Scope
+{
+public:
+ ScopeTimer(const char *fn, const char *s="", const char* e="");
+ ~ScopeTimer();
+
+ QElapsedTimer timer;
+};
+
+
+
+# define DEBUG_ENTRY_NAME(name) Scope DBG(name)
+# define DEBUG_ENTRY DEBUG_ENTRY_NAME(__PRETTY_FUNCTION__)
+# define DEBUG_COLOR_ENTRY(color) Scope DBG(__PRETTY_FUNCTION__, color, SHELL_RESET)
+# define DEBUG_IMPORTANT_ENTRY DEBUG_COLOR_ENTRY(SHELL_GREEN)
+
+# define DEBUG_TIME_ENTRY ScopeTimer DBG(__PRETTY_FUNCTION__)
+
+#else
+# define DEBUG_ENTRY_NAME(name)
+# define DEBUG_ENTRY
+# define DEBUG_COLOR_ENTRY(color)
+# define DEBUG_IMPORTANT_ENTRY
+# define DEBUG_TIME_ENTRY
+#endif // NO_DEBUG_CALLTRACE
+
+#endif // SCOPEDEBUG_H
diff --git a/src/app/session.cpp b/src/app/session.cpp
new file mode 100644
index 0000000..43b8052
--- /dev/null
+++ b/src/app/session.cpp
@@ -0,0 +1,824 @@
+#include "app/session.h"
+#include "info.h"
+#include "utils/platform_utils.h"
+
+#include
+
+#include
+#include "app/scopedebug.h"
+
+#include "viewmanager/viewmanager.h"
+#include "graph/graphmanager.h"
+#include "db_metadata/metadatamanager.h"
+
+#include "lines/linestorage.h"
+
+#include "jobs/jobstorage.h"
+
+#ifdef ENABLE_BACKGROUND_MANAGER
+#include "backgroundmanager/backgroundmanager.h"
+
+#ifdef ENABLE_RS_CHECKER
+#include "rollingstock/rs_checker/rscheckermanager.h"
+#endif // ENABLE_RS_CHECKER
+
+#endif // ENABLE_BACKGROUND_MANAGER
+
+#include "utils/jobcategorystrings.h"
+
+#include
+#include
+
+MeetingSession* MeetingSession::session;
+QString MeetingSession::appDataPath;
+
+MeetingSession::MeetingSession() :
+ hourOffset(140),
+ stationOffset(150),
+ platformOffset(15),
+
+ horizOffset(50),
+ vertOffset(30),
+
+ jobLineWidth(6),
+
+ m_Db(nullptr),
+
+ q_getPrevStop(m_Db),
+ q_getNextStop(m_Db),
+
+ q_getKmDirection(m_Db)
+{
+ session = this; //Global singleton pointer
+
+ QString settings_file;
+ if(qApp->arguments().contains("test"))
+ {
+ //If testing use exe folder instead of AppData
+ settings_file = QCoreApplication::applicationDirPath() + QStringLiteral("/traintimetable_settings.ini");
+ }
+
+ loadSettings(settings_file);
+
+ mLineStorage = new LineStorage(m_Db);
+ mJobStorage = new JobStorage(m_Db);
+
+ viewManager.reset(new ViewManager);
+
+ metaDataMgr.reset(new MetaDataManager(m_Db));
+
+#ifdef ENABLE_BACKGROUND_MANAGER
+ backgroundManager.reset(new BackgroundManager);
+#endif
+}
+
+MeetingSession::~MeetingSession()
+{
+ //JobStorage refers to LineStorage so delete it first
+ delete mJobStorage;
+ delete mLineStorage;
+}
+
+MeetingSession *MeetingSession::Get()
+{
+ return session;
+}
+
+DB_Error MeetingSession::openDB(const QString &str, bool ignoreVersion)
+{
+ DEBUG_ENTRY;
+
+ DB_Error err = closeDB();
+ if(err != DB_Error::NoError && err != DB_Error::DbNotOpen)
+ {
+ return err;
+ }
+
+ //try{
+ if(m_Db.connect(str.toUtf8(), SQLITE_OPEN_READWRITE) != SQLITE_OK)
+ {
+ //throw database_error(m_Db);
+ qWarning() << "DB:" << m_Db.error_msg();
+ return DB_Error::GenericError;
+ }
+
+ if(!ignoreVersion)
+ {
+ qint64 version = 0;
+ switch (metaDataMgr->getInt64(version, MetaDataKey::FormatVersionKey))
+ {
+ case MetaDataKey::Result::ValueFound:
+ {
+ if(version < FormatVersion)
+ return DB_Error::FormatTooOld;
+ else if(version > FormatVersion)
+ return DB_Error::FormatTooNew;
+ break;
+ }
+ default:
+ return DB_Error::FormatTooOld;
+ }
+ }
+
+ fileName = str;
+
+ //Enable foreign keys to ensure no invalid operation is allowed
+ m_Db.enable_foreign_keys(true);
+ m_Db.enable_extended_result_codes(true);
+
+ prepareQueryes();
+
+ mJobStorage->fixJobs();
+
+ // }catch(const char *msg)
+ // {
+ // QMessageBox::warning(nullptr,
+ // QObject::tr("Error"),
+ // QObject::tr("Error while opening file:\n%1\n'%2'")
+ // .arg(str)
+ // .arg(msg));
+ // throw;
+ // return false;
+ // }
+ // catch(std::exception& e)
+ // {
+ // QMessageBox::warning(nullptr,
+ // QObject::tr("Error"),
+ // QObject::tr("Error while opening file:\n%1\n'%2'")
+ // .arg(str)
+ // .arg(e.what()));
+ // throw;
+ // return false;
+ // }
+ // catch(...)
+ // {
+ // QMessageBox::warning(nullptr,
+ // QObject::tr("Error"),
+ // QObject::tr("Unknown error while opening file:\n%1").arg(str));
+ // throw;
+ // return false;
+ // }
+
+#ifdef ENABLE_RS_CHECKER
+ if(settings.getCheckRSWhenOpeningDB())
+ backgroundManager->getRsChecker()->startWorker();
+#endif
+
+
+ return DB_Error::NoError;
+}
+
+DB_Error MeetingSession::closeDB()
+{
+ DEBUG_ENTRY;
+
+ if(!m_Db.db())
+ return DB_Error::DbNotOpen;
+
+#ifdef SEARCHBOX_MODE_ASYNC
+ backgroundManager->abortTrivialTasks();
+#endif
+
+#ifdef ENABLE_BACKGROUND_MANAGER
+ backgroundManager->abortAllTasks();
+#endif
+
+ if(!viewManager->closeEditors())
+ return DB_Error::EditorsStillOpened; //User wants to continue editing
+
+ releaseAllSavepoints();
+
+ finalizeStatements();
+
+
+ //Calls sqlite3_close(), not forcing closing db like sqlite3_close_v2
+ //So in case the database is still used by some background task (returns SQLITE_BUSY)
+ //we abort closing and return. It's like nevere having closed, database is 100% working
+ int rc = m_Db.disconnect();
+ if(rc != SQLITE_OK)
+ {
+ qWarning() << "Err: closing db" << m_Db.error_code() << m_Db.error_msg();
+
+ if(rc == SQLITE_BUSY)
+ {
+ prepareQueryes(); //Try to reprepare queries
+ return DB_Error::DbBusyWhenClosing;
+ }
+ //return false;
+ }
+
+ mJobStorage->clear();
+ mLineStorage->clear();
+
+ //Clear current line
+ getViewManager()->getGraphMgr()->setCurrentLine(0);
+
+#ifdef ENABLE_RS_CHECKER
+ backgroundManager->getRsChecker()->clearModel();
+#endif
+
+ fileName.clear();
+
+ return DB_Error::NoError;
+}
+
+DB_Error MeetingSession::createNewDB(const QString& file)
+{
+ DEBUG_ENTRY;
+ DB_Error err = closeDB();
+ if(err != DB_Error::NoError && err != DB_Error::DbNotOpen)
+ {
+ return err;
+ }
+
+ int result = SQLITE_OK;
+#define CHECK(code) if((code) != SQLITE_OK) qWarning() << __LINE__ << (code) << m_Db.error_code() << m_Db.error_msg()
+
+ result = m_Db.connect(file.toUtf8(), SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
+ CHECK(result);
+
+ //See 'openDB()'
+ m_Db.enable_foreign_keys(true);
+ m_Db.enable_extended_result_codes(true);
+
+ fileName = file;
+
+ result = m_Db.execute("CREATE TABLE rs_models ("
+ "id INTEGER,"
+ "name TEXT,"
+ "suffix TEXT NOT NULL,"
+ "max_speed INTEGER,"
+ "axes INTEGER,"
+ "type INTEGER NOT NULL,"
+ "sub_type INTEGER NOT NULL,"
+ "PRIMARY KEY(id),"
+ "UNIQUE(name,suffix) )");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE rs_owners ("
+ " id INTEGER,"
+ " name TEXT UNIQUE,"
+ " PRIMARY KEY(id) )");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE rs_list ("
+ "id INTEGER,"
+ "model_id INTEGER,"
+ "number INTEGER NOT NULL,"
+ "owner_id INTEGER,"
+
+ "FOREIGN KEY(model_id) REFERENCES rs_models(id) ON DELETE RESTRICT,"
+ "FOREIGN KEY(owner_id) REFERENCES rs_owners(id) ON DELETE RESTRICT,"
+ "PRIMARY KEY(id),"
+ "UNIQUE(model_id,number,owner_id) )");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE stations ("
+ "id INTEGER PRIMARY KEY,"
+ "name TEXT UNIQUE,"
+ "short_name TEXT UNIQUE,"
+ "platforms INTEGER NOT NULL,"
+ "depot_platf INTEGER," //depot can be 0
+ "platf_color INTEGER,"
+ "defplatf_freight INTEGER,"
+ "defplatf_passenger INTEGER )");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE lines ("
+ "id INTEGER PRIMARY KEY,"
+ "name TEXT NOT NULL UNIQUE,"
+ "max_speed INTEGER DEFAULT 120,"
+ "type INTEGER )");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE railways ("
+ "id INTEGER PRIMARY KEY,"
+ "lineId INTEGER NOT NULL,"
+ "stationId INTEGER NOT NULL,"
+ "pos_meters INTEGER,"
+ "direction INTEGER DEFAULT 0,"
+
+ "UNIQUE(lineId, stationId)"
+ "UNIQUE(lineId, pos_meters)"
+ "FOREIGN KEY(lineId) REFERENCES lines(id) ON DELETE CASCADE,"
+ "FOREIGN KEY(stationId) REFERENCES stations(id) ON DELETE RESTRICT)"); //Delete lines but block when deleting stations registered in line
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE jobshifts ("
+ "id INTEGER PRIMARY KEY,"
+ "name TEXT UNIQUE NOT NULL)");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE jobs ("
+ "id INTEGER NOT NULL,"
+ "category INTEGER NOT NULL DEFAULT 0,"
+ "firstStop INTEGER,"
+ "lastStop INTEGER,"
+ "shiftId INTEGER,"
+ "PRIMARY KEY(id),"
+
+ "FOREIGN KEY(firstStop) REFERENCES stops(id) ON DELETE SET NULL,"
+ "FOREIGN KEY(lastStop) REFERENCES stops(id) ON DELETE SET NULL,"
+ "FOREIGN KEY(shiftId) REFERENCES jobshifts(id) )");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE jobsegments ("
+ "id INTEGER,"
+ "jobId INTEGER,"
+ "lineId INTEGER,"
+ "num INTEGER,"
+ "PRIMARY KEY(id),"
+ "FOREIGN KEY(jobId) REFERENCES jobs(id) ON DELETE RESTRICT ON UPDATE CASCADE,"
+ "FOREIGN KEY(lineId) REFERENCES lines(id) ON DELETE RESTRICT,"
+ "UNIQUE(jobId,num))");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE stops ("
+ "id INTEGER PRIMARY KEY,"
+ "jobId INTEGER,"
+ "stationId INTEGER,"
+ "arrival INTEGER,"
+ "departure INTEGER,"
+ "platform INTEGER NOT NULL DEFAULT 1,"
+ "transit INTEGER DEFAULT 0,"
+ "description TEXT,"
+ "segmentId INTEGER,"
+ "otherSegment INTEGER,"
+ "rw_node INTEGER,"
+ "other_rw_node INTEGER,"
+
+ "FOREIGN KEY(otherSegment) REFERENCES jobsegments(id) ON DELETE RESTRICT,"
+ "FOREIGN KEY(segmentId) REFERENCES jobsegments(id) ON DELETE RESTRICT,"
+ "FOREIGN KEY(jobId) REFERENCES jobs(id) ON DELETE RESTRICT ON UPDATE CASCADE,"
+ "FOREIGN KEY(stationId) REFERENCES stations(id) ON DELETE RESTRICT,"
+ "FOREIGN KEY(rw_node) REFERENCES railways(id) ON DELETE RESTRICT,"
+ "FOREIGN KEY(other_rw_node) REFERENCES railways(id) ON DELETE RESTRICT,"
+ "UNIQUE(jobId,arrival),"
+ "UNIQUE(jobId,departure))");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE coupling ("
+ "id INTEGER PRIMARY KEY,"
+ "stopId INTEGER,"
+ "rsId INTEGER,"
+ "operation INTEGER DEFAULT 0,"
+
+ "FOREIGN KEY(stopId) REFERENCES stops(id) ON DELETE CASCADE,"
+ "FOREIGN KEY(rsId) REFERENCES rs_list(id) ON DELETE RESTRICT,"
+ "UNIQUE(stopId,rsId))");
+ CHECK(result);
+
+ //Create also backup tables to save old jobsegments stops and couplings before editing a job and restore them if user cancels the edits.
+ //NOTE: the structure of the table must be the same, remember to update theese if updating jobsegments stops or coupling
+ result = m_Db.execute("CREATE TABLE old_jobsegments ("
+ "id INTEGER,"
+ "jobId INTEGER,"
+ "lineId INTEGER,"
+ "num INTEGER,"
+ "PRIMARY KEY(id),"
+ "FOREIGN KEY(jobId) REFERENCES jobs(id) ON DELETE RESTRICT ON UPDATE CASCADE,"
+ "FOREIGN KEY(lineId) REFERENCES lines(id) ON DELETE RESTRICT,"
+ "UNIQUE(jobId,num) )");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE old_stops ("
+ "id INTEGER PRIMARY KEY,"
+ "jobId INTEGER,"
+ "stationId INTEGER,"
+ "arrival INTEGER,"
+ "departure INTEGER,"
+ "platform INTEGER NOT NULL DEFAULT 1,"
+ "transit INTEGER DEFAULT 0,"
+ "description TEXT,"
+ "segmentId INTEGER,"
+ "otherSegment INTEGER,"
+ "rw_node INTEGER,"
+ "other_rw_node INTEGER,"
+
+ "FOREIGN KEY(otherSegment) REFERENCES old_jobsegments(id) ON DELETE RESTRICT," //NOTE: this must refer to 'old_jobsegments' instead of 'jobsegments'
+ "FOREIGN KEY(segmentId) REFERENCES old_jobsegments(id) ON DELETE RESTRICT," //NOTE: this must refer to 'old_jobsegments' instead of 'jobsegments'
+ "FOREIGN KEY(jobId) REFERENCES jobs(id) ON DELETE RESTRICT ON UPDATE CASCADE,"
+ "FOREIGN KEY(stationId) REFERENCES stations(id) ON DELETE RESTRICT,"
+ "FOREIGN KEY(rw_node) REFERENCES railways(id) ON DELETE RESTRICT,"
+ "FOREIGN KEY(other_rw_node) REFERENCES railways(id) ON DELETE RESTRICT,"
+ "UNIQUE(jobId,arrival),"
+ "UNIQUE(jobId,departure) )");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE old_coupling ("
+ "id INTEGER PRIMARY KEY,"
+ "stopId INTEGER,"
+ "rsId INTEGER,"
+ "operation INTEGER DEFAULT 0,"
+
+ "FOREIGN KEY(stopId) REFERENCES old_stops(id) ON DELETE CASCADE," //NOTE: this must refer to 'old_stops' instead of 'stops'
+ "FOREIGN KEY(rsId) REFERENCES rs_list(id) ON DELETE RESTRICT,"
+ "UNIQUE(stopId,rsId))");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE imported_rs_owners ("
+ "id INTEGER,"
+ "name TEXT,"
+ "import INTEGER,"
+ "new_name TEXT,"
+ "match_existing_id INTEGER,"
+ "sheet_idx INTEGER,"
+ "PRIMARY KEY(id),"
+ "FOREIGN KEY(match_existing_id) REFERENCES rs_owners(id) ON UPDATE RESTRICT ON DELETE RESTRICT)");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE imported_rs_models ("
+ "id INTEGER,"
+ "name TEXT,"
+ "suffix TEXT NOT NULL,"
+ "import INTEGER,"
+ "new_name TEXT,"
+ "match_existing_id INTEGER,"
+ "max_speed INTEGER,"
+ "axes INTEGER,"
+ "type INTEGER,"
+ "sub_type INTEGER,"
+ "PRIMARY KEY(id),"
+ "FOREIGN KEY(match_existing_id) REFERENCES rs_models(id) ON UPDATE RESTRICT ON DELETE RESTRICT)");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE imported_rs_list ("
+ "id INTEGER,"
+ "import INTEGER,"
+ "model_id INTEGER,"
+ "owner_id INTEGER,"
+ "number INTEGER,"
+ "new_number INTEGER,"
+ "PRIMARY KEY(id),"
+ "FOREIGN KEY(model_id) REFERENCES imported_rs_models(id) ON UPDATE RESTRICT ON DELETE RESTRICT,"
+ "FOREIGN KEY(owner_id) REFERENCES imported_rs_owners(id) ON UPDATE RESTRICT ON DELETE RESTRICT)");
+ CHECK(result);
+
+ result = m_Db.execute("CREATE TABLE metadata ("
+ "name TEXT PRIMARY KEY,"
+ "val BLOB)");
+ CHECK(result);
+#undef CHECK
+
+ metaDataMgr->setInt64(FormatVersion, false, MetaDataKey::FormatVersionKey);
+ metaDataMgr->setString(AppVersion, false, MetaDataKey::ApplicationString);
+
+ prepareQueryes();
+
+ return DB_Error::NoError;
+}
+
+/* bool MeetingSession::checkImportRSTablesEmpty()
+ * Check if import_rs_list, import_rs_models, import_rs_owners tables are empty
+ * Theese tables are used during RS importation and are cleared when the process
+ * completes or gets canceled by the user
+ * If they are not empty it might be because the application crashed before clearing theese tables
+*/
+bool MeetingSession::checkImportRSTablesEmpty()
+{
+ query q(m_Db, "SELECT COUNT(1) FROM imported_rs_list");
+ q.step();
+ int count = q.getRows().get(0);
+ if(count)
+ return false;
+
+ q.prepare("SELECT COUNT(1) FROM imported_rs_models");
+ q.step();
+ count = q.getRows().get(0);
+ if(count)
+ return false;
+
+ q.prepare("SELECT COUNT(1) FROM imported_rs_owners");
+ q.step();
+ count = q.getRows().get(0);
+ if(count)
+ return false;
+ return true;
+}
+
+bool MeetingSession::clearImportRSTables()
+{
+ command cmd(m_Db, "DELETE FROM imported_rs_list");
+ if(cmd.execute() != SQLITE_OK)
+ return false;
+
+ cmd.prepare("DELETE FROM imported_rs_models");
+ if(cmd.execute() != SQLITE_OK)
+ return false;
+
+ cmd.prepare("DELETE FROM imported_rs_owners");
+ if(cmd.execute() != SQLITE_OK)
+ return false;
+
+ return true;
+}
+
+void MeetingSession::prepareQueryes()
+{
+ DEBUG_COLOR_ENTRY(SHELL_YELLOW);
+
+ if(q_getPrevStop.prepare("SELECT MAX(prev.departure),"
+ "prev.stationId,"
+ "seg.lineId"
+ " FROM stops prev"
+ " JOIN stops s ON s.jobId=prev.jobId AND prev.departures.departure"
+ " JOIN jobsegments seg ON seg.id=nextS.segmentId"
+ " WHERE s.id=?") != SQLITE_OK)
+ {
+ throw database_error(m_Db);
+ }
+
+ if(q_getKmDirection.prepare("SELECT pos_meters, direction FROM railways WHERE lineId=? AND stationId=?") != SQLITE_OK)
+ {
+ throw database_error(m_Db);
+ }
+
+ viewManager->prepareQueries();
+}
+
+void MeetingSession::finalizeStatements()
+{
+ q_getPrevStop.finish();
+ q_getNextStop.finish();
+
+ q_getKmDirection.finish();
+
+ viewManager->finalizeQueries();
+}
+
+bool MeetingSession::setSavepoint(const QString &pointname)
+{
+ if(!m_Db.db())
+ return false;
+
+ if(savepointList.contains(pointname))
+ return true;
+
+ QString sql = QStringLiteral("SAVEPOINT %1;");
+
+ if(m_Db.execute(sql.arg(pointname).toUtf8()) != SQLITE_OK)
+ {
+ qDebug() << m_Db.error_msg();
+ return false;
+ }
+ savepointList.append(pointname);
+
+ return true;
+}
+
+bool MeetingSession::releaseSavepoint(const QString &pointname)
+{
+ if(!m_Db.db())
+ return false;
+
+ if(!savepointList.contains(pointname))
+ return true;
+
+ QString sql = QStringLiteral("RELEASE %1;");
+
+ if(m_Db.execute(sql.arg(pointname).toUtf8()) != SQLITE_OK)
+ {
+ qDebug() << m_Db.error_msg();
+ return false;
+ }
+
+ int point_index = savepointList.lastIndexOf(pointname);
+ savepointList.erase(savepointList.begin() + point_index, savepointList.end());
+
+ return true;
+}
+
+bool MeetingSession::revertToSavepoint(const QString &pointname)
+{
+ if(!m_Db.db())
+ return false;
+
+ if(!savepointList.contains(pointname))
+ return false;
+
+ QString sql = QStringLiteral("ROLLBACK TO SAVEPOINT %1;");
+
+ if(m_Db.execute(sql.arg(pointname).toUtf8()) != SQLITE_OK)
+ {
+ qDebug() << m_Db.error_msg();
+ return false;
+ }
+
+ int point_index = savepointList.lastIndexOf(pointname);
+ savepointList.erase(savepointList.begin() + point_index, savepointList.end());
+
+ return true;
+}
+
+bool MeetingSession::releaseAllSavepoints()
+{
+ if(!m_Db.db())
+ return false;
+
+ for(const QString& point : qAsConst(savepointList))
+ {
+ if(!releaseSavepoint(point))
+ return false;
+ }
+
+ // When still in a transaction, commit that too
+ if(sqlite3_get_autocommit(m_Db.db()) == 0)
+ m_Db.execute("COMMIT;");
+
+ return true;
+}
+
+bool MeetingSession::revertAll()
+{
+ for(const QString& point : savepointList)
+ {
+ if(!revertToSavepoint(point))
+ return false;
+ }
+ return true;
+}
+
+qreal MeetingSession::getStationGraphPos(db_id lineId, db_id stId, int platf)
+{
+ qreal x = horizOffset;
+
+ query q(Session->m_Db, "SELECT s.id,s.platforms,s.depot_platf FROM railways"
+ " JOIN stations s ON s.id=railways.stationId"
+ " WHERE railways.lineId=? ORDER BY railways.pos_meters ASC");
+ q.bind(1, lineId);
+
+ for(auto station : q)
+ {
+ if(station.get(0) == stId)
+ return x + platf * platformOffset;
+
+ int platfCount = station.get(1);
+ platfCount += station.get(2);
+
+ x += stationOffset + platfCount * platformOffset;
+ }
+ return x;
+}
+
+QColor MeetingSession::colorForCat(JobCategory cat)
+{
+ QColor col = settings.getCategoryColor(int(cat)); //TODO: maybe session-specific
+ if(col.isValid())
+ return col;
+ return QColor(Qt::gray); //Error
+}
+
+bool MeetingSession::getPrevStop(db_id stopId, db_id& prevSt, db_id& lineId)
+{
+ bool ret = false;
+ q_getPrevStop.bind(1, stopId);
+ int rc = q_getPrevStop.step();
+ if(rc == SQLITE_ROW)
+ {
+ //MAX() always return a row but if type is NULL we ignore it
+ auto r = q_getPrevStop.getRows();
+ if(r.column_type(0) != SQLITE_NULL)
+ {
+ prevSt = r.get(1);
+ lineId = r.get(2);
+ ret = true;
+ }
+ else
+ {
+ prevSt = 0;
+ lineId = 0;
+ }
+ }
+ else
+ {
+ qWarning() << m_Db.error_code() << m_Db.error_msg();
+ }
+
+ q_getPrevStop.reset();
+ return ret;
+}
+
+bool MeetingSession::getNextStop(db_id stopId, db_id& nextSt, db_id& lineId)
+{
+ bool ret = false;
+ q_getNextStop.bind(1, stopId);
+ int rc = q_getNextStop.step();
+ if(rc == SQLITE_ROW)
+ {
+ //MAX() always return a row but if type is NULL we ignore it
+ auto r = q_getNextStop.getRows();
+ if(r.column_type(0) != SQLITE_NULL)
+ {
+ nextSt = r.get(1);
+ lineId = r.get(2);
+ ret = true;
+ }
+ else
+ {
+ nextSt = 0;
+ lineId = 0;
+ }
+ }
+ else
+ {
+ qWarning() << m_Db.error_code() << m_Db.error_msg();
+ }
+
+ q_getNextStop.reset();
+ return ret;
+}
+
+Direction MeetingSession::getStopDirection(db_id stopId, db_id stId)
+{
+ db_id otherSt = 0;
+ db_id lineId = 0;
+ bool isNext = false;
+
+ if(!getPrevStop(stopId, otherSt, lineId))
+ {
+ getNextStop(stopId, otherSt, lineId);
+ isNext = true;
+ }
+
+ Direction dir;
+ int kmInMetersA;
+ int kmInMetersB;
+
+ {
+ q_getKmDirection.bind(1, lineId);
+ q_getKmDirection.bind(2, stId);
+ q_getKmDirection.step();
+ auto r = q_getKmDirection.getRows();
+ kmInMetersA = r.get(0);
+ dir = Direction(r.get(1));
+ q_getKmDirection.reset();
+ }
+ {
+ q_getKmDirection.bind(1, lineId);
+ q_getKmDirection.bind(2, otherSt);
+ q_getKmDirection.step();
+ auto r = q_getKmDirection.getRows();
+ kmInMetersB = r.get(0);
+ q_getKmDirection.reset();
+ }
+
+ bool absDir = (kmInMetersA > kmInMetersB);
+
+ bool ret = false;
+
+ if(isNext)
+ ret = !ret;
+
+ if(absDir)
+ ret = !ret;
+
+ if(dir == Direction::Right)
+ ret = !ret;
+
+ return Direction(ret);
+}
+
+void MeetingSession::locateAppdata()
+{
+ appDataPath = QDir::cleanPath(QStringLiteral("%1/%2/%3"))
+ .arg(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation))
+ .arg(AppCompany)
+ .arg(AppDisplayName);
+ qDebug() << appDataPath;
+}
+
+void MeetingSession::loadSettings(const QString& settings_file)
+{
+ DEBUG_ENTRY;
+
+ if(settings_file.isEmpty())
+ settings.loadSettings(appDataPath + QStringLiteral("/traintimetable_settings.ini"));
+ else
+ settings.loadSettings(settings_file);
+
+ hourOffset = settings.getHourOffset();
+ stationOffset = settings.getStationOffset();
+ horizOffset = settings.getHorizontalOffset();
+ vertOffset = settings.getVerticalOffset();
+ platformOffset = settings.getPlatformOffset();
+
+ jobLineWidth = settings.getJobLineWidth();
+}
+
+#ifdef ENABLE_BACKGROUND_MANAGER
+BackgroundManager* MeetingSession::getBackgroundManager() const
+{
+ return backgroundManager.get();
+}
+#endif
diff --git a/src/app/session.h b/src/app/session.h
new file mode 100644
index 0000000..fbb7db8
--- /dev/null
+++ b/src/app/session.h
@@ -0,0 +1,162 @@
+#ifndef MEETINGSESSION_H
+#define MEETINGSESSION_H
+
+#include "utils/types.h"
+#include "utils/directiontype.h"
+
+#include
+using namespace sqlite3pp;
+
+#include
+
+#include
+
+#include
+
+#include
+
+class LineStorage;
+class JobStorage;
+
+class ViewManager;
+class MetaDataManager;
+
+#ifdef ENABLE_BACKGROUND_MANAGER
+class BackgroundManager;
+#endif
+
+enum class DB_Error
+{
+ NoError = 0,
+ GenericError,
+ DbBusyWhenClosing,
+ DbNotOpen,
+ EditorsStillOpened,
+ FormatTooOld,
+ FormatTooNew
+};
+
+//TODO: reorder functions
+class MeetingSession : public QObject
+{
+ Q_OBJECT
+
+private:
+ static MeetingSession* session;
+public:
+ MeetingSession();
+ ~MeetingSession();
+
+ static MeetingSession* Get();
+
+ inline ViewManager* getViewManager() { return viewManager.get(); }
+
+ inline MetaDataManager* getMetaDataManager() { return metaDataMgr.get(); }
+
+#ifdef ENABLE_BACKGROUND_MANAGER
+ BackgroundManager *getBackgroundManager() const;
+#endif
+
+signals:
+ //Shifts
+ void shiftAdded(db_id shiftId);
+ void shiftRemoved(db_id shiftId);
+ void shiftNameChanged(db_id shiftId);
+
+ //A job was added/removed/modified belonging to this shift
+ void shiftJobsChanged(db_id shiftId, db_id jobId);
+
+ //Rollingstock SYNC: wire them from models
+ void rollingstockRemoved(db_id rsId);
+ void rollingStockPlanChanged(db_id rsId);
+ void rollingStockModified(db_id rsId);
+
+ //Jobs
+ void jobChanged(db_id jobId, db_id oldJobId); //Updated id/category/stops
+
+//TODO: old methods, remove them
+public:
+ qreal getStationGraphPos(db_id lineId, db_id stId, int platf = 0);
+
+ bool getPrevStop(db_id stopId, db_id &prevSt, db_id &lineId);
+ bool getNextStop(db_id stopId, db_id &nextSt, db_id &lineId);
+ Direction getStopDirection(db_id stopId, db_id stId);
+
+private:
+ std::unique_ptr viewManager;
+
+ std::unique_ptr metaDataMgr;
+
+#ifdef ENABLE_BACKGROUND_MANAGER
+ std::unique_ptr backgroundManager;
+#endif
+
+public:
+ LineStorage *mLineStorage;
+ JobStorage *mJobStorage;
+
+//Settings TODO: remove
+public:
+ void loadSettings(const QString &settings_file);
+
+ TrainTimetableSettings settings;
+
+ int hourOffset;
+ int stationOffset;
+ qreal platformOffset;
+
+ int horizOffset;
+ int vertOffset;
+
+ int jobLineWidth;
+
+//Queries TODO: remove
+public:
+ database m_Db;
+
+ query q_getPrevStop;
+ query q_getNextStop;
+
+ query q_getKmDirection;
+
+//Categories:
+public:
+ QColor colorForCat(JobCategory cat);
+
+//Savepoints TODO: seem unused
+public:
+ inline bool getDBDirty() { return !savepointList.isEmpty(); }
+
+ bool setSavepoint(const QString& pointname = "RESTOREPOINT");
+ bool releaseSavepoint(const QString& pointname = "RESTOREPOINT");
+ bool revertToSavepoint(const QString& pointname = "RESTOREPOINT");
+ bool releaseAllSavepoints();
+ bool revertAll();
+
+ QStringList savepointList;
+
+//DB
+public:
+ DB_Error createNewDB(const QString &file);
+ DB_Error openDB(const QString& str, bool ignoreVersion);
+ DB_Error closeDB();
+
+ bool checkImportRSTablesEmpty();
+ bool clearImportRSTables();
+
+ void prepareQueryes();
+ void finalizeStatements();
+
+ QString fileName; //TODO: re organize variables
+
+//AppData
+public:
+ static void locateAppdata();
+ static QString appDataPath;
+};
+
+#define Session MeetingSession::Get()
+
+#define AppSettings Session->settings
+
+#endif // MEETINGSESSION_H
diff --git a/src/backgroundmanager/CMakeLists.txt b/src/backgroundmanager/CMakeLists.txt
new file mode 100644
index 0000000..d9467f1
--- /dev/null
+++ b/src/backgroundmanager/CMakeLists.txt
@@ -0,0 +1,6 @@
+set(TRAINTIMETABLE_SOURCES
+ ${TRAINTIMETABLE_SOURCES}
+ backgroundmanager/backgroundmanager.h
+ backgroundmanager/backgroundmanager.cpp
+ PARENT_SCOPE
+)
diff --git a/src/backgroundmanager/backgroundmanager.cpp b/src/backgroundmanager/backgroundmanager.cpp
new file mode 100644
index 0000000..3a84797
--- /dev/null
+++ b/src/backgroundmanager/backgroundmanager.cpp
@@ -0,0 +1,50 @@
+#include "backgroundmanager.h"
+
+#ifdef ENABLE_BACKGROUND_MANAGER
+
+#include "app/session.h"
+
+#include
+#include "rollingstock/rs_checker/rscheckermanager.h"
+
+#include
+
+BackgroundManager::BackgroundManager(QObject *parent) :
+ QObject(parent)
+{
+#ifdef ENABLE_RS_CHECKER
+ rsChecker = new RsCheckerManager(this);
+#endif
+}
+
+BackgroundManager::~BackgroundManager()
+{
+#ifdef ENABLE_RS_CHECKER
+ delete rsChecker;
+ rsChecker = nullptr;
+#endif
+}
+
+void BackgroundManager::abortAllTasks()
+{
+ emit abortTrivialTasks();
+
+#ifdef ENABLE_RS_CHECKER
+ rsChecker->abortTasks();
+#endif
+}
+
+bool BackgroundManager::isRunning()
+{
+ bool running = QThreadPool::globalInstance()->activeThreadCount() > 0;
+ if(running)
+ return true;
+
+#ifdef ENABLE_RS_CHECKER
+ running |= rsChecker->isRunning();
+#endif
+
+ return running;
+}
+
+#endif // ENABLE_BACKGROUND_MANAGER
diff --git a/src/backgroundmanager/backgroundmanager.h b/src/backgroundmanager/backgroundmanager.h
new file mode 100644
index 0000000..6030d38
--- /dev/null
+++ b/src/backgroundmanager/backgroundmanager.h
@@ -0,0 +1,50 @@
+#ifndef BACKGROUNDMANAGER_H
+#define BACKGROUNDMANAGER_H
+
+#ifdef ENABLE_BACKGROUND_MANAGER
+
+#include
+
+#ifdef ENABLE_RS_CHECKER
+class RsCheckerManager;
+#endif
+
+//TODO: show a progress bar for all task like Qt Creator does
+class BackgroundManager : public QObject
+{
+ Q_OBJECT
+public:
+ explicit BackgroundManager(QObject *parent = nullptr);
+ ~BackgroundManager() override;
+
+ void abortAllTasks();
+ bool isRunning();
+
+#ifdef ENABLE_RS_CHECKER
+ inline RsCheckerManager *getRsChecker() const { return rsChecker; };
+#endif // ENABLE_RS_CHECKER
+
+signals:
+ /* abortTrivialTasks() signal
+ *
+ * Stop tasks that are less important like SearchTask
+ * but don't stop long running tasks like RsErrWorker
+ * This function is called when closing current session
+ * (NOTE: opening/creating new session closes the current one)
+ * If user changes his mind and still keeps this session
+ * then it would have to restart background task again
+ *
+ * So stop all trivial tasks and wait the user to confirm his choice
+ * to close this session before stopping all other tasks
+ */
+ void abortTrivialTasks();
+
+private:
+#ifdef ENABLE_RS_CHECKER
+ RsCheckerManager *rsChecker;
+#endif
+};
+
+#endif // ENABLE_BACKGROUND_MANAGER
+
+#endif // BACKGROUNDMANAGER_H
diff --git a/src/db_metadata/CMakeLists.txt b/src/db_metadata/CMakeLists.txt
new file mode 100644
index 0000000..0cbd3c8
--- /dev/null
+++ b/src/db_metadata/CMakeLists.txt
@@ -0,0 +1,17 @@
+set(TRAINTIMETABLE_SOURCES
+ ${TRAINTIMETABLE_SOURCES}
+ db_metadata/imagemetadata.h
+ db_metadata/imagemetadata.cpp
+ db_metadata/meetinginformationdialog.h
+ db_metadata/meetinginformationdialog.cpp
+ db_metadata/metadatamanager.h
+ db_metadata/metadatamanager.cpp
+ PARENT_SCOPE
+)
+
+set(TRAINTIMETABLE_UI_FILES
+ ${TRAINTIMETABLE_UI_FILES}
+ db_metadata/meetinginformationdialog.ui
+ PARENT_SCOPE
+)
+
diff --git a/src/db_metadata/imagemetadata.cpp b/src/db_metadata/imagemetadata.cpp
new file mode 100644
index 0000000..0d7b3d7
--- /dev/null
+++ b/src/db_metadata/imagemetadata.cpp
@@ -0,0 +1,149 @@
+#include "imagemetadata.h"
+
+#include
+
+namespace ImageMetaData
+{
+
+constexpr char sql_get_key_id[] = "SELECT rowid FROM metadata WHERE name=? AND val NOT NULL";
+
+ImageBlobDevice::ImageBlobDevice(sqlite3 *db, qint64 rowId, QObject *parent) :
+ QIODevice(parent),
+ mRowId(rowId),
+ mSize(0),
+ mDb(db),
+ mBlob(nullptr)
+{
+
+}
+
+ImageBlobDevice::~ImageBlobDevice()
+{
+ close();
+}
+
+bool ImageBlobDevice::open(QIODevice::OpenMode mode)
+{
+ mode |= QIODevice::ReadOnly;
+ int rc = sqlite3_blob_open(mDb, "main", "metadata", "val", mRowId, (mode & QIODevice::WriteOnly) != 0, &mBlob);
+ if(rc != SQLITE_OK || !mBlob)
+ {
+ mBlob = nullptr;
+ setErrorString(sqlite3_errmsg(mDb));
+ return false;
+ }
+
+ QIODevice::open(mode);
+
+ mSize = sqlite3_blob_bytes(mBlob);
+
+ return true;
+}
+
+void ImageBlobDevice::close()
+{
+ if(mBlob)
+ {
+ sqlite3_blob_close(mBlob);
+ mBlob = nullptr;
+ mSize = 0;
+ }
+
+ QIODevice::close();
+}
+
+qint64 ImageBlobDevice::size() const
+{
+ return mSize;
+}
+
+qint64 ImageBlobDevice::writeData(const char *data, qint64 len)
+{
+ if(!mBlob)
+ return -1;
+
+ int offset = int(pos());
+ if(len + offset >= mSize)
+ len = mSize - offset;
+
+ if(!len)
+ return -1;
+
+ int rc = sqlite3_blob_write(mBlob, data, int(len), offset);
+ if(rc == SQLITE_OK)
+ return len;
+
+ if(rc == SQLITE_READONLY)
+ return -1;
+
+ setErrorString(sqlite3_errmsg(mDb));
+ return -1;
+}
+
+qint64 ImageBlobDevice::readData(char *data, qint64 maxlen)
+{
+ if(!mBlob)
+ return -1;
+
+ int offset = int(pos());
+ if(maxlen + offset >= mSize)
+ maxlen = mSize - offset;
+
+ if(!maxlen)
+ return -1;
+
+ int rc = sqlite3_blob_read(mBlob, data, int(maxlen), offset);
+ if(rc == SQLITE_OK)
+ return maxlen;
+
+ setErrorString(sqlite3_errmsg(mDb));
+ return -1;
+}
+
+ImageBlobDevice* getImage(sqlite3pp::database& db, const MetaDataManager::Key &key)
+{
+ if(!db.db())
+ return nullptr;
+
+ sqlite3_stmt *stmt = nullptr;
+ int rc = sqlite3_prepare_v2(db.db(), sql_get_key_id, sizeof (sql_get_key_id) - 1, &stmt, nullptr);
+ if(rc != SQLITE_OK)
+ return nullptr;
+
+ rc = sqlite3_bind_text(stmt, 1, key.str, key.len, SQLITE_STATIC);
+ if(rc != SQLITE_OK)
+ {
+ sqlite3_finalize(stmt);
+ return nullptr;
+ }
+
+ rc = sqlite3_step(stmt);
+
+ qint64 rowId = 0;
+ if(rc != SQLITE_ROW)
+ {
+ sqlite3_finalize(stmt);
+ return nullptr;
+ }
+
+ rowId = sqlite3_column_int64(stmt, 0);
+ sqlite3_finalize(stmt);
+
+ if(!rowId)
+ return nullptr;
+
+ return new ImageBlobDevice(db.db(), rowId);
+}
+
+void setImage(sqlite3pp::database& db, const MetaDataManager::Key &key, const void *data, int size)
+{
+ sqlite3pp::command cmd(db, "REPLACE INTO metadata(name, val) VALUES(?, ?)");
+ sqlite3_bind_text(cmd.stmt(), 1, key.str, key.len, SQLITE_STATIC);
+ if(data)
+ sqlite3_bind_blob(cmd.stmt(), 2, data, size, SQLITE_STATIC);
+ else
+ sqlite3_bind_null(cmd.stmt(), 2);
+ cmd.execute();
+}
+
+} // namespace ImageMetaData
diff --git a/src/db_metadata/imagemetadata.h b/src/db_metadata/imagemetadata.h
new file mode 100644
index 0000000..8e12340
--- /dev/null
+++ b/src/db_metadata/imagemetadata.h
@@ -0,0 +1,42 @@
+#ifndef IMAGEBLOBDEVICE_H
+#define IMAGEBLOBDEVICE_H
+
+#include
+
+#include "metadatamanager.h"
+
+typedef struct sqlite3 sqlite3;
+typedef struct sqlite3_blob sqlite3_blob;
+
+namespace ImageMetaData
+{
+
+class ImageBlobDevice : public QIODevice
+{
+ Q_OBJECT
+public:
+ ImageBlobDevice(sqlite3 *db, qint64 rowId, QObject *parent = nullptr);
+ ~ImageBlobDevice() override;
+
+ virtual bool open(OpenMode mode) override;
+ virtual void close() override;
+
+ virtual qint64 size() const override;
+
+protected:
+ virtual qint64 readData(char *data, qint64 maxlen) override;
+ virtual qint64 writeData(const char *data, qint64 len) override;
+
+private:
+ qint64 mRowId;
+ qint64 mSize;
+ sqlite3 *mDb;
+ sqlite3_blob *mBlob;
+};
+
+ImageBlobDevice *getImage(sqlite3pp::database& db, const MetaDataManager::Key& key);
+void setImage(sqlite3pp::database& db, const MetaDataManager::Key &key, const void *data, int size);
+
+} // namespace ImageMetaData
+
+#endif // IMAGEBLOBDEVICE_H
diff --git a/src/db_metadata/meetinginformationdialog.cpp b/src/db_metadata/meetinginformationdialog.cpp
new file mode 100644
index 0000000..29ce2d3
--- /dev/null
+++ b/src/db_metadata/meetinginformationdialog.cpp
@@ -0,0 +1,285 @@
+#include "meetinginformationdialog.h"
+#include "ui_meetinginformationdialog.h"
+
+#include "app/session.h"
+#include "metadatamanager.h"
+
+#include "imagemetadata.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "utils/imageviewer.h"
+
+#include
+
+MeetingInformationDialog::MeetingInformationDialog(QWidget *parent) :
+ QDialog(parent),
+ ui(new Ui::MeetingInformationDialog),
+ needsToSaveImg(false),
+ headerIsNull(false),
+ footerIsNull(false)
+{
+ ui->setupUi(this);
+
+ connect(ui->viewPictureBut, &QPushButton::clicked, this, &MeetingInformationDialog::showImage);
+ connect(ui->importPictureBut, &QPushButton::clicked, this, &MeetingInformationDialog::importImage);
+ connect(ui->removePictureBut, &QPushButton::clicked, this, &MeetingInformationDialog::removeImage);
+ connect(ui->resetHeaderBut, &QPushButton::clicked, this, &MeetingInformationDialog::toggleHeader);
+ connect(ui->resetFooterBut, &QPushButton::clicked, this, &MeetingInformationDialog::toggleFooter);
+ connect(ui->startDate, &QDateEdit::dateChanged, this, &MeetingInformationDialog::updateMinumumDate);
+
+ QSizePolicy sp = ui->headerEdit->sizePolicy();
+ sp.setRetainSizeWhenHidden(true);
+ ui->headerEdit->setSizePolicy(sp);
+ ui->footerEdit->setSizePolicy(sp);
+
+ //Use similar font to the actual font used in sheet export
+ QFont font;
+ font.setBold(true);
+ font.setPointSize(18);
+ ui->descrEdit->document()->setDefaultFont(font);
+
+ if(!loadData())
+ {
+ QMessageBox::warning(this, tr("Database Error"),
+ tr("This database doesn't support metadata.\n"
+ "Make sure it was created by a recent version of the application and was not manipulated."));
+ setDisabled(true);
+ }
+}
+
+MeetingInformationDialog::~MeetingInformationDialog()
+{
+ delete ui;
+}
+
+bool MeetingInformationDialog::loadData()
+{
+ MetaDataManager *meta = Session->getMetaDataManager();
+
+ qint64 tmp = 0;
+ QDate date;
+
+ switch (meta->getInt64(tmp, MetaDataKey::MeetingStartDate))
+ {
+ case MetaDataKey::Result::ValueFound:
+ {
+ date = QDate::fromJulianDay(tmp);
+ break;
+ }
+ case MetaDataKey::Result::NoMetaDataTable:
+ return false; //Database has no well-formed metadata
+ default:
+ date = QDate::currentDate();
+ }
+ ui->startDate->setDate(date);
+
+ switch (meta->getInt64(tmp, MetaDataKey::MeetingEndDate))
+ {
+ case MetaDataKey::Result::ValueFound:
+ {
+ date = QDate::fromJulianDay(tmp);
+ break;
+ }
+ default:
+ date = ui->startDate->date();
+ }
+ ui->endDate->setDate(date);
+
+ qint64 showDates = 1;
+ meta->getInt64(showDates, MetaDataKey::MeetingShowDates);
+ ui->showDatesBox->setChecked(showDates == 1);
+
+ QString text;
+ meta->getString(text, MetaDataKey::MeetingLocation);
+ ui->locationEdit->setText(text.simplified());
+
+ text.clear();
+ meta->getString(text, MetaDataKey::MeetingHostAssociation);
+ ui->associationEdit->setText(text.simplified());
+
+ text.clear();
+ meta->getString(text, MetaDataKey::MeetingDescription);
+ ui->descrEdit->setPlainText(text);
+ //Align all text to center
+ QTextCursor c = ui->descrEdit->textCursor();
+ c.select(QTextCursor::Document);
+ QTextBlockFormat fmt;
+ fmt.setAlignment(Qt::AlignTop | Qt::AlignHCenter);
+ c.mergeBlockFormat(fmt);
+ ui->descrEdit->setTextCursor(c);
+
+ text.clear();
+ headerIsNull = meta->getString(text, MetaDataKey::SheetHeaderText) != MetaDataKey::ValueFound;
+ setSheetText(ui->headerEdit, ui->resetHeaderBut, text, headerIsNull);
+
+ text.clear();
+ footerIsNull = meta->getString(text, MetaDataKey::SheetFooterText) != MetaDataKey::ValueFound;
+ setSheetText(ui->footerEdit, ui->resetFooterBut, text, footerIsNull);
+
+ return true;
+}
+
+void MeetingInformationDialog::setSheetText(QLineEdit *lineEdit, QPushButton *but, const QString& text, bool isNull)
+{
+ lineEdit->setVisible(!isNull);
+
+ if(isNull)
+ {
+ but->setText(tr("Set custom text"));
+ lineEdit->setText(QString());
+ }
+ else
+ {
+ but->setText(tr("Reset"));
+ lineEdit->setText(text.simplified());
+ }
+}
+
+void MeetingInformationDialog::saveData()
+{
+ MetaDataManager *meta = Session->getMetaDataManager();
+
+ meta->setInt64(ui->startDate->date().toJulianDay(), false, MetaDataKey::MeetingStartDate);
+ meta->setInt64(ui->endDate->date().toJulianDay(), false, MetaDataKey::MeetingEndDate);
+ meta->setInt64(ui->showDatesBox->isChecked() ? 1 : 0, false, MetaDataKey::MeetingShowDates);
+
+ meta->setString(ui->locationEdit->text().simplified(), false, MetaDataKey::MeetingLocation);
+ meta->setString(ui->associationEdit->text().simplified(), false, MetaDataKey::MeetingHostAssociation);
+ meta->setString(ui->descrEdit->toPlainText(), false, MetaDataKey::MeetingDescription);
+
+ meta->setString(ui->headerEdit->text().simplified(), headerIsNull, MetaDataKey::SheetHeaderText);
+ meta->setString(ui->footerEdit->text().simplified(), footerIsNull, MetaDataKey::SheetFooterText);
+
+ if(needsToSaveImg)
+ {
+ if(img.isNull())
+ {
+ ImageMetaData::setImage(Session->m_Db, MetaDataKey::MeetingLogoPicture, nullptr, 0);
+ }
+ else
+ {
+ QByteArray arr;
+ QBuffer buf(&arr);
+ buf.open(QIODevice::WriteOnly);
+
+ QImageWriter writer(&buf, "PNG");
+ if(writer.canWrite() && writer.write(img))
+ {
+ ImageMetaData::setImage(Session->m_Db, MetaDataKey::MeetingLogoPicture, arr.data(), arr.size());
+ }else{
+ qDebug() << "MeetingInformationDialog: error saving image," << writer.errorString();
+ }
+ }
+ }
+}
+
+void MeetingInformationDialog::showImage()
+{
+ ImageViewer dlg(this);
+
+ if(img.isNull() && !needsToSaveImg)
+ {
+ std::unique_ptr imageIO;
+ imageIO.reset(ImageMetaData::getImage(Session->m_Db, MetaDataKey::MeetingLogoPicture));
+ if(imageIO && imageIO->open(QIODevice::ReadOnly))
+ {
+ QImageReader reader(imageIO.get());
+ if(reader.canRead())
+ {
+ img = reader.read(); //ERRORMSG: handle errors, show to user
+ }
+
+ if(img.isNull())
+ {
+ qDebug() << "MeetingInformationDialog: error loading image," << reader.errorString();
+ }
+
+ imageIO->close();
+ }else{
+ qDebug() << "MeetingInformationDialog: error query image," << Session->m_Db.error_msg();
+ }
+ }
+
+ dlg.setImage(img);
+
+ dlg.exec();
+
+ if(!needsToSaveImg)
+ img = QImage(); //Cleanup to free memory
+}
+
+void MeetingInformationDialog::importImage()
+{
+ QFileDialog dlg(this, tr("Import image"));
+ dlg.setFileMode(QFileDialog::ExistingFile);
+ dlg.setAcceptMode(QFileDialog::AcceptOpen);
+ dlg.setDirectory(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation));
+
+ QList mimes = QImageReader::supportedMimeTypes();
+ QStringList filters;
+ filters.reserve(mimes.size() + 1);
+ for(const QByteArray &ba : mimes)
+ filters.append(QString::fromUtf8(ba));
+
+ filters << "application/octet-stream"; // will show "All files (*)"
+
+ dlg.setMimeTypeFilters(filters);
+
+ if(dlg.exec() != QDialog::Accepted)
+ return;
+
+ QString fileName = dlg.selectedUrls().value(0).toLocalFile();
+ if(fileName.isEmpty())
+ return;
+
+ QImageReader reader(fileName);
+ reader.setQuality(100);
+ if(reader.canRead())
+ {
+ QImage image = reader.read();
+ if(image.isNull())
+ {
+ QMessageBox::warning(this, tr("Importing error"),
+ tr("The image format is not supported or the file is corrupted."));
+ qDebug() << "MeetingInformationDialog: error importing image," << reader.errorString();
+ return;
+ }
+
+ img = image;
+ needsToSaveImg = true;
+ }
+}
+
+void MeetingInformationDialog::removeImage()
+{
+ int ret = QMessageBox::question(this, tr("Remove image?"),
+ tr("Are you sure to remove the image logo?"));
+ if(ret != QMessageBox::Yes)
+ return;
+
+ img = QImage(); //Cleanup to free memory
+ needsToSaveImg = true;
+}
+
+void MeetingInformationDialog::toggleHeader()
+{
+ headerIsNull = !headerIsNull;
+ setSheetText(ui->headerEdit, ui->resetHeaderBut, QString(), headerIsNull);
+}
+
+void MeetingInformationDialog::toggleFooter()
+{
+ footerIsNull = !footerIsNull;
+ setSheetText(ui->footerEdit, ui->resetFooterBut, QString(), footerIsNull);
+}
+
+void MeetingInformationDialog::updateMinumumDate()
+{
+ ui->endDate->setMinimumDate(ui->startDate->date());
+}
diff --git a/src/db_metadata/meetinginformationdialog.h b/src/db_metadata/meetinginformationdialog.h
new file mode 100644
index 0000000..d7802fb
--- /dev/null
+++ b/src/db_metadata/meetinginformationdialog.h
@@ -0,0 +1,46 @@
+#ifndef MEETINGINFORMATIONDIALOG_H
+#define MEETINGINFORMATIONDIALOG_H
+
+#include
+
+class QLineEdit;
+
+namespace Ui {
+class MeetingInformationDialog;
+}
+
+class MeetingInformationDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit MeetingInformationDialog(QWidget *parent = nullptr);
+ ~MeetingInformationDialog();
+
+public:
+ bool loadData();
+ void saveData();
+
+private slots:
+ void showImage();
+ void importImage();
+ void removeImage();
+
+ void toggleHeader();
+ void toggleFooter();
+
+ void updateMinumumDate();
+
+private:
+ void setSheetText(QLineEdit *lineEdit, QPushButton *but, const QString &text, bool isNull);
+
+private:
+ Ui::MeetingInformationDialog *ui;
+ QImage img;
+
+ bool needsToSaveImg;
+ bool headerIsNull;
+ bool footerIsNull;
+};
+
+#endif // MEETINGINFORMATIONDIALOG_H
diff --git a/src/db_metadata/meetinginformationdialog.ui b/src/db_metadata/meetinginformationdialog.ui
new file mode 100644
index 0000000..8abb3e2
--- /dev/null
+++ b/src/db_metadata/meetinginformationdialog.ui
@@ -0,0 +1,250 @@
+
+
+ MeetingInformationDialog
+
+
+
+ 0
+ 0
+ 556
+ 552
+
+
+
+ Meeting Information
+
+
+ -
+
+
+ Meeting
+
+
+
-
+
+
+ Meeting end day:
+
+
+
+ -
+
+
+ City:
+
+
+
+ -
+
+
+ -
+
+
+ Hosting association:
+
+
+
+ -
+
+
+ -
+
+
+ Meeting start day:
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Picture Logo
+
+
+
-
+
+
+ View picture
+
+
+
+ -
+
+
+ Import Picture
+
+
+
+ -
+
+
+ Remove Picture
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ Sheet export
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 90
+ 0
+
+
+
+ Reset
+
+
+
+ -
+
+
+ Footer text:
+
+
+
+ -
+
+
+ -
+
+
+ Header text:
+
+
+
+ -
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 90
+ 0
+
+
+
+ Reset
+
+
+
+ -
+
+
+ Show meeting dates on first page
+
+
+
+
+
+
+ -
+
+
+ Meeting description
+
+
+
-
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ MeetingInformationDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ MeetingInformationDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/db_metadata/metadatamanager.cpp b/src/db_metadata/metadatamanager.cpp
new file mode 100644
index 0000000..b16b63e
--- /dev/null
+++ b/src/db_metadata/metadatamanager.cpp
@@ -0,0 +1,232 @@
+#include "metadatamanager.h"
+
+#include
+
+constexpr char sql_get_metadata[] = "SELECT val FROM metadata WHERE name=?";
+constexpr char sql_has_metadata_key[] = "SELECT 1 FROM metadata WHERE name=? AND val NOT NULL";
+constexpr char sql_set_metadata[] = "REPLACE INTO metadata(name, val) VALUES(?, ?)";
+
+MetaDataManager::MetaDataManager(sqlite3pp::database &db) :
+ mDb(db)
+{
+
+}
+
+MetaDataKey::Result MetaDataManager::hasKey(const MetaDataManager::Key &key)
+{
+ MetaDataKey::Result result = MetaDataKey::Result::NoMetaDataTable;
+ if(!mDb.db())
+ return result;
+
+ sqlite3_stmt *stmt = nullptr;
+ int rc = sqlite3_prepare_v2(mDb.db(), sql_has_metadata_key, sizeof (sql_has_metadata_key) - 1, &stmt, nullptr);
+ if(rc != SQLITE_OK)
+ return result;
+
+ rc = sqlite3_bind_text(stmt, 1, key.str, key.len, SQLITE_STATIC);
+ if(rc != SQLITE_OK)
+ {
+ sqlite3_finalize(stmt);
+ return result;
+ }
+
+ rc = sqlite3_step(stmt);
+
+ if(rc == SQLITE_ROW)
+ {
+ result = MetaDataKey::Result::ValueFound;
+ }
+ else if(rc == SQLITE_OK || rc == SQLITE_DONE)
+ {
+ result = MetaDataKey::Result::ValueNotFound;
+ }
+ else
+ {
+ result = MetaDataKey::Result::NoMetaDataTable;
+ }
+
+ sqlite3_finalize(stmt);
+ return result;
+}
+
+MetaDataKey::Result MetaDataManager::getInt64(qint64 &out, const Key& key)
+{
+ MetaDataKey::Result result = MetaDataKey::Result::NoMetaDataTable;
+ if(!mDb.db())
+ return result;
+
+ sqlite3_stmt *stmt = nullptr;
+ int rc = sqlite3_prepare_v2(mDb.db(), sql_get_metadata, sizeof (sql_get_metadata) - 1, &stmt, nullptr);
+ if(rc != SQLITE_OK)
+ return result;
+
+ rc = sqlite3_bind_text(stmt, 1, key.str, key.len, SQLITE_STATIC);
+ if(rc != SQLITE_OK)
+ {
+ sqlite3_finalize(stmt);
+ return result;
+ }
+
+ rc = sqlite3_step(stmt);
+
+ if(rc == SQLITE_ROW)
+ {
+ if(sqlite3_column_type(stmt, 0) == SQLITE_NULL)
+ {
+ result = MetaDataKey::Result::ValueIsNull;
+ }
+ else
+ {
+ result = MetaDataKey::Result::ValueFound;
+ out = sqlite3_column_int64(stmt, 0);
+ }
+ }
+ else if(rc == SQLITE_OK || rc == SQLITE_DONE)
+ {
+ result = MetaDataKey::Result::ValueNotFound;
+ }
+ else
+ {
+ result = MetaDataKey::Result::NoMetaDataTable;
+ }
+
+ sqlite3_finalize(stmt);
+ return result;
+}
+
+MetaDataKey::Result MetaDataManager::setInt64(qint64 in, bool setToNull, const Key& key)
+{
+ MetaDataKey::Result result = MetaDataKey::Result::NoMetaDataTable;
+ if(!mDb.db())
+ return result;
+
+ sqlite3_stmt *stmt = nullptr;
+ int rc = sqlite3_prepare_v2(mDb.db(), sql_set_metadata, sizeof (sql_set_metadata) - 1, &stmt, nullptr);
+ if(rc != SQLITE_OK)
+ return result;
+
+ rc = sqlite3_bind_text(stmt, 1, key.str, key.len, SQLITE_STATIC);
+ if(rc != SQLITE_OK)
+ {
+ sqlite3_finalize(stmt);
+ return result;
+ }
+
+ if(setToNull)
+ rc = sqlite3_bind_null(stmt, 2);
+ else
+ rc = sqlite3_bind_int64(stmt, 2, in);
+ if(rc != SQLITE_OK)
+ {
+ sqlite3_finalize(stmt);
+ return result;
+ }
+
+ rc = sqlite3_step(stmt);
+
+ if(rc == SQLITE_OK || rc == SQLITE_DONE)
+ {
+ result = MetaDataKey::Result::ValueFound;
+ }
+ else
+ {
+ result = MetaDataKey::Result::NoMetaDataTable;
+ }
+
+ sqlite3_finalize(stmt);
+ return result;
+}
+
+MetaDataKey::Result MetaDataManager::getString(QString &out, const Key& key)
+{
+ MetaDataKey::Result result = MetaDataKey::Result::NoMetaDataTable;
+ if(!mDb.db())
+ return result;
+
+ sqlite3_stmt *stmt = nullptr;
+ int rc = sqlite3_prepare_v2(mDb.db(), sql_get_metadata, sizeof (sql_get_metadata) - 1, &stmt, nullptr);
+ if(rc != SQLITE_OK)
+ return result;
+
+ rc = sqlite3_bind_text(stmt, 1, key.str, key.len, SQLITE_STATIC);
+ if(rc != SQLITE_OK)
+ {
+ sqlite3_finalize(stmt);
+ return result;
+ }
+
+ rc = sqlite3_step(stmt);
+
+ if(rc == SQLITE_ROW)
+ {
+ if(sqlite3_column_type(stmt, 0) == SQLITE_NULL)
+ {
+ result = MetaDataKey::Result::ValueIsNull;
+ }
+ else
+ {
+ result = MetaDataKey::Result::ValueFound;
+ const int len = sqlite3_column_bytes(stmt, 0);
+ const char *text = reinterpret_cast(sqlite3_column_text(stmt, 0));
+ out = QString::fromUtf8(text, len);
+ }
+ }
+ else if(rc == SQLITE_OK || rc == SQLITE_DONE)
+ {
+ result = MetaDataKey::Result::ValueNotFound;
+ }
+ else
+ {
+ result = MetaDataKey::Result::NoMetaDataTable;
+ }
+
+ sqlite3_finalize(stmt);
+ return result;
+}
+
+MetaDataKey::Result MetaDataManager::setString(const QString& in, bool setToNull, const Key& key)
+{
+ MetaDataKey::Result result = MetaDataKey::Result::NoMetaDataTable;
+ if(!mDb.db())
+ return result;
+
+ sqlite3_stmt *stmt = nullptr;
+ int rc = sqlite3_prepare_v2(mDb.db(), sql_set_metadata, sizeof (sql_set_metadata) - 1, &stmt, nullptr);
+ if(rc != SQLITE_OK)
+ return result;
+
+ rc = sqlite3_bind_text(stmt, 1, key.str, key.len, SQLITE_STATIC);
+ if(rc != SQLITE_OK)
+ {
+ sqlite3_finalize(stmt);
+ return result;
+ }
+
+ QByteArray arr = in.toUtf8();
+
+ if(setToNull)
+ rc = sqlite3_bind_null(stmt, 2);
+ else
+ {
+ rc = sqlite3_bind_text(stmt, 2, arr.data(), arr.size(), SQLITE_STATIC);
+ }
+ if(rc != SQLITE_OK)
+ {
+ sqlite3_finalize(stmt);
+ return result;
+ }
+
+ rc = sqlite3_step(stmt);
+
+ if(rc == SQLITE_OK || rc == SQLITE_DONE)
+ {
+ result = MetaDataKey::Result::ValueFound;
+ }
+ else
+ {
+ result = MetaDataKey::Result::NoMetaDataTable;
+ }
+
+ sqlite3_finalize(stmt);
+ return result;
+}
diff --git a/src/db_metadata/metadatamanager.h b/src/db_metadata/metadatamanager.h
new file mode 100644
index 0000000..4f4a31b
--- /dev/null
+++ b/src/db_metadata/metadatamanager.h
@@ -0,0 +1,74 @@
+#ifndef METADATAMANAGER_H
+#define METADATAMANAGER_H
+
+#include
+#include
+
+namespace sqlite3pp {
+class database;
+}
+
+namespace MetaDataKey
+{
+enum Result
+{
+ ValueFound = 0,
+ ValueIsNull,
+ ValueNotFound,
+ NoMetaDataTable, //Format is too old, 'metadata' table is not present
+
+ NResults
+};
+
+//BEGING Key constants TODO: maybe make static or extern to avoid duplication
+
+//Database
+constexpr char FormatVersionKey[] = "format_version"; //INTEGER: version, NOTE: FormatVersion is aleady used by info.h constants
+constexpr char ApplicationString[] = "application_str"; //STRING: application version string 'maj.min.patch'
+
+//Meeting
+constexpr char MeetingShowDates[] = "meeting_show_dates"; //INTEGER: 1 shows dates, 0 hides them
+constexpr char MeetingStartDate[] = "meeting_start_date"; //INTEGER: Start date in Julian Day integer
+constexpr char MeetingEndDate[] = "meeting_end_date"; //INTEGER: End date in Juliand Day integer
+constexpr char MeetingLocation[] = "meeting_location"; //STRING: city name
+constexpr char MeetingDescription[] = "meeting_descr"; //STRING: brief description of the meeting
+constexpr char MeetingHostAssociation[] = "meeting_host"; //STRING: name of association that is hosting the meeting
+constexpr char MeetingLogoPicture[] = "meeting_logo"; //BLOB: PNG alpha image, usually hosting association logo
+
+//ODT Export Sheet
+constexpr char SheetHeaderText[] = "sheet_header"; //STRING: sheet header text
+constexpr char SheetFooterText[] = "sheet_footer"; //STRING: sheet footer text
+
+//Jobs
+#define METADATA_MAKE_RS_KEY(category) ("job_default_stop_" ## #category)
+
+//END Key constants
+}
+
+class MetaDataManager
+{
+public:
+ MetaDataManager(sqlite3pp::database &db);
+
+ struct Key
+ {
+ template
+ constexpr inline Key(const char (&val)[N]) :str(val), len(N - 1) {}
+
+ const char *str;
+ const int len;
+ };
+
+ MetaDataKey::Result hasKey(const Key& key);
+
+ MetaDataKey::Result getInt64(qint64 &out, const Key& key);
+ MetaDataKey::Result setInt64(qint64 in, bool setToNull, const Key &key);
+
+ MetaDataKey::Result getString(QString &out, const Key &key);
+ MetaDataKey::Result setString(const QString &in, bool setToNull, const Key &key);
+
+private:
+ sqlite3pp::database &mDb;
+};
+
+#endif // METADATAMANAGER_H
diff --git a/src/graph/CMakeLists.txt b/src/graph/CMakeLists.txt
new file mode 100644
index 0000000..ca1d279
--- /dev/null
+++ b/src/graph/CMakeLists.txt
@@ -0,0 +1,16 @@
+set(TRAINTIMETABLE_SOURCES
+ ${TRAINTIMETABLE_SOURCES}
+ graph/backgroundhelper.h
+ graph/backgroundhelper.cpp
+ graph/graphicsscene.h
+ graph/graphicsscene.cpp
+ graph/graphicsview.h
+ graph/graphicsview.cpp
+ graph/graphmanager.h
+ graph/graphmanager.cpp
+ graph/hourpane.h
+ graph/hourpane.cpp
+ graph/stationlayer.h
+ graph/stationlayer.cpp
+ PARENT_SCOPE
+)
diff --git a/src/graph/backgroundhelper.cpp b/src/graph/backgroundhelper.cpp
new file mode 100644
index 0000000..04d2d5c
--- /dev/null
+++ b/src/graph/backgroundhelper.cpp
@@ -0,0 +1,175 @@
+#include "backgroundhelper.h"
+
+#include "app/scopedebug.h"
+
+#include
+#include
+#include
+
+#include
+
+#include
+
+#include "app/session.h"
+
+BackgroundHelper::BackgroundHelper(QObject *parent) :
+ QObject(parent)
+{
+
+}
+
+BackgroundHelper::~BackgroundHelper()
+{
+
+}
+
+void BackgroundHelper::setHourLinePen(const QPen& pen)
+{
+ if(hourLinePen == pen)
+ return;
+
+ hourLinePen = pen;
+ emit updateGraph();
+}
+
+void BackgroundHelper::setHourTextPen(const QPen &pen)
+{
+ if(hourTextPen == pen)
+ return;
+
+ hourTextPen = pen;
+ emit updateGraph();
+}
+
+void BackgroundHelper::setHourTextFont(const QFont& font)
+{
+ if(hourTextFont == font)
+ return;
+
+ hourTextFont = font;
+ emit updateGraph();
+}
+
+void BackgroundHelper::setHourOffset(qreal value)
+{
+ hourOffset = value;
+ emit updateGraph();
+}
+
+void BackgroundHelper::setVertOffset(qreal value)
+{
+ vertOffset = value;
+ emit vertOffsetChanged();
+ emit updateGraph();
+}
+
+qreal BackgroundHelper::getVertOffset() const
+{
+ return vertOffset;
+}
+
+void BackgroundHelper::setHourHorizOffset(qreal value)
+{
+ hourHorizOffset = value;
+ emit horizHorizOffsetChanged();
+ emit updateGraph();
+}
+
+qreal BackgroundHelper::getHourHorizOffset() const
+{
+ return hourHorizOffset;
+}
+
+void BackgroundHelper::drawBackgroundLines(QPainter *painter, const QRectF &rect)
+{
+ const qreal x1 = qMax(qreal(hourHorizOffset), rect.left());
+ const qreal x2 = rect.right();
+ const qreal t = qMax(rect.top(), vertOffset);
+ const qreal b = rect.bottom();
+
+ if(x1 > x2 || b < vertOffset || t > b)
+ return;
+
+ qreal f = std::remainder(t - vertOffset, hourOffset);
+
+ if(f < 0)
+ f += hourOffset;
+ qreal f1 = qFuzzyIsNull(f) ? vertOffset : qMax(t - f + hourOffset, vertOffset);
+
+
+ const qreal l = std::remainder(b - vertOffset, hourOffset);
+ const qreal l1 = b - l;
+
+ std::size_t n = std::size_t((l1 - f1)/hourOffset) + 1;
+
+ QLineF *arr = new QLineF[n];
+ for(std::size_t i = 0; i < n; i++)
+ {
+ arr[i] = QLineF(x1, f1, x2, f1);
+ f1 += hourOffset;
+ }
+
+ painter->setPen(hourLinePen);
+ painter->drawLines(arr, int(n));
+ delete [] arr;
+}
+
+void BackgroundHelper::drawForegroundHours(QPainter *painter, const QRectF &rect, int scroll)
+{
+ painter->setFont(hourTextFont);
+ painter->setPen(hourTextPen);
+
+ //qDebug() << "Drawing hours..." << rect << scroll;
+ const QString fmt(QStringLiteral("%1:00"));
+
+ const qreal top = scroll;
+ const qreal bottom = rect.bottom();
+
+ int h = qFloor(top / hourOffset);
+ qreal y = h * hourOffset - scroll + vertOffset;
+
+ for(; h <= 24 && y <= bottom; h++)
+ {
+ //qDebug() << "Y:" << y << fmt.arg(h);
+ painter->drawText(QPointF(5, y + 8), fmt.arg(h)); //y + 8 to center text vertically
+ y += hourOffset;
+ }
+}
+
+void BackgroundHelper::drawForegroundStationLabels(QPainter *painter, const QRectF &rect, int hScroll, db_id lineId)
+{
+ query q(Session->m_Db, "SELECT s.name,s.short_name,s.platforms,s.depot_platf FROM railways"
+ " JOIN stations s ON s.id=railways.stationId"
+ " WHERE railways.lineId=? ORDER BY railways.pos_meters ASC");
+ q.bind(1, lineId);
+
+ QFont f;
+ f.setBold(true);
+ f.setPointSize(15);
+ painter->setFont(f);
+ painter->setPen(AppSettings.getStationTextColor());
+
+ const qreal platformOffset = Session->platformOffset;
+ const int stationOffset = Session->stationOffset;
+
+ qreal x = Session->horizOffset;
+
+ QRectF r = rect;
+
+ for(auto station : q)
+ {
+ QString stName;
+ if(station.column_bytes(1) == 0)
+ stName = station.get(0); //Fallback to full name
+ else
+ stName = station.get(1);
+
+ int platf = station.get(2);
+ platf += station.get(3);
+
+ r.setLeft(x - hScroll); //Eat width
+ painter->drawText(r, Qt::AlignVCenter, stName);
+
+ x += stationOffset + platf * platformOffset;
+ }
+}
diff --git a/src/graph/backgroundhelper.h b/src/graph/backgroundhelper.h
new file mode 100644
index 0000000..3e7fb1f
--- /dev/null
+++ b/src/graph/backgroundhelper.h
@@ -0,0 +1,50 @@
+#ifndef BACKGROUNDHELPER_H
+#define BACKGROUNDHELPER_H
+
+#include
+
+#include
+#include
+
+#include "utils/types.h"
+
+class BackgroundHelper : public QObject
+{
+ Q_OBJECT
+public:
+ BackgroundHelper(QObject *parent = nullptr);
+ ~BackgroundHelper();
+
+ void setHourLinePen(const QPen &pen);
+ void setHourTextPen(const QPen &pen);
+ void setHourTextFont(const QFont &font);
+
+ void setHourOffset(qreal value);
+
+ void setVertOffset(qreal value);
+ qreal getVertOffset() const;
+
+ void setHourHorizOffset(qreal value);
+ qreal getHourHorizOffset() const;
+
+ void drawBackgroundLines(QPainter *painter, const QRectF& rect);
+ void drawForegroundHours(QPainter *painter, const QRectF& rect, int scroll);
+
+ void drawForegroundStationLabels(QPainter *painter, const QRectF& rect, int hScroll, db_id lineId);
+
+signals:
+ void updateGraph();
+ void vertOffsetChanged();
+ void horizHorizOffsetChanged();
+
+private:
+ qreal hourOffset;
+ qreal vertOffset;
+ qreal hourHorizOffset;
+
+ QFont hourTextFont;
+ QPen hourTextPen;
+ QPen hourLinePen;
+};
+
+#endif // BACKGROUNDHELPER_H
diff --git a/src/graph/graphicsscene.cpp b/src/graph/graphicsscene.cpp
new file mode 100644
index 0000000..62ff003
--- /dev/null
+++ b/src/graph/graphicsscene.cpp
@@ -0,0 +1,22 @@
+#include "graphicsscene.h"
+
+GraphicsScene::GraphicsScene(QObject *parent) : QGraphicsScene(parent)
+{
+
+}
+
+void GraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *e)
+{
+ /* This is needed to clear selection in some nasty cases:
+ * 1 - select a job in Line1 (JobPathEditor opens...)
+ * 2 - change to Line2 (but the selected job is not in Line2)
+ * 3 - JobPathEditor is still open and you want to lose it
+ * 4 - clicking in empty area won't emit 'selectionChanged()' because
+ * the selection was already emty so we need to subclass
+ * and manually emit 'selectionCleared()'
+*/
+ QGraphicsScene::mousePressEvent(e);
+
+ if(selectedItems().isEmpty())
+ emit selectionCleared();
+}
diff --git a/src/graph/graphicsscene.h b/src/graph/graphicsscene.h
new file mode 100644
index 0000000..7b775ec
--- /dev/null
+++ b/src/graph/graphicsscene.h
@@ -0,0 +1,19 @@
+#ifndef GRAPHICSSCENE_H
+#define GRAPHICSSCENE_H
+
+#include
+
+class GraphicsScene : public QGraphicsScene
+{
+ Q_OBJECT
+public:
+ explicit GraphicsScene(QObject *parent = nullptr);
+
+signals:
+ void selectionCleared();
+
+protected:
+ void mousePressEvent(QGraphicsSceneMouseEvent *e);
+};
+
+#endif // GRAPHICSSCENE_H
diff --git a/src/graph/graphicsview.cpp b/src/graph/graphicsview.cpp
new file mode 100644
index 0000000..fa752b9
--- /dev/null
+++ b/src/graph/graphicsview.cpp
@@ -0,0 +1,77 @@
+#include "graphicsview.h"
+
+#include "graphmanager.h"
+#include "backgroundhelper.h"
+
+#include "hourpane.h"
+#include "stationlayer.h"
+
+#include
+
+#include
+
+#include
+#include
+
+GraphicsView::GraphicsView(GraphManager *mgr, QWidget *parent) :
+ QGraphicsView(parent),
+ helper(mgr->getBackGround())
+{
+ //setRenderHint(QPainter::Antialiasing); It blurs background lines with big pen sizes
+ setAlignment(Qt::AlignLeft | Qt::AlignTop);
+
+ hourPane = new HourPane(helper, this);
+ stationLayer = new StationLayer(mgr, this);
+ updateHourHorizOffset();
+ updateStationVertOffset();
+
+ connect(verticalScrollBar(), &QScrollBar::valueChanged, hourPane, &HourPane::setScroll);
+ connect(horizontalScrollBar(), &QScrollBar::valueChanged, stationLayer, &StationLayer::setScroll);
+
+ connect(helper, &BackgroundHelper::horizHorizOffsetChanged, this, &GraphicsView::updateHourHorizOffset);
+ connect(helper, &BackgroundHelper::vertOffsetChanged, this, &GraphicsView::updateStationVertOffset);
+ connect(helper, &BackgroundHelper::updateGraph, this, static_cast(&GraphicsView::update));
+}
+
+void GraphicsView::drawBackground(QPainter *painter, const QRectF &rect)
+{
+ helper->drawBackgroundLines(painter, rect);
+}
+
+void GraphicsView::updateHourHorizOffset()
+{
+ hourPane->resize(int(helper->getHourHorizOffset()) - 5, viewport()->height());
+}
+
+void GraphicsView::updateStationVertOffset()
+{
+ stationLayer->resize(viewport()->width(), int(helper->getVertOffset()) - 5);
+}
+
+void GraphicsView::redrawStationNames()
+{
+ stationLayer->update();
+}
+
+bool GraphicsView::viewportEvent(QEvent *e)
+{
+ switch (e->type())
+ {
+ case QEvent::Resize:
+ {
+ //qDebug() << e << "Resizing HourPane";
+ //qDebug() << "View:" << rect() << "Viewport:" << viewport()->rect();
+ hourPane->resize(hourPane->width(), viewport()->height());
+ hourPane->setScroll(verticalScrollBar()->value());
+
+ stationLayer->resize(viewport()->width(), stationLayer->height());
+ stationLayer->setScroll(horizontalScrollBar()->value());
+
+ break;
+ }
+ default:
+ break;
+ }
+
+ return QGraphicsView::viewportEvent(e);
+}
diff --git a/src/graph/graphicsview.h b/src/graph/graphicsview.h
new file mode 100644
index 0000000..31f1a25
--- /dev/null
+++ b/src/graph/graphicsview.h
@@ -0,0 +1,32 @@
+#ifndef GRAPHICSVIEW_H
+#define GRAPHICSVIEW_H
+
+#include
+
+class HourPane;
+class StationLayer;
+class BackgroundHelper;
+class GraphManager;
+
+class GraphicsView : public QGraphicsView
+{
+public:
+ GraphicsView(GraphManager *mgr, QWidget *parent = nullptr);
+
+ void redrawStationNames();
+
+public slots:
+ void updateStationVertOffset();
+ void updateHourHorizOffset();
+
+protected:
+ void drawBackground(QPainter *painter, const QRectF &rect) override;
+ bool viewportEvent(QEvent *e) override;
+
+private:
+ BackgroundHelper *helper;
+ HourPane *hourPane;
+ StationLayer *stationLayer;
+};
+
+#endif // GRAPHICSVIEW_H
diff --git a/src/graph/graphmanager.cpp b/src/graph/graphmanager.cpp
new file mode 100644
index 0000000..c822d3d
--- /dev/null
+++ b/src/graph/graphmanager.cpp
@@ -0,0 +1,244 @@
+#include "graphmanager.h"
+
+#include "app/session.h"
+#include "viewmanager/viewmanager.h"
+
+#include "backgroundhelper.h"
+#include "graphicsview.h"
+#include "graphicsscene.h"
+#include "utils/model_roles.h"
+
+#include "lines/linestorage.h"
+
+#include "app/scopedebug.h"
+
+#include
+
+GraphManager::GraphManager(QObject *parent) :
+ QObject(parent),
+ backGround(nullptr),
+ curLineId(0),
+ curJobId(0)
+{
+ backGround = new BackgroundHelper(this);
+
+ lineStorage = Session->mLineStorage;
+
+ connect(&AppSettings, &TrainTimetableSettings::jobGraphOptionsChanged, this, &GraphManager::updateGraphOptions);
+ updateGraphOptions();
+
+ view = new GraphicsView(this);
+ view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+ view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+
+ connect(lineStorage, &LineStorage::lineNameChanged, this, &GraphManager::onLineNameChanged);
+ connect(lineStorage, &LineStorage::lineStationsModified, this, &GraphManager::onLineModified);
+ connect(lineStorage, &LineStorage::lineRemoved, this, &GraphManager::onLineRemoved);
+}
+
+GraphManager::~GraphManager()
+{
+
+}
+
+bool GraphManager::setCurrentLine(db_id lineId)
+{
+ if(curLineId == lineId)
+ return true;
+
+ if(lineId < 0)
+ lineId = 0;
+
+ LineStorage *lines = Session->mLineStorage;
+ bool error = false;
+
+ GraphicsScene *scene = static_cast(view->scene());
+ if(scene)
+ {
+ disconnect(scene, &QGraphicsScene::selectionChanged, this, &GraphManager::onSelectionChanged);
+ disconnect(scene, &GraphicsScene::selectionCleared, this, &GraphManager::onSelectionCleared);
+ scene->clearSelection();
+ }
+ view->setScene(nullptr);
+ scene = nullptr;
+ if(curLineId)
+ lines->releaseLine(curLineId);
+
+ if(lineId > 0)
+ {
+ if(lines->increfLine(lineId) && (scene = static_cast(lineStorage->sceneForLine(lineId))))
+ {
+ connect(scene, &QGraphicsScene::selectionChanged, this, &GraphManager::onSelectionChanged);
+ connect(scene, &GraphicsScene::selectionCleared, this, &GraphManager::onSelectionCleared);
+ view->setScene(scene);
+ view->centerOn(0.0, 0.0);
+ }
+ else
+ {
+ error = true;
+ lineId = 0;
+ }
+ }
+
+ view->redrawStationNames();
+ curLineId = lineId;
+ emit currentLineChanged(curLineId);
+ return !error;
+}
+
+void GraphManager::onSelectionChanged()
+{
+ //TODO: single selection. Ctrl+click allow to select multiple items but only the first one is considerated
+ QGraphicsScene *scene = view->scene();
+ if(scene && !scene->selectedItems().isEmpty())
+ {
+ auto sel = scene->selectedItems();
+ QGraphicsItem *item = sel.first();
+ db_id jobId = item->data(JOB_ID_ROLE).toLongLong();
+
+ if(curJobId == jobId)
+ return;
+
+ curJobId = jobId;
+
+ Session->getViewManager()->requestJobEditor(jobId);
+
+ emit jobSelected(jobId);
+ }
+ else
+ {
+ onSelectionCleared();
+ }
+}
+
+void GraphManager::onSelectionCleared()
+{
+ curJobId = 0;
+ Session->getViewManager()->requestClearJob();
+ emit jobSelected(0);
+}
+
+BackgroundHelper *GraphManager::getBackGround() const
+{
+ return backGround;
+}
+
+JobSelection GraphManager::getSelectedJob()
+{
+ if(!view)
+ return {0, 0}; //NULL
+ QGraphicsScene *scene = view->scene();
+ if(!scene)
+ return {0, 0};
+
+ auto selection = scene->selectedItems();
+ if(selection.isEmpty())
+ return {0, 0};
+
+ QGraphicsItem *item = selection.first();
+ db_id jobId = item->data(JOB_ID_ROLE).toLongLong();
+ db_id segmentId = item->data(SEGMENT_ROLE).toLongLong();
+
+ return {jobId, segmentId};
+}
+
+void GraphManager::clearSelection()
+{
+ QGraphicsScene *scene = view->scene();
+ if(scene)
+ {
+ scene->clearSelection();
+ }
+}
+
+/* db_id GraphManager::showFirstLine()
+ * Tries to select first line in Alphabetical order
+ * If it succeds returs its lineId
+ * Otherwise returns 0
+ */
+db_id GraphManager::getFirstLineId()
+{
+ db_id firstLineId = 0;
+
+ if(Session->m_Db.db())
+ {
+ query q(Session->m_Db, "SELECT id,MIN(name) FROM lines");
+ q.step();
+ firstLineId = q.getRows().get(0);
+ }
+
+ return firstLineId;
+}
+
+db_id GraphManager::getCurLineId() const
+{
+ return curLineId;
+}
+
+GraphicsView *GraphManager::getView() const
+{
+ return view;
+}
+
+void GraphManager::updateGraphOptions()
+{
+ //TODO: maybe get rid of theese variables in MeetingSession and always use AppSettings?
+ int hourOffset = AppSettings.getHourOffset();
+ backGround->setHourOffset(hourOffset);
+ Session->hourOffset = hourOffset;
+
+ int horizOffset = AppSettings.getHorizontalOffset();
+ backGround->setHourHorizOffset(AppSettings.getHourLineOffset());
+ Session->horizOffset = horizOffset;
+
+ int vertOffset = AppSettings.getVerticalOffset();
+ backGround->setVertOffset(vertOffset);
+ Session->vertOffset = vertOffset;
+
+ Session->stationOffset = AppSettings.getStationOffset();
+ Session->platformOffset = AppSettings.getPlatformOffset();
+
+ Session->jobLineWidth = AppSettings.getJobLineWidth();
+
+ QPen hourLinePen;
+ hourLinePen.setColor(AppSettings.getHourLineColor());
+ hourLinePen.setWidth(AppSettings.getHourLineWidth());
+ backGround->setHourLinePen(hourLinePen);
+
+ backGround->setHourTextPen(AppSettings.getHourTextColor());
+
+ QFont f;
+ f.setPointSize(11);
+ backGround->setHourTextFont(f);
+}
+
+void GraphManager::onLineNameChanged(db_id lineId)
+{
+ if(lineId == curLineId)
+ {
+ //Emit the signal again
+ //So MainWindow->lineComboSearch (CustomCompletionLineEdit)
+ //updates the text
+ emit currentLineChanged(curLineId);
+ }
+}
+
+void GraphManager::onLineModified(db_id lineId)
+{
+ if(lineId == curLineId)
+ {
+ view->updateStationVertOffset();
+ view->redrawStationNames();
+ }
+}
+
+void GraphManager::onLineRemoved(db_id lineId)
+{
+ if(curLineId == lineId)
+ {
+ //Current line is removed, show another line instead
+ lineId = getFirstLineId();
+ if(lineId)
+ setCurrentLine(lineId);
+ }
+}
diff --git a/src/graph/graphmanager.h b/src/graph/graphmanager.h
new file mode 100644
index 0000000..4d987b5
--- /dev/null
+++ b/src/graph/graphmanager.h
@@ -0,0 +1,65 @@
+#ifndef GRAPHMANAGER_H
+#define GRAPHMANAGER_H
+
+#include
+
+#include "utils/types.h"
+
+class BackgroundHelper;
+class GraphicsView;
+class LineStorage;
+
+typedef struct JobSelection
+{
+ db_id jobId;
+ db_id segmentId;
+} JobSelection;
+
+class GraphManager : public QObject
+{
+ Q_OBJECT
+public:
+ explicit GraphManager(QObject *parent = nullptr);
+ ~GraphManager();
+
+ GraphicsView *getView() const;
+
+ db_id getCurLineId() const;
+
+ BackgroundHelper *getBackGround() const;
+
+ JobSelection getSelectedJob();
+ void clearSelection();
+
+ db_id getFirstLineId();
+
+signals:
+ void currentLineChanged(db_id lineId);
+
+ void jobSelected(db_id jobId);
+
+public slots:
+ bool setCurrentLine(db_id lineId);
+ void onSelectionChanged();
+ void onSelectionCleared();
+
+private slots:
+ void onLineNameChanged(db_id lineId);
+ void onLineModified(db_id lineId);
+ void onLineRemoved(db_id lineId);
+
+ void updateGraphOptions();
+
+public:
+ LineStorage *lineStorage;
+
+private:
+ BackgroundHelper *backGround;
+ GraphicsView *view;
+
+ db_id curLineId;
+
+ db_id curJobId;
+};
+
+#endif // GRAPHMANAGER_H
diff --git a/src/graph/hourpane.cpp b/src/graph/hourpane.cpp
new file mode 100644
index 0000000..c8b75e8
--- /dev/null
+++ b/src/graph/hourpane.cpp
@@ -0,0 +1,29 @@
+#include "hourpane.h"
+
+#include "backgroundhelper.h"
+
+#include
+
+#include
+
+HourPane::HourPane(BackgroundHelper *h, QWidget *parent) :
+ QWidget (parent),
+ helper(h),
+ verticalScroll(0)
+{
+
+}
+
+void HourPane::paintEvent(QPaintEvent *)
+{
+ QPainter p(this);
+ QColor c(255, 255, 255, 220);
+ p.fillRect(rect(), c);
+ helper->drawForegroundHours(&p, rect(), verticalScroll);
+}
+
+void HourPane::setScroll(int value)
+{
+ verticalScroll = value;
+ update();
+}
diff --git a/src/graph/hourpane.h b/src/graph/hourpane.h
new file mode 100644
index 0000000..39f40a4
--- /dev/null
+++ b/src/graph/hourpane.h
@@ -0,0 +1,25 @@
+#ifndef HOURPANE_H
+#define HOURPANE_H
+
+#include
+
+class BackgroundHelper;
+
+class HourPane : public QWidget
+{
+ Q_OBJECT
+public:
+ HourPane(BackgroundHelper *h, QWidget *parent);
+
+public slots:
+ void setScroll(int value);
+
+protected:
+ void paintEvent(QPaintEvent *);
+
+private:
+ BackgroundHelper *helper;
+ int verticalScroll;
+};
+
+#endif // HOURPANE_H
diff --git a/src/graph/stationlayer.cpp b/src/graph/stationlayer.cpp
new file mode 100644
index 0000000..1f1a793
--- /dev/null
+++ b/src/graph/stationlayer.cpp
@@ -0,0 +1,39 @@
+#include "stationlayer.h"
+
+#include
+
+#include "graphmanager.h"
+#include "backgroundhelper.h"
+
+#include "app/session.h"
+#include "lines/linestorage.h"
+
+StationLayer::StationLayer(GraphManager *mgr, QWidget *parent) :
+ QWidget(parent),
+ graphMgr(mgr),
+ horizontalScroll(0)
+{
+ connect(Session->mLineStorage, &LineStorage::stationNameChanged,
+ this, static_cast(&StationLayer::update));
+}
+
+void StationLayer::setScroll(int value)
+{
+ horizontalScroll = value;
+ update();
+}
+
+void StationLayer::paintEvent(QPaintEvent *)
+{
+ QPainter p(this);
+ QColor c(255, 255, 255, 220);
+ p.fillRect(rect(), c);
+
+ db_id lineId = graphMgr->getCurLineId();
+ if(lineId == 0)
+ return; //No line selected
+
+ graphMgr->getBackGround()->drawForegroundStationLabels(&p, rect(),
+ horizontalScroll,
+ lineId);
+}
diff --git a/src/graph/stationlayer.h b/src/graph/stationlayer.h
new file mode 100644
index 0000000..d04c2ff
--- /dev/null
+++ b/src/graph/stationlayer.h
@@ -0,0 +1,25 @@
+#ifndef STATIONLAYER_H
+#define STATIONLAYER_H
+
+#include
+
+class GraphManager;
+
+class StationLayer : public QWidget
+{
+ Q_OBJECT
+public:
+ StationLayer(GraphManager *mgr, QWidget *parent = nullptr);
+
+public slots:
+ void setScroll(int value);
+
+protected:
+ void paintEvent(QPaintEvent *) override;
+
+private:
+ GraphManager *graphMgr;
+ int horizontalScroll;
+};
+
+#endif // STATIONLAYER_H
diff --git a/src/jobs/CMakeLists.txt b/src/jobs/CMakeLists.txt
new file mode 100644
index 0000000..6bc96e4
--- /dev/null
+++ b/src/jobs/CMakeLists.txt
@@ -0,0 +1,19 @@
+add_subdirectory(jobeditor)
+
+set(TRAINTIMETABLE_SOURCES
+ ${TRAINTIMETABLE_SOURCES}
+ jobs/jobssqlmodel.h
+ jobs/jobssqlmodel.cpp
+ jobs/jobstorage.h
+ jobs/jobstorage.cpp
+ jobs/traingraphics.h
+ jobs/traingraphics.cpp
+ jobs/jobsmanager.h
+ jobs/jobsmanager.cpp
+ PARENT_SCOPE
+)
+
+set(TRAINTIMETABLE_UI_FILES
+ ${TRAINTIMETABLE_UI_FILES}
+ PARENT_SCOPE
+)
diff --git a/src/jobs/jobeditor/CMakeLists.txt b/src/jobs/jobeditor/CMakeLists.txt
new file mode 100644
index 0000000..3683752
--- /dev/null
+++ b/src/jobs/jobeditor/CMakeLists.txt
@@ -0,0 +1,26 @@
+add_subdirectory(model)
+add_subdirectory(shiftbusy)
+
+set(TRAINTIMETABLE_SOURCES
+ ${TRAINTIMETABLE_SOURCES}
+
+ jobs/jobeditor/jobpatheditor.h
+ jobs/jobeditor/rscoupledialog.h
+ jobs/jobeditor/stopdelegate.h
+ jobs/jobeditor/stopeditor.h
+ jobs/jobeditor/editstopdialog.h
+
+ jobs/jobeditor/jobpatheditor.cpp
+ jobs/jobeditor/rscoupledialog.cpp
+ jobs/jobeditor/stopdelegate.cpp
+ jobs/jobeditor/stopeditor.cpp
+ jobs/jobeditor/editstopdialog.cpp
+ PARENT_SCOPE
+)
+
+set(TRAINTIMETABLE_UI_FILES
+ ${TRAINTIMETABLE_UI_FILES}
+ jobs/jobeditor/editstopdialog.ui
+ jobs/jobeditor/jobpatheditor.ui
+ PARENT_SCOPE
+)
diff --git a/src/jobs/jobeditor/editstopdialog.cpp b/src/jobs/jobeditor/editstopdialog.cpp
new file mode 100644
index 0000000..861e9a8
--- /dev/null
+++ b/src/jobs/jobeditor/editstopdialog.cpp
@@ -0,0 +1,727 @@
+#include "editstopdialog.h"
+#include "ui_editstopdialog.h"
+
+#include "app/session.h"
+
+#include
+
+#include "app/scopedebug.h"
+
+#include "model/stopmodel.h"
+
+#include "lines/helpers.h"
+#include "utils/jobcategorystrings.h"
+
+#include
+
+#include
+
+#include "utils/platform_utils.h"
+
+#include "rscoupledialog.h"
+#include "model/rscouplinginterface.h"
+
+#include "model/rsproxymodel.h"
+#include "model/stopcouplingmodel.h"
+#include "model/trainassetmodel.h"
+#include "model/jobpassingsmodel.h"
+
+#include "utils/sqldelegate/modelpageswitcher.h"
+#include "utils/sqldelegate/customcompletionlineedit.h"
+
+#include "stations/stationsmatchmodel.h"
+
+EditStopDialog::EditStopDialog(QWidget *parent) :
+ QDialog(parent),
+ ui(new Ui::EditStopDialog),
+ readOnly(false)
+{
+ ui->setupUi(this);
+
+ stationsMatchModel = new StationsMatchModel(Session->m_Db, this);
+ stationLineEdit = new CustomCompletionLineEdit(stationsMatchModel, this);
+ ui->stopBoxLayout->addWidget(stationLineEdit, 0, 1);
+
+ //Coupling
+ couplingMgr = new RSCouplingInterface(Session->m_Db, this);
+
+ coupledModel = new StopCouplingModel(Session->m_Db, this);
+ auto ps = new ModelPageSwitcher(true, this);
+ ps->setModel(coupledModel);
+ ui->coupledView->setModel(coupledModel);
+ ui->coupledLayout->insertWidget(1, ps);
+
+ uncoupledModel = new StopCouplingModel(Session->m_Db, this);
+ ps = new ModelPageSwitcher(true, this);
+ ps->setModel(uncoupledModel);
+ ui->uncoupledView->setModel(uncoupledModel);
+ ui->uncoupledLayout->insertWidget(1, ps);
+
+ ui->coupledView->setContextMenuPolicy(Qt::CustomContextMenu);
+ ui->uncoupledView->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(ui->coupledView, &QAbstractItemView::customContextMenuRequested, this, &EditStopDialog::couplingCustomContextMenuRequested);
+ connect(ui->uncoupledView, &QAbstractItemView::customContextMenuRequested, this, &EditStopDialog::couplingCustomContextMenuRequested);
+
+ //Setup train asset models
+ trainAssetModelBefore = new TrainAssetModel(Session->m_Db, this);
+ ps = new ModelPageSwitcher(true, this);
+ ps->setModel(trainAssetModelBefore);
+ ui->assetBeforeView->setModel(trainAssetModelBefore);
+ ui->trainAssetGridLayout->addWidget(ps, 2, 0);
+
+ trainAssetModelAfter = new TrainAssetModel(Session->m_Db, this);
+ ps = new ModelPageSwitcher(true, this);
+ ps->setModel(trainAssetModelAfter);
+ ui->assetAfterView->setModel(trainAssetModelAfter);
+ ui->trainAssetGridLayout->addWidget(ps, 2, 1);
+
+ ui->assetBeforeView->setContextMenuPolicy(Qt::CustomContextMenu);
+ ui->assetAfterView->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(ui->assetBeforeView, &QAbstractItemView::customContextMenuRequested, this, &EditStopDialog::couplingCustomContextMenuRequested);
+ connect(ui->assetAfterView, &QAbstractItemView::customContextMenuRequested, this, &EditStopDialog::couplingCustomContextMenuRequested);
+
+ //Setup Crossings/Passings
+ passingsModel = new JobPassingsModel(this);
+ ui->passingsView->setModel(passingsModel);
+
+ crossingsModel = new JobPassingsModel(this);
+ ui->crossingsView->setModel(crossingsModel);
+
+ connect(stationLineEdit, &CustomCompletionLineEdit::dataIdChanged, this, &EditStopDialog::onStEditingFinished);
+
+ connect(ui->editCoupledBut, &QPushButton::clicked, this, &EditStopDialog::editCoupled);
+ connect(ui->editUncoupledBut, &QPushButton::clicked, this, &EditStopDialog::editUncoupled);
+
+ connect(ui->calcPassingsBut, &QPushButton::clicked, this, &EditStopDialog::calcPassings);
+
+ connect(ui->calcTimeBut, &QPushButton::clicked, this, &EditStopDialog::calcTime);
+ connect(ui->applyTimeBut, &QPushButton::clicked, this, &EditStopDialog::applyTime);
+
+ connect(ui->platfRadio, &QRadioButton::toggled, this, &EditStopDialog::onPlatfRadioToggled);
+
+ connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+ //BIG TODO: temporarily disable option to Cancel dialog
+ //This is because at the moment it doesn't seem Coupling are canceled
+ //So you get a mixed state: Arrival/Departure/Descriptio ecc changes are canceled but Coupling changes are still applied
+ ui->buttonBox->setStandardButtons(QDialogButtonBox::Ok);
+
+ setReadOnly(true);
+}
+
+EditStopDialog::~EditStopDialog()
+{
+ delete ui;
+}
+
+void EditStopDialog::clearUi()
+{
+ m_stopId = 0;
+ stopIdx = QModelIndex();
+
+ m_jobId = 0;
+
+ m_prevStId = 0;
+
+ trainAssetModelBefore->setStop(0, QTime(), TrainAssetModel::BeforeStop);
+ trainAssetModelAfter->setStop(0, QTime(), TrainAssetModel::AfterStop);
+
+ originalArrival = QTime();
+ originalDeparture = QTime();
+
+ curLine = 0;
+ curSegment = 0;
+
+ //TODO: clear UI properly
+}
+
+void EditStopDialog::showBeforeAsset(bool val)
+{
+ ui->assetBeforeView->setVisible(val);
+ ui->assetBeforeLabel->setVisible(val);
+}
+
+void EditStopDialog::showAfterAsset(bool val)
+{
+ ui->assetAfterView->setVisible(val);
+ ui->assetAfterLabel->setVisible(val);
+}
+
+void EditStopDialog::setStop(StopModel *stops, const QModelIndex& idx)
+{
+ DEBUG_ENTRY;
+
+ if(!idx.isValid())
+ {
+ clearUi();
+ return;
+ }
+
+ stopIdx = idx;
+ stopModel = stops;
+
+ m_jobId = idx.data(JOB_ID_ROLE).toLongLong();
+ m_jobCat = JobCategory(idx.data(JOB_CATEGORY_ROLE).toInt());
+
+ m_stopId = idx.data(STOP_ID).toLongLong();
+ m_stationId = idx.data(STATION_ID).toLongLong();
+
+ stopType = StopType(idx.data(STOP_TYPE_ROLE).toInt());
+
+ if(idx.row() == 0) //First stop
+ {
+ m_prevStId = 0;
+
+ showBeforeAsset(false); //Hide train asset before stop
+ showAfterAsset(true);
+ }
+ else //Not First stop
+ {
+ QModelIndex prevIdx = idx.model()->index(idx.row() - 1, idx.column());
+ previousDep = prevIdx.data(DEP_ROLE).toTime();
+ m_prevStId = prevIdx.data(STATION_ID).toLongLong();
+
+ //Cannot arrive at same time (or before) the previous departure, minimum travel duration of one minute
+ //This reflects StopModel behaviour when editing stops with StopEditor
+ QTime minArrival = previousDep.addSecs(60);
+ ui->arrivalTimeEdit->setMinimumTime(minArrival);
+ ui->calcTimeArr->setMinimumTime(minArrival);
+
+ if(stopType == Normal)
+ minArrival = minArrival.addSecs(60); //At least 1 minute stop for normal stops
+ ui->departureTimeEdit->setMinimumTime(minArrival);
+ ui->calcTimeDep->setMinimumTime(minArrival);
+
+ showBeforeAsset(true);
+
+ if (idx.row() == stopModel->rowCount() - 2) //Last stop (size - 1 - AddHere)
+ {
+ showAfterAsset(false); //Hide train asset after stop
+ }
+ else //A stop in the middle
+ {
+ showAfterAsset(true);
+ }
+ }
+
+
+ const QString jobName = JobCategoryName::jobName(m_jobId, m_jobCat);
+ setWindowTitle(jobName);
+
+ originalArrival = idx.data(ARR_ROLE).toTime();
+ originalDeparture = idx.data(DEP_ROLE).toTime();
+
+ curSegment = idx.data(SEGMENT_ROLE).toLongLong();
+ curLine = idx.data(CUR_LINE_ROLE).toLongLong();
+
+ stationsMatchModel->setFilter(curLine, m_prevStId);
+
+ coupledModel->setStop(m_stopId, RsOp::Coupled);
+ uncoupledModel->setStop(m_stopId, RsOp::Uncoupled);
+
+ trainAssetModelBefore->setStop(m_jobId, originalArrival, TrainAssetModel::BeforeStop);
+ trainAssetModelAfter->setStop(m_jobId, originalArrival, TrainAssetModel::AfterStop);
+
+ updateInfo();
+
+ calcPassings();
+}
+
+void EditStopDialog::updateInfo()
+{
+ ui->arrivalTimeEdit->setTime(originalArrival);
+ ui->departureTimeEdit->setTime(originalDeparture);
+
+ stationLineEdit->setData(m_stationId);
+
+ int platf = stopIdx.data(PLATF_ID).toInt();
+
+ int platfCount = 0;
+ int depotCount = 0;
+ stopModel->getStationPlatfCount(m_stationId, platfCount, depotCount);
+ setPlatformCount(platfCount, depotCount);
+ setPlatform(platf);
+
+ //Show previous station if any
+ if(m_prevStId)
+ {
+ query q(Session->m_Db, "SELECT name FROM stations WHERE id=?");
+ q.bind(1, m_prevStId);
+ q.step();
+ ui->prevStEdit->setText(q.getRows().get(0));
+ }
+
+ const QString descr = stopIdx.data(STOP_DESCR_ROLE).toString();
+
+ ui->descriptionEdit->setPlainText(descr);
+
+ if(stopType == First)
+ {
+ ui->arrivalTimeEdit->setEnabled(false);
+ ui->calcTimeArr->setEnabled(false);
+
+ ui->departureTimeEdit->setEnabled(true);
+ ui->calcTimeDep->setEnabled(true);
+ }
+ else if (stopType == Last || stopType == Transit || stopType == TransitLineChange)
+ {
+ ui->departureTimeEdit->setEnabled(false);
+ ui->calcTimeDep->setEnabled(false);
+
+ ui->arrivalTimeEdit->setEnabled(true);
+ ui->calcTimeArr->setEnabled(true);
+ }
+ else
+ {
+ ui->arrivalTimeEdit->setEnabled(true);
+ ui->calcTimeArr->setEnabled(true);
+ ui->departureTimeEdit->setEnabled(true);
+ ui->calcTimeDep->setEnabled(true);
+ }
+
+ if(stopType == Transit || stopType == TransitLineChange)
+ {
+ //On transit you cannot couple/uncouple rollingstock
+ ui->editCoupledBut->setEnabled(false);
+ ui->editUncoupledBut->setEnabled(false);
+ }
+
+ couplingMgr->loadCouplings(stopModel, m_stopId, m_jobId, originalArrival);
+
+ speedBeforeStop = getTrainSpeedKmH(false);
+
+ updateSpeedAfterStop();
+ //Save original speed after stop to compare it after new coupling operations
+ originalSpeedAfterStopKmH = newSpeedAfterStopKmH; //Initially there is no difference
+
+ updateDistance();
+}
+
+void EditStopDialog::setPlatformCount(int maxMainPlatf, int maxDepots)
+{
+ ui->mainPlatfSpin->setMaximum(maxMainPlatf);
+ ui->depotSpin->setMaximum(maxDepots);
+
+ ui->platfRadio->setEnabled(maxMainPlatf);
+ ui->mainPlatfSpin->setEnabled(maxMainPlatf);
+
+ ui->depotSpin->setEnabled(maxDepots);
+ ui->depotRadio->setEnabled(maxDepots);
+}
+
+void EditStopDialog::setPlatform(int platf)
+{
+ if(platf < 0)
+ {
+ ui->platfRadio->setChecked(false);
+ ui->mainPlatfSpin->setEnabled(false);
+ ui->mainPlatfSpin->setValue(0);
+
+ ui->depotRadio->setChecked(true);
+ ui->depotSpin->setEnabled(true);
+ ui->depotSpin->setValue( - platf); //Negative num --> to positive
+ }
+ else
+ {
+ ui->platfRadio->setChecked(true);
+ ui->mainPlatfSpin->setEnabled(true);
+ ui->mainPlatfSpin->setValue(platf + 1); //DB starts from platf 0 --> change to 1
+
+ ui->depotRadio->setChecked(false);
+ ui->depotSpin->setEnabled(false);
+ ui->depotSpin->setValue(0);
+ }
+}
+
+int EditStopDialog::getPlatform()
+{
+ if(ui->platfRadio->isChecked())
+ return ui->mainPlatfSpin->value() - 1; //Positive from 0
+ else
+ return -ui->depotSpin->value(); //Negative from -1
+}
+
+void EditStopDialog::onStEditingFinished(db_id stationId)
+{
+ DEBUG_ENTRY;
+
+ m_stationId = stationId;
+
+ int platfCount = 0;
+ int depotCount = 0;
+ stopModel->getStationPlatfCount(m_stationId, platfCount, depotCount);
+ setPlatformCount(platfCount, depotCount);
+
+ //Fix platform
+ int platf = stopModel->data(stopIdx, PLATF_ID).toInt();
+ if(platf < 0 && -platf > depotCount)
+ {
+ if(depotCount)
+ platf = -depotCount; //Max depot platform
+ else
+ platf = 0; //If there arent depots, use platform 0 (First main platform)
+ }
+ else if(platf >= platfCount)
+ {
+ if(platfCount)
+ platf = platfCount - 1; //Max main platform
+ else
+ platf = -1; //If there are no main platforms, use -1 (First depot platform)
+ }
+
+ //Update UI for platform
+ setPlatform(platf);
+
+ updateDistance();
+}
+
+void EditStopDialog::saveDataToModel()
+{
+ DEBUG_ENTRY;
+
+ stopModel->setData(stopIdx, m_stationId, STATION_ID);
+
+ int platf = getPlatform();
+ stopModel->setData(stopIdx, platf, PLATF_ID);
+
+ if(ui->descriptionEdit->document()->isModified())
+ {
+ stopModel->setData(stopIdx,
+ ui->descriptionEdit->toPlainText(),
+ STOP_DESCR_ROLE);
+ }
+
+ stopModel->setData(stopIdx, ui->arrivalTimeEdit->time(), ARR_ROLE);
+ stopModel->setData(stopIdx, ui->departureTimeEdit->time(), DEP_ROLE);
+}
+
+void EditStopDialog::updateDistance()
+{
+ DEBUG_ENTRY;
+
+ if(stopType == First)
+ {
+ ui->infoLabel->setText(tr("This is the first stop"));
+ return;
+ }
+
+ query q(Session->m_Db, "SELECT name FROM stations WHERE id=?");
+ q.bind(1, m_prevStId);
+ q.step();
+
+ QString res = tr("Train leaves %1 at %2")
+ .arg(q.getRows().get(0))
+ .arg(previousDep.toString("HH:mm"));
+ if(stopType == Last)
+ res.append(tr("\nThis is the last stop"));
+ ui->infoLabel->setText(res);
+
+ query q_getLineNameAndSpeed(Session->m_Db, "SELECT name,max_speed FROM lines WHERE id=?");
+ q_getLineNameAndSpeed.bind(1, curLine);
+ if(q_getLineNameAndSpeed.step() != SQLITE_ROW)
+ {
+ //Error
+ }
+ auto r = q_getLineNameAndSpeed.getRows();
+ QString lineName = r.get(0);
+ int lineSpeedKmH = r.get(1);
+
+ ui->currentLineText->setText(lineName);
+ ui->lineSpeedBox->setValue(lineSpeedKmH);
+}
+
+void EditStopDialog::editCoupled()
+{
+ coupledModel->clearCache();
+ trainAssetModelAfter->clearCache();
+
+ RSCoupleDialog dlg(couplingMgr, RsOp::Coupled, this);
+ dlg.setWindowTitle(tr("Couple"));
+ dlg.loadProxyModels(Session->m_Db, m_jobId, m_stopId, m_stationId, ui->arrivalTimeEdit->time());
+
+ dlg.exec();
+
+ coupledModel->refreshData();
+ trainAssetModelAfter->refreshData();
+ updateSpeedAfterStop();
+}
+
+
+void EditStopDialog::editUncoupled()
+{
+ uncoupledModel->clearCache();
+ trainAssetModelAfter->clearCache();
+
+ RSCoupleDialog dlg(couplingMgr, RsOp::Uncoupled, this);
+ dlg.setWindowTitle(tr("Uncouple"));
+ dlg.loadProxyModels(Session->m_Db, m_jobId, m_stopId, m_stationId, originalArrival);
+
+ dlg.exec();
+
+ uncoupledModel->refreshData();
+ trainAssetModelAfter->refreshData();
+ updateSpeedAfterStop();
+}
+
+bool EditStopDialog::hasEngineAfterStop()
+{
+ DEBUG_ENTRY;
+ return couplingMgr->hasEngineAfterStop();
+}
+
+void EditStopDialog::calcPassings()
+{
+ DEBUG_ENTRY;
+
+ Direction myDirection = Session->getStopDirection(m_stopId, m_stationId);
+
+ query q(Session->m_Db, "SELECT s.id, s.jobid, j.category, s.arrival, s.departure, s.platform"
+ " FROM stops s"
+ " JOIN jobs j ON j.id=s.jobId"
+ " WHERE s.stationId=? AND s.departure >=? AND s.arrival<=? AND s.jobId <> ?");
+
+ q.bind(1, m_stationId);
+ q.bind(2, originalArrival);
+ q.bind(3, originalDeparture);
+ q.bind(4, m_jobId);
+
+ QVector passings, crossings;
+
+ for(auto r : q)
+ {
+ JobPassingsModel::Entry e;
+
+ db_id otherStopId = r.get(0);
+ e.jobId = r.get(1);
+ e.category = JobCategory(r.get(2));
+ e.arrival = r.get(3);
+ e.departure = r.get(4);
+ e.platform = r.get(5);
+
+ Direction otherDir = Session->getStopDirection(otherStopId, m_stationId);
+
+ if(myDirection == otherDir)
+ passings.append(e); //Same direction -> Passing
+ else
+ crossings.append(e); //Opposite direction -> Crossing
+ }
+
+ q.reset();
+
+ passingsModel->setJobs(passings);
+ crossingsModel->setJobs(crossings);
+
+ ui->passingsView->resizeColumnsToContents();
+ ui->crossingsView->resizeColumnsToContents();
+}
+
+void EditStopDialog::couplingCustomContextMenuRequested(const QPoint& pos)
+{
+ QMenu menu(this);
+ QAction *act = menu.addAction(tr("Refresh"));
+
+ //HACK: could be ui->coupledView or ui->uncoupledView or ui->assetBeforeView or ui->assetAfterView
+ QAbstractItemView *view = qobject_cast(sender());
+ if(!view)
+ return; //Error: not called by the view?
+
+ if(menu.exec(view->viewport()->mapToGlobal(pos)) != act)
+ return; //User didn't select 'Refresh' action
+
+ //Refresh data
+ coupledModel->refreshData();
+ coupledModel->clearCache();
+
+ uncoupledModel->refreshData();
+ uncoupledModel->clearCache();
+
+ trainAssetModelBefore->refreshData();
+ trainAssetModelBefore->clearCache();
+
+ trainAssetModelAfter->refreshData();
+ trainAssetModelAfter->clearCache();
+}
+
+QSet EditStopDialog::getRsToUpdate() const
+{
+ return rsToUpdate; //TODO: fill when coupling/uncoupling/canceling operations
+}
+
+int EditStopDialog::getTrainSpeedKmH(bool afterStop)
+{
+ query q(Session->m_Db, "SELECT MIN(rs_models.max_speed), rsId FROM("
+ "SELECT coupling.rsId AS rsId, MAX(stops.arrival)"
+ " FROM stops"
+ " JOIN coupling ON coupling.stopId=stops.id"
+ " WHERE stops.jobId=? AND stops.arrival"
+ " GROUP BY coupling.rsId"
+ " HAVING coupling.operation=1)"
+ " JOIN rs_list ON rs_list.id=rsId"
+ " JOIN rs_models ON rs_models.id=rs_list.model_id");
+ q.bind(1, m_jobId); //TODO: maybe move to model
+
+ //HACK: 1 minute is the min interval between stops,
+ //by adding 1 minute we include the current stop but leave out the next one
+ if(afterStop)
+ q.bind(2, originalArrival.addSecs(60));
+ else
+ q.bind(2, originalArrival);
+
+ q.step();
+ return q.getRows().get(0);
+}
+
+void EditStopDialog::updateSpeedAfterStop()
+{
+ newSpeedAfterStopKmH = getTrainSpeedKmH(true);
+ ui->trainSpeedBox->setValue(newSpeedAfterStopKmH);
+
+ const int lineSpeedKmH = ui->lineSpeedBox->value();
+ const int maxSpeedKmH = qMin(newSpeedAfterStopKmH, lineSpeedKmH);
+
+ ui->speedEdit->setValue(maxSpeedKmH);
+}
+
+void EditStopDialog::calcTime()
+{
+ DEBUG_IMPORTANT_ENTRY;
+
+ double maxSpeedKmH = ui->speedEdit->value();
+
+ if(maxSpeedKmH == 0.0)
+ {
+ qDebug() << "Err!";
+ return;
+ }
+
+ const double distanceMeters = lines::getStationsDistanceInMeters(Session->m_Db, curLine, m_stationId, m_prevStId);
+
+ QTime time = previousDep;
+
+ const double secs = (distanceMeters / maxSpeedKmH) * 3.6;
+ time = time.addSecs(qCeil(secs));
+ qDebug() << "Arrival:" << time;
+
+ ui->calcTimeArr->setTime(time);
+
+ QTime curArr = ui->arrivalTimeEdit->time();
+ QTime curDep = ui->departureTimeEdit->time();
+
+ time = time.addSecs(curArr.secsTo(curDep));
+ ui->calcTimeDep->setTime(time);
+}
+
+void EditStopDialog::applyTime()
+{
+ ui->arrivalTimeEdit->setTime(ui->calcTimeArr->time());
+ ui->departureTimeEdit->setTime(ui->calcTimeDep->time());
+}
+
+void EditStopDialog::setReadOnly(bool value)
+{
+ readOnly = value;
+
+ stationLineEdit->setReadOnly(readOnly);
+ ui->arrivalTimeEdit->setReadOnly(readOnly);
+ ui->departureTimeEdit->setReadOnly(readOnly);
+
+ ui->platfRadio->setEnabled(!readOnly);
+ ui->depotRadio->setEnabled(!readOnly);
+ ui->mainPlatfSpin->setReadOnly(readOnly);
+ ui->depotSpin->setReadOnly(readOnly);
+
+ ui->descriptionEdit->setReadOnly(readOnly);
+
+ ui->editCoupledBut->setEnabled(!readOnly);
+ ui->editUncoupledBut->setEnabled(!readOnly);
+
+ ui->applyTimeBut->setEnabled(!readOnly);
+}
+
+void EditStopDialog::onPlatfRadioToggled(bool enable)
+{
+ DEBUG_ENTRY;
+ ui->mainPlatfSpin->setEnabled(enable);
+ ui->depotSpin->setDisabled(enable);
+}
+
+void EditStopDialog::done(int val)
+{
+ if(val == QDialog::Accepted)
+ {
+ if(stopIdx.row() < stopModel->rowCount() - 2)
+ {
+ //We are not last stop
+
+ //Check if train has at least one engine after this stop
+ //But not if we are Last stop (size - 1 - AddHere)
+ //because the train doesn't have to leave the station
+ bool electricOnNonElectrifiedLine = false;
+ if(!couplingMgr->hasEngineAfterStop(&electricOnNonElectrifiedLine) || electricOnNonElectrifiedLine)
+ {
+ int ret = QMessageBox::warning(this,
+ tr("No Engine Left"),
+ electricOnNonElectrifiedLine ?
+ tr("It seems you have uncoupled all job engines except for electric ones "
+ "but the line is not electrified\n"
+ "(The train isn't able to move)\n"
+ "Do you want to couple a non electric engine?") :
+ tr("It seems you have uncoupled all job engines\n"
+ "(The train isn't able to move)\n"
+ "Do you want to couple an engine?"),
+ QMessageBox::Yes | QMessageBox::No);
+
+ if(ret == QMessageBox::Yes)
+ {
+ return; //Second chance to edit couplings
+ }
+ }
+
+#ifdef ENABLE_AUTO_TIME_RECALC
+ if(originalSpeedAfterStop != newSpeedAfterStop)
+ {
+ int speedBefore = originalSpeedAfterStop;
+ int speedAfter = newSpeedAfterStop;
+
+ LinesModel *linesModel = stopModel->getLinesModel();
+ db_id lineId = curLine ? curLine : stopIdx.data(NEXT_LINE_ROLE).toLongLong();
+ int lineSpeed = linesModel->getLineSpeed(lineId);
+ if(speedBefore == 0)
+ {
+ //If speed is null (likely because there weren't RS coupled before)
+ //Fall back to line max speed
+ speedBefore = lineSpeed;
+ }
+ if(speedAfter == 0)
+ {
+ //If speed is null (likely because there isn't RS coupled after this stop)
+ //Fall back to line max speed
+ speedAfter = lineSpeed;
+ }
+ int ret = QMessageBox::question(this,
+ tr("Train Speed Changed"),
+ tr("Train speed after this stop has changed from a value of %1 km/h to %2 km/h\n"
+ "Do you want to rebase travel times to this new speed?\n"
+ "NOTE: this doesn't affect stop times but you will lose manual adjustments to travel times")
+ .arg(speedBefore).arg(speedAfter),
+ QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Yes);
+
+ if(ret == QMessageBox::Cancel)
+ {
+ return; //Second chance to edit couplings
+ }
+
+ if(ret == QMessageBox::Yes)
+ {
+ stopModel->rebaseTimesToSpeed(stopIdx.row(), ui->arrivalTimeEdit->time(), ui->departureTimeEdit->time());
+ }
+ }
+#endif
+ }
+
+ saveDataToModel();
+ }
+
+ QDialog::done(val);
+}
diff --git a/src/jobs/jobeditor/editstopdialog.h b/src/jobs/jobeditor/editstopdialog.h
new file mode 100644
index 0000000..6812c59
--- /dev/null
+++ b/src/jobs/jobeditor/editstopdialog.h
@@ -0,0 +1,119 @@
+#ifndef EDITSTOPDIALOG2_H
+#define EDITSTOPDIALOG2_H
+
+#include
+#include
+#include
+#include
+
+#include "utils/types.h"
+
+class CustomCompletionLineEdit;
+class StopModel;
+class TrainAssetModel;
+class RSCouplingInterface;
+class JobPassingsModel;
+class StopCouplingModel;
+class StationsMatchModel;
+
+namespace Ui {
+class EditStopDialog;
+}
+
+class EditStopDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ EditStopDialog(QWidget *parent = nullptr);
+ ~EditStopDialog() override;
+
+ void updateInfo();
+ void onStEditingFinished(db_id stationId);
+
+ void updateSpeedAfterStop();
+
+ bool hasEngineAfterStop();
+
+ QSet getRsToUpdate() const;
+
+ void setStop(StopModel *stops, const QModelIndex &idx);
+
+ void clearUi();
+
+ void setReadOnly(bool value);
+
+public slots:
+ void done(int val) override;
+
+ void editCoupled();
+ void editUncoupled();
+
+ void calcTime();
+ void applyTime();
+
+ void onPlatfRadioToggled(bool enable);
+
+ void calcPassings();
+
+ void couplingCustomContextMenuRequested(const QPoint &pos);
+
+private:
+ void saveDataToModel();
+ void updateDistance();
+
+ void showBeforeAsset(bool val);
+ void showAfterAsset(bool val);
+
+ void setPlatformCount(int maxMainPlatf, int maxDepots);
+ void setPlatform(int platf);
+ int getPlatform();
+
+ int getTrainSpeedKmH(bool afterStop);
+
+private:
+ Ui::EditStopDialog *ui;
+ CustomCompletionLineEdit *stationLineEdit;
+ StationsMatchModel *stationsMatchModel;
+
+ db_id m_jobId;
+ db_id m_stopId;
+
+ db_id m_stationId;
+ db_id m_prevStId;
+
+ db_id curSegment;
+ db_id curLine;
+
+ StopModel *stopModel;
+ QPersistentModelIndex stopIdx;
+
+ StopType stopType;
+ JobCategory m_jobCat;
+
+ QTime previousDep;
+
+ QTime originalArrival;
+ QTime originalDeparture;
+
+ int speedBeforeStop;
+ int originalSpeedAfterStopKmH;
+ int newSpeedAfterStopKmH;
+
+ QSet rsToUpdate;
+
+ RSCouplingInterface *couplingMgr;
+
+ StopCouplingModel *coupledModel;
+ StopCouplingModel *uncoupledModel;
+
+ TrainAssetModel *trainAssetModelBefore;
+ TrainAssetModel *trainAssetModelAfter;
+
+ JobPassingsModel *passingsModel;
+ JobPassingsModel *crossingsModel;
+
+ bool readOnly;
+};
+
+#endif // EDITSTOPDIALOG2_H
diff --git a/src/jobs/jobeditor/editstopdialog.ui b/src/jobs/jobeditor/editstopdialog.ui
new file mode 100644
index 0000000..76954ee
--- /dev/null
+++ b/src/jobs/jobeditor/editstopdialog.ui
@@ -0,0 +1,548 @@
+
+
+ EditStopDialog
+
+
+
+ 0
+ 0
+ 550
+ 512
+
+
+
+
+
+
+
+ 4
+
+
+ 4
+
+
+ 4
+
+
+ 4
+
+ -
+
+
+ 0
+
+
+
+ Stop
+
+
+
+ 9
+
+
-
+
+
+ Time
+
+
+
-
+
+
+ -
+
+
+ Arrival
+
+
+
+ -
+
+
+ Station
+
+
+
+ -
+
+
+ Departure
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+ Coupled
+
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ -
+
+
+ Edit
+
+
+
+
+
+
+ -
+
+
+ Uncoupled
+
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+ -
+
+
+ Edit
+
+
+
+
+
+
+ -
+
+
+ Platform
+
+
+
-
+
+
+ Main
+
+
+
+ -
+
+
+ Depot
+
+
+
+ -
+
+
+ 1
+
+
+
+ -
+
+
+ 1
+
+
+
+
+
+
+ -
+
+
+ Description
+
+
+
+ 0
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
+ 3
+
+
-
+
+
+
+
+
+
+
+
+
+ Time
+
+
+ -
+
+
+ Arrival
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Previous Station
+
+
+
+ -
+
+
+ Line Max.Speed
+
+
+
+ -
+
+
+ Speed
+
+
+
+ -
+
+
+ Train Max.Speed
+
+
+
+ -
+
+
+ Departure
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ Re-Calc
+
+
+
+ -
+
+
+ Apply
+
+
+
+ -
+
+
+ Km/h
+
+
+ 999
+
+
+
+ -
+
+
+ true
+
+
+ QAbstractSpinBox::NoButtons
+
+
+ Km/h
+
+
+ 999
+
+
+
+ -
+
+
+ false
+
+
+ true
+
+
+ true
+
+
+ QAbstractSpinBox::NoButtons
+
+
+ false
+
+
+ Km/h
+
+
+ 999
+
+
+
+ -
+
+
+ Line
+
+
+
+ -
+
+
+ true
+
+
+
+
+
+
+
+ Info
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ QFrame::NoFrame
+
+
+ Qt::ScrollBarAsNeeded
+
+
+ Qt::ScrollBarAsNeeded
+
+
+ QAbstractScrollArea::AdjustToContents
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 515
+ 553
+
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 30
+
+
+
+ Infos here...
+
+
+
+ -
+
+
+ Rollingstock
+
+
+
-
+
+
+
+ 10
+ 75
+ true
+
+
+
+ Asset After Stop
+
+
+
+ -
+
+
+
+ 10
+ 75
+ true
+
+
+
+ Asset Before Stop
+
+
+
+ -
+
+
+
+ 0
+ 100
+
+
+
+
+ 12
+ 50
+ false
+
+
+
+
+ -
+
+
+
+ 0
+ 100
+
+
+
+
+ 12
+
+
+
+
+
+
+
+ -
+
+
+
+ 10
+
+
+
+ Passings / Crossings
+
+
+
-
+
+
+ Refresh
+
+
+
+ -
+
+
+ Passings
+
+
+
+ -
+
+
+ -
+
+
+ Crossings
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
diff --git a/src/jobs/jobeditor/jobpatheditor.cpp b/src/jobs/jobeditor/jobpatheditor.cpp
new file mode 100644
index 0000000..45d0490
--- /dev/null
+++ b/src/jobs/jobeditor/jobpatheditor.cpp
@@ -0,0 +1,770 @@
+#include "jobpatheditor.h"
+#include "ui_jobpatheditor.h"
+
+#include "app/session.h"
+#include "app/scopedebug.h"
+
+#include "viewmanager/viewmanager.h"
+
+#include
+#include
+
+#include
+
+#include
+
+#include "model/stopmodel.h"
+#include "stopdelegate.h"
+
+#include "jobs/jobeditor/editstopdialog.h"
+
+#include "lines/linestorage.h"
+
+#include "jobs/jobstorage.h"
+#include "utils/jobcategorystrings.h"
+
+#include "shiftbusy/shiftbusydialog.h"
+#include "shiftbusy/shiftbusymodel.h"
+
+#include "odt_export/jobsheetexport.h"
+
+#ifdef ENABLE_RS_CHECKER
+#include "backgroundmanager/backgroundmanager.h"
+#include "rollingstock/rs_checker/rscheckermanager.h"
+#endif
+
+#include "utils/file_format_names.h"
+
+#include "utils/sqldelegate/customcompletionlineedit.h"
+#include "shifts/shiftcombomodel.h"
+
+JobPathEditor::JobPathEditor(QWidget *parent) :
+ QDialog(parent),
+ ui(new Ui::JobPathEditor),
+ stopModel(nullptr),
+ isClear(true),
+ canSetJob(true),
+ m_readOnly(false)
+{
+ ui->setupUi(this);
+
+ QStringList catNames;
+ catNames.reserve(int(JobCategory::NCategories));
+ for(int cat = 0; cat < int(JobCategory::NCategories); cat++)
+ {
+ catNames.append(JobCategoryName::tr(JobCategoryFullNameTable[cat]));
+ }
+
+ ui->categoryCombo->addItems(catNames);
+ ui->categoryCombo->setEditable(false);
+ ui->categoryCombo->setCurrentIndex(-1);
+
+ ShiftComboModel *shiftComboModel = new ShiftComboModel(Session->m_Db, this);
+ shiftCustomCombo = new CustomCompletionLineEdit(shiftComboModel);
+ ui->verticalLayout->insertWidget(ui->verticalLayout->indexOf(ui->categoryCombo) + 1, shiftCustomCombo);
+
+ stopModel = new StopModel(Session->m_Db, this);
+ connect(shiftCustomCombo, &CustomCompletionLineEdit::dataIdChanged, stopModel, &StopModel::setNewShiftId);
+ ui->view->setModel(stopModel);
+
+ delegate = new StopDelegate(Session->m_Db, this);
+ ui->view->setItemDelegateForColumn(0, delegate);
+
+ ui->view->setResizeMode(QListView::Adjust);
+ ui->view->setMovement(QListView::Static);
+ ui->view->setSelectionMode(QListView::ContiguousSelection);
+
+ connect(ui->categoryCombo, static_cast(&QComboBox::activated), stopModel, &StopModel::setCategory);
+ connect(stopModel, &StopModel::categoryChanged, this, &JobPathEditor::onCategoryChanged);
+
+ connect(ui->jobIdSpin, static_cast(&QSpinBox::valueChanged), this, &JobPathEditor::onIdSpinValueChanged);
+ connect(stopModel, &StopModel::jobIdChanged, this, &JobPathEditor::onJobIdChanged);
+
+ connect(stopModel, &StopModel::edited, this, &JobPathEditor::setEdited);
+
+ connect(stopModel, &StopModel::errorSetShiftWithoutStops, this, &JobPathEditor::onShiftError);
+ connect(stopModel, &StopModel::jobShiftChanged, this, &JobPathEditor::onJobShiftChanged);
+
+ connect(ui->sheetBut, &QPushButton::clicked, this, &JobPathEditor::onSaveSheet);
+
+ ui->view->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(ui->view, &QListView::customContextMenuRequested, this, &JobPathEditor::showContextMenu);
+ connect(ui->view, &QListView::clicked, this, &JobPathEditor::onIndexClicked);
+
+ //Connect to stationsModel to update station views
+ //NOTE: here we use queued connections to avoid freezing the UI if there are many stations to update
+ // with queued connections they are update one at a time and the UI stays responsive
+ connect(this, &JobPathEditor::stationChange, Session->mLineStorage, &LineStorage::stationPlanChanged, Qt::QueuedConnection);
+
+ connect(Session->mJobStorage, &JobStorage::aboutToRemoveJob, this, &JobPathEditor::onJobRemoved);
+ connect(&AppSettings, &TrainTimetableSettings::jobColorsChanged, this, &JobPathEditor::updateSpinColor);
+
+ setReadOnly(false);
+ setEdited(false);
+
+ setMinimumWidth(340);
+}
+
+JobPathEditor::~JobPathEditor()
+{
+ clearJob();
+
+ delete ui;
+}
+
+bool JobPathEditor::setJob(db_id jobId)
+{
+ if(!canSetJob)
+ return false; //We are busy - (Avoid nested loop calls from inside 'saveChanges()')
+
+ if(!isClear && stopModel->getJobId() == jobId)
+ return true; //Fake return, we already set this job
+
+ if(isEdited())
+ {
+ if(!maybeSave())
+ return false; //User still wants to edit the current job
+ }
+
+ return setJob_internal(jobId);
+}
+
+bool JobPathEditor::setJob_internal(db_id jobId)
+{
+ DEBUG_IMPORTANT_ENTRY;
+
+ if(!canSetJob)
+ return false; //We are busy - (Avoid nested loop calls from inside 'saveChanges()')
+
+ if(!isClear && stopModel->getJobId() == jobId)
+ return true; //Fake return, we already set this job
+
+ isClear = false;
+
+ stopModel->loadJobStops(jobId); //Load from database
+
+ //If read-only hide 'AddHere' row (last one)
+ ui->view->setRowHidden(stopModel->rowCount() - 1, m_readOnly);
+
+ return true;
+}
+
+bool JobPathEditor::createNewJob(db_id *out)
+{
+ /* Creates a new job and fills UI
+ *
+ * A newly created job is always invalid because you haven't
+ * added stops to it yet, so it isn't shown in any line graph.
+ * Therefore if it is'n shown you can't delete it or edit it because
+ * you cannot click it.
+ *
+ * To avoid leaving invalid jobs in the database without user knowing it
+ * we remove automatically the job if we cannot open JobPathEditor
+ * or if user 'Cancel' the JobPathEditor
+ * or if user doesn't add at least 2 stops to the job
+ */
+
+ if(out)
+ *out = 0;
+
+ if(!clearJob())
+ return false; //Busy JobPathEditor
+
+ JobStorage *jobs = Session->mJobStorage;
+
+ db_id jobId = 0;
+ jobs->addJob(&jobId); //Request add job
+
+ if(jobId == 0)
+ {
+ return false; //An error occurred in database, abort
+ }
+
+ if(!setJob_internal(jobId))
+ {
+ //If we fail opening JobPathEditor remove the job
+ //Call directly to the model.
+ jobs->removeJob(jobId);
+ return false;
+ }
+
+ if(out)
+ *out = jobId;
+
+
+ return true;
+}
+
+void JobPathEditor::toggleTransit(const QModelIndex& index)
+{
+ DEBUG_ENTRY;
+
+ if(m_readOnly)
+ return;
+
+ StopType type = StopDelegate::getStopType(index);
+ if(type == First || type == Last)
+ return;
+
+ if(type == Transit || type == TransitLineChange)
+ {
+ type = Normal;
+ }
+ else
+ {
+ db_id nextLineId = index.data(NEXT_LINE_ROLE).toLongLong();
+ if (nextLineId != 0) {
+ type = TransitLineChange;
+ }
+ else
+ {
+ type = Transit;
+ }
+ }
+
+ int err = stopModel->setStopType(index, type);
+
+ if(err == StopModel::ErrorTransitWithCouplings)
+ {
+ QMessageBox::warning(this,
+ tr("Invalid Operation"),
+ tr("Transit cannot have coupling or uncoupling operations.\n"
+ "Remove theese operation to set Transit on this stop."));
+ }
+}
+
+void JobPathEditor::showContextMenu(const QPoint& pos)
+{
+ QModelIndex index = ui->view->indexAt(pos);
+ if(!index.isValid() || stopModel->isAddHere(index))
+ return;
+
+ QMenu menu(this);
+ QAction *toggleTransitAct = menu.addAction(tr("Toggle transit"));
+ QAction *setToTransitAct = menu.addAction(tr("Set transit"));
+ QAction *unsetTransit = menu.addAction(tr("Unset transit"));
+ QAction *removeStopAct = menu.addAction(tr("Remove"));
+ QAction *insertBeforeAct = menu.addAction(tr("Insert before"));
+ QAction *editStopAct = menu.addAction(tr("Edit stop"));
+
+ toggleTransitAct->setEnabled(!m_readOnly);
+ setToTransitAct->setEnabled(!m_readOnly);
+ unsetTransit->setEnabled(!m_readOnly);
+ removeStopAct->setEnabled(!m_readOnly);
+ insertBeforeAct->setEnabled(!m_readOnly);
+
+ QAction *act = menu.exec(ui->view->viewport()->mapToGlobal(pos));
+
+ QItemSelectionModel *sm = ui->view->selectionModel();
+
+ QItemSelectionRange range;
+ QItemSelection s = ui->view->selectionModel()->selection();
+ if(s.count() > 0)
+ {
+ //Take the first range only
+ range = s.at(0); //Save range for later
+ }
+
+ //Select only 1 index
+ sm->select(index, QItemSelectionModel::ClearAndSelect);
+
+ if(act == editStopAct)
+ {
+ EditStopDialog dlg(this);
+ dlg.setReadOnly(m_readOnly);
+ dlg.setStop(stopModel, index);
+ dlg.exec();
+ return;
+ }
+
+ if(m_readOnly)
+ return;
+
+ if(range.isValid())
+ {
+ StopType type = StopType::ToggleType;
+ bool useRange = true;
+ if(act == toggleTransitAct)
+ type = StopType::ToggleType;
+ else if(act == setToTransitAct)
+ type = StopType::Transit;
+ else if(act == unsetTransit)
+ type = StopType::Normal;
+ else
+ useRange = false;
+
+ if(useRange)
+ {
+ stopModel->setStopTypeRange(range.top(), range.bottom(), type);
+ //Select only the range we changed (unselect possible other indexes)
+ sm->select(QItemSelection(range.topLeft(), range.bottomRight()), QItemSelectionModel::ClearAndSelect);
+ return;
+ }
+ }
+ else if(act == toggleTransitAct)
+ {
+ toggleTransit(index);
+ return;
+ }
+
+ if(act == removeStopAct)
+ {
+ stopModel->removeStop(index);
+ }
+ else if(act == insertBeforeAct)
+ {
+ stopModel->insertStopBefore(index);
+ }
+}
+
+bool JobPathEditor::clearJob()
+{
+ DEBUG_ENTRY;
+
+ if(!canSetJob)
+ return false;
+
+ if(isEdited())
+ {
+ if(!maybeSave())
+ return false;
+ }
+
+ isClear = true;
+
+ //Reset color
+ ui->jobIdSpin->setPalette(QPalette());
+
+ stopModel->clearJob();
+
+ return true;
+}
+
+void JobPathEditor::prepareQueries()
+{
+ stopModel->prepareQueries();
+}
+
+void JobPathEditor::finalizeQueries()
+{
+ stopModel->finalizeQueries();
+}
+
+void JobPathEditor::done(int res)
+{
+ if(res == Accepted)
+ {
+ //Accepted: save changes
+ if(!saveChanges())
+ return; //Give user a second chance to edit job
+ }
+ else
+ {
+ //Rejected: discard changes
+ discardChanges();
+ }
+
+ //NOTE: if we call QDialog::done() the dialog is closed and QDockWidget remains open but empty
+ setResult(res);
+ if(res == QDialog::Accepted)
+ emit accepted();
+ else if(res == QDialog::Rejected)
+ emit rejected();
+ emit finished(res);
+}
+
+bool JobPathEditor::saveChanges()
+{
+ DEBUG_IMPORTANT_ENTRY;
+
+ if(!canSetJob)
+ return false;
+ canSetJob = false;
+
+ closeStopEditor();
+
+ stopModel->removeLastIfEmpty();
+ stopModel->uncoupleStillCoupledAtLastStop();
+
+ if(stopModel->rowCount() < 3) //At least 2 stops + AddHere
+ {
+ int res = QMessageBox::warning(this,
+ tr("Error"),
+ tr("You must register at least 2 stops.\n"
+ "Do you want to delete this job?"),
+ QMessageBox::Yes | QMessageBox::No);
+
+ if(res == QMessageBox::Yes)
+ {
+ qDebug() << "User wants to delete job:" << stopModel->getJobId();
+ stopModel->commitChanges();
+ Session->mJobStorage->removeJob(stopModel->getJobId());
+ canSetJob = true;
+ clearJob();
+ setEnabled(false);
+ return true;
+ }
+ canSetJob = true;
+
+ return false; //Give user a second chance
+ }
+
+ //Re-check shift because user may have added stops so this job could last longer!
+ if(stopModel->getNewShiftId())
+ {
+ auto times = stopModel->getFirstLastTimes();
+
+ ShiftBusyModel model(Session->m_Db);
+ model.loadData(stopModel->getNewShiftId(),
+ stopModel->getJobId(),
+ times.first, times.second);
+ if(model.hasConcurrentJobs())
+ {
+ ShiftBusyDlg dlg(this);
+ dlg.setModel(&model);
+ dlg.exec();
+
+ stopModel->setNewShiftId(stopModel->getJobShiftId());
+
+ canSetJob = true;
+ return false;
+ }
+ }
+
+ JobStorage *jobs = Session->mJobStorage;
+ jobs->updateFirstLast(stopModel->getJobId());
+
+ //Update views
+ Session->getViewManager()->updateRSPlans(stopModel->getRsToUpdate());
+
+#ifdef ENABLE_RS_CHECKER
+ //Check RS for errors
+ if(AppSettings.getCheckRSOnJobEdit())
+ Session->getBackgroundManager()->getRsChecker()->checkRs(stopModel->getRsToUpdate());
+#endif
+
+ //Update station views
+ auto stations = stopModel->getStationsToUpdate();
+ for(db_id stId : stations)
+ {
+ emit stationChange(stId);
+ }
+
+ stopModel->commitChanges();
+
+ jobs->updateJobPath(stopModel->getJobId());
+
+ //When updating the path selection gets cleared so we restore it
+ Session->getViewManager()->requestJobSelection(stopModel->getJobId(), true, true);
+
+ canSetJob = true;
+ return true;
+}
+
+void JobPathEditor::discardChanges()
+{
+ DEBUG_ENTRY;
+
+ if(!canSetJob)
+ return;
+
+ canSetJob = false;
+
+ closeStopEditor(); //Close before rolling savepoint
+
+ //Save them before reverting changes
+ QSet rsToUpdate = stopModel->getRsToUpdate();
+ QSet stToUpdate = stopModel->getStationsToUpdate();
+
+ stopModel->revertChanges(); //Re-load old job from db
+
+ //After re-load but before possible 'clearJob()' (Below)
+ //Because this hides 'AddHere' so after 'loadJobStops()'
+ //Before 'clearJob()' because sets isEdited = false and
+ //'maybeSave()' doesn't be called in an infinite loop
+
+ canSetJob = true;
+
+ if(stopModel->rowCount() < 3) //At least 2 stops + AddHere
+ {
+ //User discarded an invalid job so we delete it
+ //This usually happens when you create a new job but then you change your mind and press 'Discard'
+ qDebug() << "User wants to delete job:" << stopModel->getJobId();
+ stopModel->commitChanges();
+ Session->mJobStorage->removeJob(stopModel->getJobId());
+ clearJob();
+ setEnabled(false);
+ }
+
+ //After possible job deletion update views
+
+ //Update RS views
+ Session->getViewManager()->updateRSPlans(rsToUpdate);
+
+#ifdef ENABLE_RS_CHECKER
+ //Check RS for errors
+ if(AppSettings.getCheckRSOnJobEdit())
+ Session->getBackgroundManager()->getRsChecker()->checkRs(rsToUpdate);
+#endif
+
+ //Update station views
+ for(db_id stId : stToUpdate)
+ {
+ emit stationChange(stId);
+ }
+}
+
+db_id JobPathEditor::currentJobId() const
+{
+ return stopModel->getJobId();
+}
+
+bool JobPathEditor::maybeSave()
+{
+ DEBUG_ENTRY;
+ QMessageBox::StandardButton ret = QMessageBox::question(this,
+ tr("Save?"),
+ tr("Do you want to save changes to job %1")
+ .arg(stopModel->getJobId()),
+ QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
+ switch (ret)
+ {
+ case QMessageBox::Yes:
+ return saveChanges();
+ case QMessageBox::No:
+ discardChanges();
+ return true;
+ default:
+ break;
+ }
+ return false;
+}
+
+void JobPathEditor::updateSpinColor()
+{
+ if(!isClear)
+ {
+ QColor col = Session->colorForCat(stopModel->getCategory());
+ setSpinColor(col);
+ }
+}
+
+void JobPathEditor::onJobRemoved(db_id jobId)
+{
+ //If the job shown is about to be removed clear JobPathEditor
+ if(stopModel->getJobId() == jobId)
+ {
+ if(clearJob())
+ setEnabled(false);
+ }
+}
+
+void JobPathEditor::onIdSpinValueChanged(int jobId)
+{
+ if(!stopModel->setNewJobId(jobId))
+ {
+ QMessageBox::warning(this, tr("Invalid"),
+ tr("Job number %1 is already exists.
"
+ "Please choose a different number.").arg(jobId));
+ }
+}
+
+void JobPathEditor::onJobIdChanged(db_id jobId)
+{
+ ui->jobIdSpin->setValue(int(jobId));
+}
+
+void JobPathEditor::setSpinColor(const QColor& col)
+{
+ QPalette pal = ui->jobIdSpin->palette();
+ pal.setColor(QPalette::Text, col);
+ ui->jobIdSpin->setPalette(pal);
+}
+
+void JobPathEditor::onCategoryChanged(int newCat)
+{
+ ui->categoryCombo->setCurrentIndex(newCat);
+ updateSpinColor();
+}
+
+void JobPathEditor::onJobShiftChanged(db_id shiftId)
+{
+ shiftCustomCombo->setData(shiftId);
+
+ if(shiftId)
+ {
+ auto times = stopModel->getFirstLastTimes();
+
+ ShiftBusyModel model(Session->m_Db);
+ model.loadData(shiftId,
+ stopModel->getJobId(),
+ times.first, times.second);
+ if(model.hasConcurrentJobs())
+ {
+ ShiftBusyDlg dlg(this);
+ dlg.setModel(&model);
+ dlg.exec();
+
+ stopModel->setNewShiftId(0);
+
+ return;
+ }
+ }
+}
+
+void JobPathEditor::onShiftError()
+{
+ QMessageBox::warning(this,
+ tr("Empty Job"),
+ tr("Before setting a shift you should add stops to this job"),
+ QMessageBox::Ok);
+}
+
+bool JobPathEditor::isEdited() const
+{
+ return stopModel->isEdited();
+}
+
+void JobPathEditor::setEdited(bool val)
+{
+ ui->buttonBox->setEnabled(val);
+ ui->sheetBut->setEnabled(!val);
+}
+
+void JobPathEditor::setReadOnly(bool readOnly)
+{
+ if(m_readOnly == readOnly)
+ return;
+
+ m_readOnly = readOnly;
+
+ ui->jobIdSpin->setReadOnly(m_readOnly);
+ ui->categoryCombo->setDisabled(m_readOnly);
+ shiftCustomCombo->setDisabled(m_readOnly);
+
+ ui->buttonBox->setVisible(!m_readOnly);
+
+ //If read-only hide 'AddHere' row (last one)
+ int size = stopModel->rowCount();
+ if(size > 0)
+ ui->view->setRowHidden(size - 1, m_readOnly);
+
+ if(m_readOnly)
+ {
+ ui->view->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ }
+ else
+ {
+ ui->view->setEditTriggers(QAbstractItemView::DoubleClicked);
+ }
+}
+
+void JobPathEditor::onSaveSheet()
+{
+ QFileDialog dlg(this, tr("Save Job Sheet"));
+ dlg.setFileMode(QFileDialog::AnyFile);
+ dlg.setAcceptMode(QFileDialog::AcceptSave);
+ dlg.setDirectory(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
+ dlg.selectFile(tr("job%1_sheet.odt").arg(stopModel->getJobId()));
+
+ QStringList filters;
+ filters << FileFormats::tr(FileFormats::odtFormat);
+ dlg.setNameFilters(filters);
+
+ if(dlg.exec() != QDialog::Accepted)
+ return;
+
+ QString fileName = dlg.selectedUrls().value(0).toLocalFile();
+
+ if(fileName.isEmpty())
+ return;
+
+ JobSheetExport sheet(stopModel->getJobId(), stopModel->getCategory());
+ sheet.write();
+ sheet.save(fileName);
+}
+
+void JobPathEditor::onIndexClicked(const QModelIndex& index)
+{
+ DEBUG_ENTRY;
+
+ if(m_readOnly)
+ return;
+
+ int addHere = index.data(ADDHERE_ROLE).toInt();
+ if(addHere == 1)
+ {
+ qDebug() << index << "AddHere";
+
+ stopModel->addStop();
+
+ int row = index.row();
+ if(row > 0 && AppSettings.getChooseLineOnAddStop())
+ {
+ //idx - 1 is former Last Stop (now it became a normal Stop)
+ //idx is new Last Stop (former AddHere)
+ //idx + 1 is the new AddHere
+
+ //Edit former Last Stop
+ QModelIndex prev = stopModel->index(row - 1, 0);
+ ui->view->setCurrentIndex(prev);
+ ui->view->scrollTo(prev);
+ ui->view->edit(prev);
+
+ //Tell editor to popup lines combo
+ //QAbstractItemView::edit doesn't let you pass additional arguments
+ //So we work around by emitting a signal
+ //See 'StopDelegate::createEditor()'
+ delegate->popupEditorLinesCombo();
+ }
+ else
+ {
+ QModelIndex lastStop = stopModel->index(row, 0);
+ ui->view->setCurrentIndex(lastStop);
+ ui->view->scrollTo(lastStop);
+ ui->view->edit(lastStop);
+ }
+ }
+}
+
+bool JobPathEditor::getCanSetJob() const
+{
+ return canSetJob;
+}
+
+void JobPathEditor::closeStopEditor()
+{
+ QModelIndex idx = ui->view->currentIndex();
+ QWidget *ed = ui->view->indexWidget(idx);
+ if(ed == nullptr)
+ return;
+ delegate->commitData(ed);
+ delegate->closeEditor(ed);
+}
+
+void JobPathEditor::closeEvent(QCloseEvent *e)
+{
+ //TODO: prevent QDockWidget closing even if we ignore this event
+ if(isEdited())
+ {
+ if(maybeSave())
+ e->accept();
+ else
+ e->ignore();
+ }
+ else
+ {
+ e->accept();
+ }
+}
+
+void JobPathEditor::selectStop(db_id stopId)
+{
+ int row = stopModel->getStopRow(stopId);
+ if(row >= 0)
+ {
+ QModelIndex idx = stopModel->index(row, 0);
+ ui->view->setCurrentIndex(idx);
+ ui->view->scrollTo(idx, QListView::EnsureVisible);
+ }
+}
diff --git a/src/jobs/jobeditor/jobpatheditor.h b/src/jobs/jobeditor/jobpatheditor.h
new file mode 100644
index 0000000..d11a15f
--- /dev/null
+++ b/src/jobs/jobeditor/jobpatheditor.h
@@ -0,0 +1,103 @@
+#ifndef JOBPATHEDITOR_H
+#define JOBPATHEDITOR_H
+
+#include
+
+#include
+
+#include
+
+#include "utils/types.h"
+
+class StopDelegate;
+class CustomCompletionLineEdit;
+
+class StopModel;
+
+namespace Ui {
+class JobPathEditor;
+}
+
+class JobPathEditor : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit JobPathEditor(QWidget *parent = nullptr);
+ ~JobPathEditor()override;
+
+ void prepareQueries();
+ void finalizeQueries();
+
+ bool setJob(db_id jobId);
+ bool createNewJob(db_id *out = nullptr);
+
+ bool clearJob();
+
+ bool saveChanges();
+
+ void discardChanges();
+
+ db_id currentJobId() const;
+
+ bool isEdited() const;
+
+ bool maybeSave();
+
+ void toggleTransit(const QModelIndex &index);
+ void closeStopEditor();
+ void setReadOnly(bool readOnly);
+
+ bool getCanSetJob() const;
+
+ void selectStop(db_id stopId);
+
+signals:
+ void stationChange(db_id stId);
+
+public slots:
+ void done(int) override;
+
+ void onShiftError();
+ void onSaveSheet();
+
+ void updateSpinColor();
+
+protected:
+ virtual void closeEvent(QCloseEvent *e) override;
+
+private slots:
+ void setEdited(bool val);
+
+ void showContextMenu(const QPoint &pos);
+ void onIndexClicked(const QModelIndex &index);
+ void onJobRemoved(db_id jobId);
+
+ void onIdSpinValueChanged(int jobId);
+ void onJobIdChanged(db_id jobId);
+
+ void onCategoryChanged(int newCat);
+
+ void onJobShiftChanged(db_id shiftId);
+
+private:
+ void setSpinColor(const QColor &col);
+
+ bool setJob_internal(db_id jobId);
+
+private:
+ Ui::JobPathEditor *ui;
+
+ CustomCompletionLineEdit *shiftCustomCombo;
+
+ StopModel *stopModel;
+ StopDelegate *delegate;
+
+ //TODO: there are too many bools
+ bool isClear;
+
+ bool canSetJob; //TODO: better name
+ bool m_readOnly;
+};
+
+#endif // JOBPATHEDITOR_H
diff --git a/src/jobs/jobeditor/jobpatheditor.ui b/src/jobs/jobeditor/jobpatheditor.ui
new file mode 100644
index 0000000..2e9d0ff
--- /dev/null
+++ b/src/jobs/jobeditor/jobpatheditor.ui
@@ -0,0 +1,109 @@
+
+
+ JobPathEditor
+
+
+
+ 0
+ 0
+ 320
+ 603
+
+
+
+
+
+
+
+ 9
+
+ -
+
+
+
+ 12
+ 75
+ true
+
+
+
+ Qt::AlignCenter
+
+
+ 99999
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 12
+
+
+
+
+ -
+
+
+ -
+
+
+ Save Sheet
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ JobPathEditor
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ JobPathEditor
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/jobs/jobeditor/model/CMakeLists.txt b/src/jobs/jobeditor/model/CMakeLists.txt
new file mode 100644
index 0000000..6d8d187
--- /dev/null
+++ b/src/jobs/jobeditor/model/CMakeLists.txt
@@ -0,0 +1,24 @@
+set(TRAINTIMETABLE_SOURCES
+ ${TRAINTIMETABLE_SOURCES}
+
+ jobs/jobeditor/model/jobpassingsmodel.h
+ jobs/jobeditor/model/rscouplinginterface.h
+ jobs/jobeditor/model/rslistondemandmodel.h
+ jobs/jobeditor/model/rslistondemandmodelresultevent.h
+ jobs/jobeditor/model/rsproxymodel.h
+ jobs/jobeditor/model/stationlineslistmodel.h
+ jobs/jobeditor/model/stopcouplingmodel.h
+ jobs/jobeditor/model/trainassetmodel.h
+ jobs/jobeditor/model/stopmodel.h
+
+ jobs/jobeditor/model/jobpassingsmodel.cpp
+ jobs/jobeditor/model/rscouplinginterface.cpp
+ jobs/jobeditor/model/rslistondemandmodel.cpp
+ jobs/jobeditor/model/rslistondemandmodelresultevent.cpp
+ jobs/jobeditor/model/rsproxymodel.cpp
+ jobs/jobeditor/model/stationlineslistmodel.cpp
+ jobs/jobeditor/model/stopcouplingmodel.cpp
+ jobs/jobeditor/model/trainassetmodel.cpp
+ jobs/jobeditor/model/stopmodel.cpp
+ PARENT_SCOPE
+)
diff --git a/src/jobs/jobeditor/model/jobpassingsmodel.cpp b/src/jobs/jobeditor/model/jobpassingsmodel.cpp
new file mode 100644
index 0000000..a9c425d
--- /dev/null
+++ b/src/jobs/jobeditor/model/jobpassingsmodel.cpp
@@ -0,0 +1,100 @@
+#include "jobpassingsmodel.h"
+
+#include
+
+#include "utils/model_roles.h"
+#include "utils/jobcategorystrings.h"
+#include "utils/platform_utils.h"
+
+JobPassingsModel::JobPassingsModel(QObject *parent) :
+ QAbstractTableModel(parent)
+{
+}
+
+QVariant JobPassingsModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if(orientation == Qt::Horizontal && role == Qt::DisplayRole)
+ {
+ switch (section)
+ {
+ case JobNameCol:
+ {
+ return tr("Job");
+ }
+ case ArrivalCol:
+ {
+ return tr("Arrival");
+ }
+ case DepartureCol:
+ {
+ return tr("Departure");
+ }
+ case PlatformCol:
+ {
+ return tr("Platform");
+ }
+ }
+ }
+
+ return QAbstractTableModel::headerData(section, orientation, role);
+}
+
+int JobPassingsModel::rowCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : m_data.size();
+}
+
+int JobPassingsModel::columnCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : 4;
+}
+
+QVariant JobPassingsModel::data(const QModelIndex &idx, int role) const
+{
+ if (!idx.isValid() || idx.row() >= m_data.size() || idx.column() >= NCols)
+ return QVariant();
+
+ const Entry& e = m_data.at(idx.row());
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ {
+ switch (idx.column())
+ {
+ case JobNameCol:
+ return JobCategoryName::jobName(e.jobId, e.category);
+ case ArrivalCol:
+ return e.arrival;
+ case DepartureCol:
+ return e.departure;
+ case PlatformCol:
+ return utils::platformName(e.platform);
+ }
+ break;
+ }
+ case Qt::TextAlignmentRole:
+ {
+ return Qt::AlignCenter;
+ }
+ case Qt::FontRole:
+ {
+ QFont f;
+ f.setPointSize(10);
+ if(idx.column() == JobNameCol)
+ f.setBold(true);
+ return f;
+ }
+ case JOB_ID_ROLE:
+ {
+ return e.jobId;
+ }
+ }
+ return QVariant();
+}
+
+void JobPassingsModel::setJobs(const QVector &vec)
+{
+ beginResetModel();
+ m_data = vec;
+ endResetModel();
+}
diff --git a/src/jobs/jobeditor/model/jobpassingsmodel.h b/src/jobs/jobeditor/model/jobpassingsmodel.h
new file mode 100644
index 0000000..1f9c0c5
--- /dev/null
+++ b/src/jobs/jobeditor/model/jobpassingsmodel.h
@@ -0,0 +1,50 @@
+#ifndef JOBPASSINGSMODEL_H
+#define JOBPASSINGSMODEL_H
+
+#include
+
+#include
+
+#include
+
+#include "utils/types.h"
+
+class JobPassingsModel : public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ typedef enum {
+ JobNameCol = 0,
+ ArrivalCol,
+ DepartureCol,
+ PlatformCol,
+ NCols
+ } Columns;
+
+ typedef struct
+ {
+ db_id jobId;
+ QTime arrival;
+ QTime departure;
+ int platform;
+ JobCategory category;
+ } Entry;
+
+ explicit JobPassingsModel(QObject *parent = nullptr);
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+
+ // Basic functionality:
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override;
+
+ QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
+
+ void setJobs(const QVector& vec);
+
+private:
+ QVector m_data;
+};
+
+#endif // JOBPASSINGSMODEL_H
diff --git a/src/jobs/jobeditor/model/rscouplinginterface.cpp b/src/jobs/jobeditor/model/rscouplinginterface.cpp
new file mode 100644
index 0000000..5135602
--- /dev/null
+++ b/src/jobs/jobeditor/model/rscouplinginterface.cpp
@@ -0,0 +1,439 @@
+#include "rscouplinginterface.h"
+
+#include
+
+#include
+
+#include
+
+#include "stopmodel.h"
+
+RSCouplingInterface::RSCouplingInterface(database &db, QObject *parent) :
+ QObject(parent),
+ stopsModel(nullptr),
+ mDb(db),
+ q_deleteCoupling(mDb, "DELETE FROM coupling WHERE stopId=? AND rsId=?"),
+ q_addCoupling(mDb, "INSERT INTO"
+ " coupling(stopId,rsId,operation)"
+ " VALUES(?, ?, ?)")
+{
+
+}
+
+void RSCouplingInterface::loadCouplings(StopModel *model, db_id stopId, db_id jobId, QTime arr)
+{
+ stopsModel = model;
+
+ m_stopId = stopId;
+ m_jobId = jobId;
+ arrival = arr;
+
+ coupled.clear();
+ uncoupled.clear();
+
+ query q(mDb, "SELECT rsId, operation FROM coupling WHERE stopId=?");
+ q.bind(1, m_stopId);
+
+ for(auto rs : q)
+ {
+ db_id rsId = rs.get(0);
+ int op = rs.get(1);
+
+ if(op == RsOp::Coupled)
+ coupled.append(rsId);
+ else
+ uncoupled.append(rsId);
+ }
+}
+
+bool RSCouplingInterface::contains(db_id rsId, RsOp op) const
+{
+ if(op == RsOp::Coupled)
+ return coupled.contains(rsId);
+ else
+ return uncoupled.contains(rsId);
+}
+
+bool RSCouplingInterface::coupleRS(db_id rsId, const QString& rsName, bool on, bool checkTractionType)
+{
+ stopsModel->startStopsEditing();
+ stopsModel->markRsToUpdate(rsId);
+
+ if(on)
+ {
+ if(coupled.contains(rsId))
+ {
+ qWarning() << "Error already checked:" << rsId;
+ return true;
+ }
+
+ db_id jobId = 0;
+
+ query q_RS_lastOp(mDb, "SELECT MAX(stops.arrival), coupling.operation, stops.jobId"
+ " FROM stops"
+ " JOIN coupling"
+ " ON coupling.stopId=stops.id"
+ " AND coupling.rsId=?"
+ " AND stops.arrival");
+ q_RS_lastOp.bind(1, rsId);
+ q_RS_lastOp.bind(2, arrival);
+ int ret = q_RS_lastOp.step();
+
+ bool isOccupied = false; //No Op means RS is turned off in a depot so it isn't occupied
+ if(ret == SQLITE_ROW)
+ {
+ auto row = q_RS_lastOp.getRows();
+ RsOp operation = RsOp(row.get(1)); //Get last operation
+ jobId = row.get(2);
+ isOccupied = (operation == RsOp::Coupled);
+ }
+
+ if(isOccupied)
+ {
+ if(jobId == m_jobId)
+ {
+ qWarning() << "Error while adding coupling op. Stop:" << m_stopId
+ << "Rs:" << rsId << "Already coupled by this job:" << m_jobId;
+
+ QMessageBox::warning(qApp->activeWindow(),
+ tr("Error"),
+ tr("Error while adding coupling operation.\n"
+ "Rollingstock %1 is already coupled by this job (%2)")
+ .arg(rsName).arg(m_jobId),
+ QMessageBox::Ok);
+ return false;
+ }
+ else
+ {
+ qWarning() << "Error while adding coupling op. Stop:" << m_stopId
+ << "Rs:" << rsId << "Occupied by this job:" << jobId;
+
+ int but = QMessageBox::warning(qApp->activeWindow(),
+ tr("Error"),
+ tr("Error while adding coupling operation.\n"
+ "Rollingstock %1 is already coupled to another job (%2)\n"
+ "Do you still want to couple it?")
+ .arg(rsName).arg(jobId),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
+
+ if(but == QMessageBox::No)
+ return false; //Abort
+ }
+ }
+
+ if(checkTractionType)
+ {
+ LineType lineType = stopsModel->getLineTypeAfterStop(m_stopId);
+ if(lineType == LineType::NonElectric)
+ {
+ //Query RS type
+ query q_getRSType(mDb, "SELECT rs_models.type,rs_models.sub_type"
+ " FROM rs_list"
+ " JOIN rs_models ON rs_models.id=rs_list.model_id"
+ " WHERE rs_list.id=?");
+ q_getRSType.bind(1, rsId);
+ if(q_getRSType.step() != SQLITE_ROW)
+ {
+ qWarning() << "RS seems to not exist, ID:" << rsId;
+ }
+
+ auto rs = q_getRSType.getRows();
+ RsType type = RsType(rs.get(0));
+ RsEngineSubType subType = RsEngineSubType(rs.get(1));
+
+ if(type == RsType::Engine && subType == RsEngineSubType::Electric)
+ {
+ int but = QMessageBox::warning(qApp->activeWindow(),
+ tr("Warning"),
+ tr("Rollingstock %1 is an Electric engine but the line is not electrified\n"
+ "This engine will not be albe to move a train.\n"
+ "Do you still want to couple it?")
+ .arg(rsName),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
+ if(but == QMessageBox::No)
+ return false; //Abort
+ }
+ }
+ }
+
+ q_addCoupling.bind(1, m_stopId);
+ q_addCoupling.bind(2, rsId);
+ q_addCoupling.bind(3, RsOp::Coupled);
+ ret = q_addCoupling.execute();
+ q_addCoupling.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qWarning() << "Error while adding coupling op. Stop:" << m_stopId
+ << "Rs:" << rsId << "Op: Coupled " << "Ret:" << ret
+ << mDb.error_msg();
+ return false;
+ }
+
+ coupled.append(rsId);
+
+ //Check if there is a next coupling operation in the same job
+ query q(mDb, "SELECT s2.id, s2.arrival, s2.stationId, stations.name"
+ " FROM coupling"
+ " JOIN stops s2 ON s2.id=coupling.stopId"
+ " JOIN stops s1 ON s1.id=?"
+ " JOIN stations ON stations.id=s2.stationId"
+ " WHERE coupling.rsId=? AND coupling.operation=? AND s1.jobId=s2.jobId AND s1.arrival < s2.arrival");
+ q.bind(1, m_stopId);
+ q.bind(2, rsId);
+ q.bind(3, RsOp::Coupled);
+
+ if(q.step() == SQLITE_ROW)
+ {
+ auto r = q.getRows();
+ db_id stopId = r.get(0);
+ QTime arr = r.get(1);
+ db_id stId = r.get(2);
+ QString stName = r.get(3);
+
+ qDebug() << "Found coupling, RS:" << rsId << "Stop:" << stopId << "St:" << stId << arr;
+
+ int but = QMessageBox::question(qApp->activeWindow(),
+ tr("Delete coupling?"),
+ tr("You couple %1 also in a next stop in %2 at %3.\n"
+ "Do you want to remove the other coupling operation?")
+ .arg(rsName)
+ .arg(stName)
+ .arg(arr.toString("HH:mm")),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
+ if(but == QMessageBox::Yes)
+ {
+ qDebug() << "Deleting coupling";
+
+ q_deleteCoupling.bind(1, stopId);
+ q_deleteCoupling.bind(2, rsId);
+ ret = q_deleteCoupling.execute();
+ q_deleteCoupling.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qWarning() << "Error while deleting next coupling op. Stop:" << stopId
+ << "Rs:" << rsId << "Op: Uncoupled " << "Ret:" << ret
+ << mDb.error_msg();
+ }
+ }
+ else
+ {
+ qDebug() << "Keeping couple";
+ }
+ }
+ }
+ else
+ {
+ int row = coupled.indexOf(rsId);
+ if(row == -1)
+ return false;
+
+ q_deleteCoupling.bind(1, m_stopId);
+ q_deleteCoupling.bind(2, rsId);
+ int ret = q_deleteCoupling.execute();
+ q_deleteCoupling.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qWarning() << "Error while deleting coupling op. Stop:" << m_stopId
+ << "Rs:" << rsId << "Op: Coupled " << "Ret:" << ret
+ << mDb.error_msg();
+ return false;
+ }
+
+ coupled.removeAt(row);
+
+ //Check if there is a next uncoupling operation
+ query q(mDb, "SELECT s2.id, MIN(s2.arrival), s2.stationId, stations.name"
+ " FROM coupling"
+ " JOIN stops s2 ON s2.id=coupling.stopId"
+ " JOIN stops s1 ON s1.id=?"
+ " JOIN stations ON stations.id=s2.stationId"
+ " WHERE coupling.rsId=? AND coupling.operation=? AND s2.arrival > s1.arrival AND s2.jobId=s1.jobId");
+ q.bind(1, m_stopId);
+ q.bind(2, rsId);
+ q.bind(3, RsOp::Uncoupled);
+
+ if(q.step() == SQLITE_ROW && q.getRows().column_type(0) != SQLITE_NULL)
+ {
+ auto r = q.getRows();
+ db_id stopId = r.get(0);
+ QTime arr = r.get(1);
+ db_id stId = r.get(2);
+ QString stName = r.get(3);
+
+ qDebug() << "Found uncoupling, RS:" << rsId << "Stop:" << stopId << "St:" << stId << arr;
+
+ int but = QMessageBox::question(qApp->activeWindow(),
+ tr("Delete uncoupling?"),
+ tr("You don't couple %1 anymore.\n"
+ "Do you want to remove also the uncoupling operation in %2 at %3?")
+ .arg(rsName)
+ .arg(stName)
+ .arg(arr.toString("HH:mm")),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
+ if(but == QMessageBox::Yes)
+ {
+ qDebug() << "Deleting coupling";
+
+ q_deleteCoupling.bind(1, stopId);
+ q_deleteCoupling.bind(2, rsId);
+ ret = q_deleteCoupling.execute();
+ q_deleteCoupling.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qWarning() << "Error while deleting next uncoupling op. Stop:" << stopId
+ << "Rs:" << rsId << "Op: Uncoupled " << "Ret:" << ret
+ << mDb.error_msg();
+ }
+ }
+ else
+ {
+ qDebug() << "Keeping couple";
+ }
+ }
+ }
+
+ return true;
+}
+
+bool RSCouplingInterface::uncoupleRS(db_id rsId, const QString& rsName, bool on)
+{
+ stopsModel->startStopsEditing();
+ stopsModel->markRsToUpdate(rsId);
+
+ if(on)
+ {
+ if(uncoupled.contains(rsId))
+ {
+ qWarning() << "Error already checked:" << rsId;
+ return true;
+ }
+
+ q_addCoupling.bind(1, m_stopId);
+ q_addCoupling.bind(2, rsId);
+ q_addCoupling.bind(3, RsOp::Uncoupled);
+ int ret = q_addCoupling.execute();
+ q_addCoupling.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qWarning() << "Error while adding coupling op. Stop:" << m_stopId
+ << "Rs:" << rsId << "Op: Uncoupled " << "Ret:" << ret
+ << mDb.error_msg();
+ return false;
+ }
+
+ uncoupled.append(rsId);
+
+ //Check if there is a next uncoupling operation
+ query q(mDb, "SELECT s2.id, MIN(s2.arrival), s2.stationId, stations.name"
+ " FROM coupling"
+ " JOIN stops s2 ON s2.id=coupling.stopId"
+ " JOIN stops s1 ON s1.id=?"
+ " JOIN stations ON stations.id=s2.stationId"
+ " WHERE coupling.rsId=? AND coupling.operation=? AND s2.arrival > s1.arrival AND s2.jobId=s1.jobId");
+ q.bind(1, m_stopId);
+ q.bind(2, rsId);
+ q.bind(3, RsOp::Uncoupled);
+
+ if(q.step() == SQLITE_ROW && q.getRows().column_type(0) != SQLITE_NULL)
+ {
+ auto r = q.getRows();
+ db_id stopId = r.get(0);
+ QTime arr = r.get(1);
+ db_id stId = r.get(2);
+ QString stName = r.get(3);
+
+ qDebug() << "Found uncoupling, RS:" << rsId << "Stop:" << stopId << "St:" << stId << arr;
+
+ int but = QMessageBox::question(qApp->activeWindow(),
+ tr("Delete uncoupling?"),
+ tr("You uncouple %1 also in %2 at %3.\n"
+ "Do you want to remove the other uncoupling operation?")
+ .arg(rsName)
+ .arg(stName)
+ .arg(arr.toString("HH:mm")),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
+ if(but == QMessageBox::Yes)
+ {
+ qDebug() << "Deleting coupling";
+
+ q_deleteCoupling.bind(1, stopId);
+ q_deleteCoupling.bind(2, rsId);
+ ret = q_deleteCoupling.execute();
+ q_deleteCoupling.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qWarning() << "Error while deleting next uncoupling op. Stop:" << stopId
+ << "Rs:" << rsId << "Op: Uncoupled " << "Ret:" << ret
+ << mDb.error_msg();
+ }
+ }
+ else
+ {
+ qDebug() << "Keeping couple";
+ }
+ }
+ }
+ else
+ {
+ int row = uncoupled.indexOf(rsId);
+ if(row == -1)
+ return false;
+
+ q_deleteCoupling.bind(1, m_stopId);
+ q_deleteCoupling.bind(2, rsId);
+ int ret = q_deleteCoupling.execute();
+ q_deleteCoupling.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qWarning() << "Error while deleting coupling op. Stop:" << m_stopId
+ << "Rs:" << rsId << "Op: Uncoupled " << "Ret:" << ret
+ << mDb.error_msg();
+ return false;
+ }
+
+ uncoupled.removeAt(row);
+ }
+
+ return true;
+}
+
+bool RSCouplingInterface::hasEngineAfterStop(bool *isElectricOnNonElectrifiedLine)
+{
+ query q_hasEngine(mDb, "SELECT coupling.rsId,MAX(rs_models.sub_type),MAX(stops.arrival)"
+ " FROM stops"
+ " JOIN coupling ON coupling.stopId=stops.id"
+ " JOIN rs_list ON rs_list.id=coupling.rsId"
+ " JOIN rs_models ON rs_models.id=rs_list.model_id"
+ " WHERE stops.jobId=? AND stops.arrival<=? AND rs_models.type=0"
+ " GROUP BY coupling.rsId"
+ " HAVING coupling.operation=1"
+ " LIMIT 1");
+ q_hasEngine.bind(1, m_jobId);
+ q_hasEngine.bind(2, arrival);
+ if(q_hasEngine.step() != SQLITE_ROW)
+ return false; //No engine
+
+ if(isElectricOnNonElectrifiedLine)
+ {
+ RsEngineSubType subType = RsEngineSubType(q_hasEngine.getRows().get(1));
+ *isElectricOnNonElectrifiedLine = (subType == RsEngineSubType::Electric) && (getLineType() != LineType::Electric);
+ }
+ return true;
+}
+
+LineType RSCouplingInterface::getLineType() const
+{
+ return stopsModel->getLineTypeAfterStop(m_stopId);
+}
+
+db_id RSCouplingInterface::getJobId() const
+{
+ return m_jobId;
+}
diff --git a/src/jobs/jobeditor/model/rscouplinginterface.h b/src/jobs/jobeditor/model/rscouplinginterface.h
new file mode 100644
index 0000000..80866c8
--- /dev/null
+++ b/src/jobs/jobeditor/model/rscouplinginterface.h
@@ -0,0 +1,49 @@
+#ifndef RSCOUPLINGINTERFACE_H
+#define RSCOUPLINGINTERFACE_H
+
+#include
+
+#include
+
+#include "sqlite3pp/sqlite3pp.h"
+using namespace sqlite3pp;
+
+#include "utils/types.h"
+
+class StopModel;
+
+class RSCouplingInterface : public QObject
+{
+ Q_OBJECT
+public:
+ explicit RSCouplingInterface(database &db, QObject *parent = nullptr);
+
+ void loadCouplings(StopModel *model, db_id stopId, db_id jobId, QTime arr);
+
+ bool contains(db_id rsId, RsOp op) const;
+
+ bool coupleRS(db_id rsId, const QString &rsName, bool on, bool checkTractionType);
+ bool uncoupleRS(db_id rsId, const QString &rsName, bool on);
+
+ bool hasEngineAfterStop(bool *isElectricOnNonElectrifiedLine = nullptr);
+
+ LineType getLineType() const;
+
+ db_id getJobId() const;
+
+private:
+ StopModel *stopsModel;
+
+ QVector coupled;
+ QVector uncoupled;
+
+ database &mDb;
+ command q_deleteCoupling;
+ command q_addCoupling;
+
+ db_id m_stopId;
+ db_id m_jobId;
+ QTime arrival;
+};
+
+#endif // RSCOUPLINGINTERFACE_H
diff --git a/src/jobs/jobeditor/model/rslistondemandmodel.cpp b/src/jobs/jobeditor/model/rslistondemandmodel.cpp
new file mode 100644
index 0000000..705be0b
--- /dev/null
+++ b/src/jobs/jobeditor/model/rslistondemandmodel.cpp
@@ -0,0 +1,198 @@
+#include "rslistondemandmodel.h"
+#include "rslistondemandmodelresultevent.h"
+
+#include
+
+RSListOnDemandModel::RSListOnDemandModel(sqlite3pp::database &db, QObject *parent) :
+ IPagedItemModel(50, db, parent),
+ cacheFirstRow(0),
+ firstPendingRow(-BatchSize)
+{
+ sortColumn = Name;
+}
+
+bool RSListOnDemandModel::event(QEvent *e)
+{
+ if(e->type() == RSListOnDemandModelResultEvent::_Type)
+ {
+ RSListOnDemandModelResultEvent *ev = static_cast(e);
+ ev->setAccepted(true);
+
+ handleResult(ev->items, ev->firstRow);
+
+ return true;
+ }
+
+ return QAbstractTableModel::event(e);
+}
+
+int RSListOnDemandModel::rowCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : curItemCount;
+}
+
+int RSListOnDemandModel::columnCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : NCols;
+}
+
+QVariant RSListOnDemandModel::data(const QModelIndex &idx, int role) const
+{
+ const int row = idx.row();
+ if (!idx.isValid() || row >= curItemCount || idx.column() >= NCols)
+ return QVariant();
+
+ //qDebug() << "Data:" << idx.row();
+
+ if(row < cacheFirstRow || row >= cacheFirstRow + cache.size())
+ {
+ //Fetch above or below current cache
+ const_cast(this)->fetchRow(row);
+
+ //Temporarily return null
+ return role == Qt::DisplayRole ? QVariant("...") : QVariant();
+ }
+
+ const RSItem& item = cache.at(row - cacheFirstRow);
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ {
+ switch (idx.column())
+ {
+ case Name:
+ return item.name;
+ }
+
+ break;
+ }
+ case Qt::FontRole:
+ {
+ if(item.type == RsType::Engine)
+ {
+ //Engines in bold
+ QFont f;
+ f.setBold(true);
+ return f;
+ }
+ break;
+ }
+ }
+
+ return QVariant();
+}
+
+void RSListOnDemandModel::clearCache()
+{
+ cache.clear();
+ cache.squeeze();
+ cacheFirstRow = 0;
+}
+
+void RSListOnDemandModel::fetchRow(int row)
+{
+ if(firstPendingRow != -BatchSize)
+ return; //Currently fetching another batch, wait for it to finish first
+
+ if(row >= firstPendingRow && row < firstPendingRow + BatchSize)
+ return; //Already fetching this batch
+
+ if(row >= cacheFirstRow && row < cacheFirstRow + cache.size())
+ return; //Already cached
+
+ //TODO: abort fetching here
+
+ const int remainder = row % BatchSize;
+ firstPendingRow = row - remainder;
+
+ QVariant val;
+ int valRow = 0;
+// RSItem *item = nullptr;
+
+// if(cache.size())
+// {
+// if(firstPendingRow >= cacheFirstRow + cache.size())
+// {
+// valRow = cacheFirstRow + cache.size();
+// item = &cache.last();
+// }
+// else if(firstPendingRow > (cacheFirstRow - firstPendingRow))
+// {
+// valRow = cacheFirstRow;
+// item = &cache.first();
+// }
+// }
+
+ /*switch (sortCol) TODO: use val in WHERE clause
+ {
+ case Name:
+ {
+ if(item)
+ {
+ val = item->name;
+ }
+ break;
+ }
+ //No data hint for TypeCol column
+ }*/
+
+ //TODO: use a custom QRunnable
+ // QMetaObject::invokeMethod(this, "internalFetch", Qt::QueuedConnection,
+ // Q_ARG(int, firstPendingRow), Q_ARG(int, sortCol),
+ // Q_ARG(int, valRow), Q_ARG(QVariant, val));
+ internalFetch(firstPendingRow, sortColumn, val.isNull() ? 0 : valRow, val);
+}
+
+void RSListOnDemandModel::handleResult(const QVector& items, int firstRow)
+{
+ if(firstRow == cacheFirstRow + cache.size())
+ {
+ cache.append(items);
+ if(cache.size() > ItemsPerPage)
+ {
+ const int extra = cache.size() - ItemsPerPage; //Round up to BatchSize
+ const int remainder = extra % BatchSize;
+ const int n = remainder ? extra + BatchSize - remainder : extra;
+ cache.remove(0, n);
+ cacheFirstRow += n;
+ }
+ }
+ else
+ {
+ if(firstRow + items.size() == cacheFirstRow)
+ {
+ QVector tmp = items;
+ tmp.append(cache);
+ cache = tmp;
+ if(cache.size() > ItemsPerPage)
+ {
+ const int n = cache.size() - ItemsPerPage;
+ cache.remove(ItemsPerPage, n);
+ }
+ }
+ else
+ {
+ cache = items;
+ }
+ cacheFirstRow = firstRow;
+ }
+
+ firstPendingRow = -BatchSize;
+
+ int lastRow = firstRow + items.count(); //Last row + 1 extra to re-trigger possible next batch
+ if(lastRow >= curItemCount)
+ lastRow = curItemCount -1; //Ok, there is no extra row so notify just our batch
+
+ if(firstRow > 0)
+ firstRow--; //Try notify also the row before because there might be another batch waiting so re-trigger it
+ QModelIndex firstIdx = index(firstRow, 0);
+ QModelIndex lastIdx = index(lastRow, NCols - 1);
+ emit dataChanged(firstIdx, lastIdx);
+}
+
+void RSListOnDemandModel::setSortingColumn(int /*col*/)
+{
+ //Only sort by name
+}
diff --git a/src/jobs/jobeditor/model/rslistondemandmodel.h b/src/jobs/jobeditor/model/rslistondemandmodel.h
new file mode 100644
index 0000000..92d5fc5
--- /dev/null
+++ b/src/jobs/jobeditor/model/rslistondemandmodel.h
@@ -0,0 +1,60 @@
+#ifndef RSLISTONDEMANDMODEL_H
+#define RSLISTONDEMANDMODEL_H
+
+#include "utils/sqldelegate/pageditemmodel.h"
+
+#include "utils/types.h"
+
+#include
+
+
+class RSListOnDemandModel : public IPagedItemModel
+{
+ Q_OBJECT
+
+public:
+
+ enum { BatchSize = 25 };
+
+ typedef enum {
+ Name = 0,
+ NCols
+ } Columns;
+
+ typedef struct RSItem_
+ {
+ db_id rsId;
+ QString name;
+ RsType type;
+ } RSItem;
+
+ RSListOnDemandModel(sqlite3pp::database &db, QObject *parent = nullptr);
+
+ bool event(QEvent *e) override;
+
+ // QAbstractTableModel
+
+ // Basic functionality:
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override;
+
+ QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
+
+ // IPagedItemModel
+
+ // Cached rows management
+ virtual void clearCache() override;
+ virtual void setSortingColumn(int col) override;
+
+private:
+ virtual void internalFetch(int first, int sortColumn, int valRow, const QVariant &val) = 0;
+ void fetchRow(int row);
+ void handleResult(const QVector &items, int firstRow);
+
+private:
+ QVector cache;
+ int cacheFirstRow;
+ int firstPendingRow;
+};
+
+#endif // RSLISTONDEMANDMODEL_H
diff --git a/src/jobs/jobeditor/model/rslistondemandmodelresultevent.cpp b/src/jobs/jobeditor/model/rslistondemandmodelresultevent.cpp
new file mode 100644
index 0000000..35e1222
--- /dev/null
+++ b/src/jobs/jobeditor/model/rslistondemandmodelresultevent.cpp
@@ -0,0 +1,6 @@
+#include "rslistondemandmodelresultevent.h"
+
+RSListOnDemandModelResultEvent::RSListOnDemandModelResultEvent() :
+ QEvent(_Type)
+{
+}
diff --git a/src/jobs/jobeditor/model/rslistondemandmodelresultevent.h b/src/jobs/jobeditor/model/rslistondemandmodelresultevent.h
new file mode 100644
index 0000000..4cf9ded
--- /dev/null
+++ b/src/jobs/jobeditor/model/rslistondemandmodelresultevent.h
@@ -0,0 +1,19 @@
+#ifndef RSLISTONDEMANDMODELRESULTEVENT_H
+#define RSLISTONDEMANDMODELRESULTEVENT_H
+
+#include "rslistondemandmodel.h"
+#include
+#include "utils/worker_event_types.h"
+
+class RSListOnDemandModelResultEvent : public QEvent
+{
+public:
+ static constexpr Type _Type = Type(CustomEvents::RsOnDemandListModelResult);
+
+ RSListOnDemandModelResultEvent();
+
+ QVector items;
+ int firstRow;
+};
+
+#endif // RSLISTONDEMANDMODELRESULTEVENT_H
diff --git a/src/jobs/jobeditor/model/rsproxymodel.cpp b/src/jobs/jobeditor/model/rsproxymodel.cpp
new file mode 100644
index 0000000..6729b94
--- /dev/null
+++ b/src/jobs/jobeditor/model/rsproxymodel.cpp
@@ -0,0 +1,173 @@
+#include "rsproxymodel.h"
+
+#include "rscouplinginterface.h"
+
+#include "utils/jobcategorystrings.h"
+
+#include
+
+
+RSProxyModel::RSProxyModel(RSCouplingInterface *mgr,
+ RsOp o,
+ RsType type,
+ QObject *parent) :
+ QAbstractListModel (parent),
+ couplingMgr(mgr),
+ op(o),
+ targetType(type)
+{
+
+}
+
+int RSProxyModel::rowCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : m_data.size();
+}
+
+QVariant RSProxyModel::data(const QModelIndex &idx, int role) const
+{
+ if(!idx.isValid() || idx.column() > 0 || idx.row() >= m_data.size())
+ return QVariant();
+
+ const RsItem& item = m_data.at(idx.row());
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ return item.rsName;
+ case Qt::CheckStateRole:
+ {
+ return couplingMgr->contains(item.rsId, op) ? Qt::Checked : Qt::Unchecked;
+ }
+ case Qt::BackgroundRole: //NOTE SYNC: RSCoupleDialog
+ {
+ if(item.flag == ErrNotCoupledBefore || item.flag == ErrAlreadyCoupled)
+ {
+ //Error: already coupled or already uncoupled or not coupled at all before this stop
+ return QBrush(qRgb(255, 86, 255)); //Solid light magenta #FF56FF
+ }
+ if(item.flag == WrongStation)
+ {
+ //Error: RS is not in this station
+ return QBrush(qRgb(255, 61, 67)); //Solid light red #FF3d43
+ }
+ if(targetType == RsType::Engine && item.engineType == RsEngineSubType::Electric && couplingMgr->getLineType() == LineType::NonElectric)
+ {
+ //Warn Electric traction not possible
+ return QBrush(qRgb(0, 0, 255), Qt::FDiagPattern); //Blue
+ }
+ if(item.flag == FirstUseOfRS)
+ {
+ return QBrush(qRgb(0, 255, 255)); //Cyan
+ }
+ if(item.flag == UnusedRS)
+ {
+ return QBrush(qRgb(0, 255, 0)); //Green
+ }
+ break;
+ }
+ case Qt::ToolTipRole:
+ {
+ if(item.flag == ErrNotCoupledBefore)
+ {
+ //Error
+ return tr("Rollingstock %1 cannot be uncoupled here because it wasn't coupled to this job before this stop "
+ "or because it was already uncoupled before this stop.
"
+ "Please remove the tick").arg(item.rsName);
+ }
+ if(item.flag == ErrAlreadyCoupled)
+ {
+ //Error
+ if(item.jobId == couplingMgr->getJobId())
+ {
+ return tr("Rollingstock %1 cannot be coupled here because it was already coupled to this job before this stop
"
+ "Please remove the tick").arg(item.rsName);
+ }else{
+ return tr("Rollingstock %1 cannot be coupled here because it was already coupled before this stop
"
+ "to job %2
"
+ "Please remove the tick")
+ .arg(item.rsName)
+ .arg(JobCategoryName::jobName(item.jobId, item.jobCat));
+ }
+ }
+ if(item.flag == WrongStation)
+ {
+ //Error
+ return tr("Rollingstock %1 cannot be coupled here because it is not in this station.
"
+ "Please remove the tick").arg(item.rsName);
+ }
+ if(targetType == RsType::Engine && item.engineType == RsEngineSubType::Electric && couplingMgr->getLineType() == LineType::NonElectric)
+ {
+ //Warn Electric traction not possible
+ return tr("Engine %1 is electric but the line is not electrified!").arg(item.rsName);
+ }
+ if(item.flag == HasNextOperation)
+ {
+ return tr("Rollingstock %1 is coupled in this station also by %2 at %3.")
+ .arg(item.rsName)
+ .arg(JobCategoryName::jobName(item.jobId, item.jobCat))
+ .arg(item.time.toString("HH:mm"));
+ }
+ if(item.flag == LastOperation)
+ {
+ return tr("Rollingstock %1 was left in this station by %2 at %3.")
+ .arg(item.rsName)
+ .arg(JobCategoryName::jobName(item.jobId, item.jobCat))
+ .arg(item.time.toString("HH:mm"));
+ }
+ if(item.flag == FirstUseOfRS)
+ {
+ if(op == Coupled && couplingMgr->contains(item.rsId, Coupled))
+ return tr("This is the first use of this rollingstock %1").arg(item.rsName);
+ return tr("This would be the first use of this rollingstock %1").arg(item.rsName);
+ }
+ if(item.flag == UnusedRS)
+ {
+ return tr("Rollingstock %1 is never used in this session. You can couple it for the first time from any one station")
+ .arg(item.rsName);
+ }
+ break;
+ }
+ }
+
+ return QVariant();
+}
+
+bool RSProxyModel::setData(const QModelIndex &idx, const QVariant &value, int role)
+{
+ if(role != Qt::CheckStateRole || !idx.isValid() || idx.column() > 0 || idx.row() >= m_data.size())
+ return false;
+
+ Qt::CheckState state = value.value();
+
+ const RsItem& item = m_data.at(idx.row());
+
+ bool ret = false;
+
+ if(op == RsOp::Coupled) //Check traction type only if we are dealing with RsType::Engine
+ ret = couplingMgr->coupleRS(item.rsId, item.rsName, state == Qt::Checked, targetType == RsType::Engine);
+ else
+ ret = couplingMgr->uncoupleRS(item.rsId, item.rsName, state == Qt::Checked);
+
+ if(ret)
+ emit dataChanged(idx, idx);
+
+ return ret;
+}
+
+Qt::ItemFlags RSProxyModel::flags(const QModelIndex &index) const
+{
+ if(index.isValid())
+ return Qt::ItemIsEnabled
+ | Qt::ItemNeverHasChildren
+ | Qt::ItemIsSelectable
+ | Qt::ItemIsUserCheckable;
+ return Qt::NoItemFlags;
+}
+
+void RSProxyModel::loadData(const QVector &items)
+{
+ beginResetModel();
+ m_data = items;
+ endResetModel();
+}
diff --git a/src/jobs/jobeditor/model/rsproxymodel.h b/src/jobs/jobeditor/model/rsproxymodel.h
new file mode 100644
index 0000000..c1197c1
--- /dev/null
+++ b/src/jobs/jobeditor/model/rsproxymodel.h
@@ -0,0 +1,63 @@
+#ifndef RSPROXYMODEL_H
+#define RSPROXYMODEL_H
+
+#include
+
+#include
+
+#include "utils/types.h"
+
+class RSCouplingInterface;
+
+class RSProxyModel : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+
+ RSProxyModel(RSCouplingInterface *mgr,
+ RsOp o,
+ RsType type,
+ QObject *parent = nullptr);
+
+ int rowCount(const QModelIndex &parent) const override;
+
+ QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
+ bool setData(const QModelIndex &idx, const QVariant &value, int role) override;
+
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+
+ enum RSItemFlg
+ {
+ NoFlag = 0,
+ WrongStation = 1,
+ LastOperation = 2,
+ HasNextOperation = 3,
+ FirstUseOfRS = 4,
+ UnusedRS = 5,
+ ErrNotCoupledBefore = 6,
+ ErrAlreadyCoupled = 7
+ };
+
+ struct RsItem
+ {
+ db_id rsId;
+ db_id jobId; //Can be next job or previous job
+ QString rsName;
+ int flag;
+ QTime time; //Can be next or previous operation time
+ JobCategory jobCat;
+ RsEngineSubType engineType;
+ };
+
+ void loadData(const QVector& items);
+
+private:
+ QVector m_data;
+
+ RSCouplingInterface *couplingMgr;
+ RsOp op;
+ RsType targetType;
+};
+
+#endif // RSPROXYMODEL_H
diff --git a/src/jobs/jobeditor/model/stationlineslistmodel.cpp b/src/jobs/jobeditor/model/stationlineslistmodel.cpp
new file mode 100644
index 0000000..79929a2
--- /dev/null
+++ b/src/jobs/jobeditor/model/stationlineslistmodel.cpp
@@ -0,0 +1,422 @@
+#include "stationlineslistmodel.h"
+
+#include
+
+#include
+using namespace sqlite3pp;
+
+#include "utils/rs_types_names.h"
+
+#include "utils/worker_event_types.h"
+
+#include
+
+class StationLinesListModelResultEvent : public QEvent
+{
+public:
+ static constexpr Type _Type = Type(CustomEvents::StationLinesListModelResult);
+ inline StationLinesListModelResultEvent() :QEvent(_Type) {}
+
+ QVector items;
+ int firstRow;
+};
+
+StationLinesListModel::StationLinesListModel(sqlite3pp::database &db, QObject *parent) :
+ IPagedItemModel(50, db, parent),
+ m_stationId(0),
+ cacheFirstRow(0),
+ firstPendingRow(-BatchSize)
+{
+ sortColumn = Name;
+}
+
+bool StationLinesListModel::event(QEvent *e)
+{
+ if(e->type() == StationLinesListModelResultEvent::_Type)
+ {
+ StationLinesListModelResultEvent *ev = static_cast(e);
+ ev->setAccepted(true);
+
+ handleResult(ev->items, ev->firstRow);
+
+ return true;
+ }
+
+ return QAbstractTableModel::event(e);
+}
+
+QVariant StationLinesListModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if(role == Qt::DisplayRole)
+ {
+ if(orientation == Qt::Horizontal)
+ {
+ switch (section)
+ {
+ case Name:
+ return tr("Name");
+ default:
+ break;
+ }
+ }
+ else
+ {
+ return section + curPage * ItemsPerPage + 1;
+ }
+ }
+ return IPagedItemModel::headerData(section, orientation, role);
+}
+
+int StationLinesListModel::rowCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : curItemCount;
+}
+
+int StationLinesListModel::columnCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : NCols;
+}
+
+QVariant StationLinesListModel::data(const QModelIndex &idx, int role) const
+{
+ const int row = idx.row();
+ if (!idx.isValid() || row >= curItemCount || idx.column() >= NCols)
+ return QVariant();
+
+ //qDebug() << "Data:" << idx.row();
+
+ if(row < cacheFirstRow || row >= cacheFirstRow + cache.size())
+ {
+ //Fetch above or below current cache
+ const_cast(this)->fetchRow(row);
+
+ //Temporarily return null
+ return role == Qt::DisplayRole ? QVariant("...") : QVariant();
+ }
+
+ const LineItem& item = cache.at(row - cacheFirstRow);
+
+ switch (role)
+ {
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ {
+ switch (idx.column())
+ {
+ case Name:
+ return item.name;
+ }
+
+ break;
+ }
+ }
+
+ return QVariant();
+}
+
+void StationLinesListModel::clearCache()
+{
+ cache.clear();
+ cache.squeeze();
+ cacheFirstRow = 0;
+}
+
+void StationLinesListModel::refreshData()
+{
+ if(!mDb.db())
+ return;
+
+ int count = 0;
+ if(m_stationId)
+ {
+ query q(mDb, "SELECT COUNT(1) FROM railways WHERE stationId=?");
+ q.bind(1, m_stationId);
+ q.step();
+ count = q.getRows().get(0);
+ }
+ if(count != totalItemsCount)
+ {
+ beginResetModel();
+
+ clearCache();
+ totalItemsCount = count;
+ emit totalItemsCountChanged(totalItemsCount);
+
+ //Round up division
+ const int rem = count % ItemsPerPage;
+ pageCount = count / ItemsPerPage + (rem != 0);
+ emit pageCountChanged(pageCount);
+
+ if(curPage >= pageCount)
+ {
+ switchToPage(pageCount - 1);
+ }
+
+ curItemCount = totalItemsCount ? (curPage == pageCount - 1 && rem) ? rem : ItemsPerPage : 0;
+
+ endResetModel();
+ }
+}
+
+void StationLinesListModel::fetchRow(int row)
+{
+ if(firstPendingRow != -BatchSize)
+ return; //Currently fetching another batch, wait for it to finish first
+
+ if(row >= firstPendingRow && row < firstPendingRow + BatchSize)
+ return; //Already fetching this batch
+
+ if(row >= cacheFirstRow && row < cacheFirstRow + cache.size())
+ return; //Already cached
+
+ //TODO: abort fetching here
+
+ const int remainder = row % BatchSize;
+ firstPendingRow = row - remainder;
+ qDebug() << "Requested:" << row << "From:" << firstPendingRow;
+
+ QVariant val;
+ int valRow = 0;
+ // RSOwner *item = nullptr;
+
+ // if(cache.size())
+ // {
+ // if(firstPendingRow >= cacheFirstRow + cache.size())
+ // {
+ // valRow = cacheFirstRow + cache.size();
+ // item = &cache.last();
+ // }
+ // else if(firstPendingRow > (cacheFirstRow - firstPendingRow))
+ // {
+ // valRow = cacheFirstRow;
+ // item = &cache.first();
+ // }
+ // }
+
+ /*switch (sortCol) TODO: use val in WHERE clause
+ {
+ case Name:
+ {
+ if(item)
+ {
+ val = item->name;
+ }
+ break;
+ }
+ //No data hint for TypeCol column
+ }*/
+
+ //TODO: use a custom QRunnable
+ // QMetaObject::invokeMethod(this, "internalFetch", Qt::QueuedConnection,
+ // Q_ARG(int, firstPendingRow), Q_ARG(int, sortCol),
+ // Q_ARG(int, valRow), Q_ARG(QVariant, val));
+ internalFetch(firstPendingRow, sortColumn, val.isNull() ? 0 : valRow, val);
+}
+
+void StationLinesListModel::internalFetch(int first, int sortCol, int valRow, const QVariant& val)
+{
+ query q(mDb);
+
+ int offset = first - valRow + curPage * ItemsPerPage;
+ bool reverse = false;
+
+ // if(valRow > first)
+ // {
+ // offset = 0;
+ // reverse = true;
+ // }
+
+ qDebug() << "Fetching:" << first << "ValRow:" << valRow << val << "Offset:" << offset << "Reverse:" << reverse;
+
+ //const char *whereCol;
+
+ QByteArray sql = "SELECT railways.lineId,lines.name FROM railways"
+ " JOIN lines ON lines.id=railways.lineId"
+ " WHERE railways.stationId=?3"
+ " ORDER BY lines.name"; //TODO: support valRow
+ // switch (sortCol)
+ // {
+ // case Name:
+ // {
+ // whereCol = "name"; //Order by 2 columns, no where clause
+ // break;
+ // }
+ // }
+
+ // if(val.isValid())
+ // {
+ // sql += " WHERE ";
+ // sql += whereCol;
+ // if(reverse)
+ // sql += "3";
+ // else
+ // sql += ">?3";
+ // }
+
+ // sql += " ORDER BY ";
+ // sql += whereCol;
+
+ // if(reverse)
+ // sql += " DESC";
+
+ sql += " LIMIT ?1";
+ if(offset)
+ sql += " OFFSET ?2";
+
+ q.prepare(sql);
+ q.bind(1, BatchSize);
+ if(offset)
+ q.bind(2, offset);
+ q.bind(3, m_stationId);
+
+ if(val.isValid())
+ {
+ switch (sortCol)
+ {
+ case Name:
+ {
+ q.bind(3, val.toString());
+ break;
+ }
+ }
+ }
+
+ QVector vec(BatchSize);
+
+ auto it = q.begin();
+ const auto end = q.end();
+
+ if(reverse)
+ {
+ int i = BatchSize - 1;
+
+ for(; it != end; ++it)
+ {
+ auto r = *it;
+ LineItem &item = vec[i];
+ item.lineId = r.get(0);
+ item.name = r.get(1);
+ i--;
+ }
+ if(i > -1)
+ vec.remove(0, i + 1);
+ }
+ else
+ {
+ int i = 0;
+
+ for(; it != end; ++it)
+ {
+ auto r = *it;
+ LineItem &item = vec[i];
+ item.lineId = r.get(0);
+ item.name = r.get(1);
+ i++;
+ }
+ if(i < BatchSize)
+ vec.remove(i, BatchSize - i);
+ }
+
+
+ StationLinesListModelResultEvent *ev = new StationLinesListModelResultEvent;
+ ev->items = vec;
+ ev->firstRow = first;
+
+ qApp->postEvent(this, ev);
+}
+
+void StationLinesListModel::handleResult(const QVector& items, int firstRow)
+{
+ if(firstRow == cacheFirstRow + cache.size())
+ {
+ qDebug() << "RES: appending First:" << cacheFirstRow;
+ cache.append(items);
+ if(cache.size() > ItemsPerPage)
+ {
+ const int extra = cache.size() - ItemsPerPage; //Round up to BatchSize
+ const int remainder = extra % BatchSize;
+ const int n = remainder ? extra + BatchSize - remainder : extra;
+ qDebug() << "RES: removing last" << n;
+ cache.remove(0, n);
+ cacheFirstRow += n;
+ }
+ }
+ else
+ {
+ if(firstRow + items.size() == cacheFirstRow)
+ {
+ qDebug() << "RES: prepending First:" << cacheFirstRow;
+ QVector tmp = items;
+ tmp.append(cache);
+ cache = tmp;
+ if(cache.size() > ItemsPerPage)
+ {
+ const int n = cache.size() - ItemsPerPage;
+ cache.remove(ItemsPerPage, n);
+ qDebug() << "RES: removing first" << n;
+ }
+ }
+ else
+ {
+ qDebug() << "RES: replacing";
+ cache = items;
+ }
+ cacheFirstRow = firstRow;
+ qDebug() << "NEW First:" << cacheFirstRow;
+ }
+
+ firstPendingRow = -BatchSize;
+
+ int lastRow = firstRow + items.count(); //Last row + 1 extra to re-trigger possible next batch
+ if(lastRow >= curItemCount)
+ lastRow = curItemCount -1; //Ok, there is no extra row so notify just our batch
+
+ if(firstRow > 0)
+ firstRow--; //Try notify also the row before because there might be another batch waiting so re-trigger it
+ QModelIndex firstIdx = index(firstRow, 0);
+ QModelIndex lastIdx = index(lastRow, NCols - 1);
+ emit dataChanged(firstIdx, lastIdx);
+ emit resultsReady();
+
+ qDebug() << "TOTAL: From:" << cacheFirstRow << "To:" << cacheFirstRow + cache.size() - 1;
+}
+
+void StationLinesListModel::setSortingColumn(int /*col*/)
+{
+ //Only sort by name
+}
+
+void StationLinesListModel::setStationId(db_id stationId)
+{
+ m_stationId = stationId;
+ clearCache();
+ refreshData();
+}
+
+int StationLinesListModel::getLineRow(db_id lineId)
+{
+ if(!m_stationId)
+ return -1;
+
+ query q(mDb, "SELECT 1 FROM railways WHERE lineId=? AND stationId=? LIMIT 1");
+ q.bind(1, lineId);
+ q.bind(2, m_stationId);
+ if(q.step() != SQLITE_ROW)
+ return -1; //This line doesn't pass in this station
+
+ q.prepare("SELECT COUNT(1) FROM railways"
+ " JOIN lines l1 ON l1.id=railways.lineId"
+ " JOIN lines l2 ON l2.id=?"
+ " WHERE railways.stationId=? AND l1.name(0);
+}
+
+db_id StationLinesListModel::getLineIdAt(int row)
+{
+ if(row < cacheFirstRow || row >= cacheFirstRow + cache.size())
+ return 0;
+ return cache.at(row - cacheFirstRow).lineId;
+}
diff --git a/src/jobs/jobeditor/model/stationlineslistmodel.h b/src/jobs/jobeditor/model/stationlineslistmodel.h
new file mode 100644
index 0000000..c0443dc
--- /dev/null
+++ b/src/jobs/jobeditor/model/stationlineslistmodel.h
@@ -0,0 +1,74 @@
+#ifndef STATIONLINESLISTMODEL_H
+#define STATIONLINESLISTMODEL_H
+
+#include "utils/sqldelegate/pageditemmodel.h"
+
+#include "utils/types.h"
+
+#include
+
+
+class StationLinesListModel : public IPagedItemModel
+{
+ Q_OBJECT
+
+public:
+
+ enum { BatchSize = 25 };
+
+ typedef enum {
+ Name = 0,
+ NCols
+ } Columns;
+
+ typedef struct RSOwner_
+ {
+ db_id lineId;
+ QString name;
+ } LineItem;
+
+ StationLinesListModel(sqlite3pp::database &db, QObject *parent = nullptr);
+ bool event(QEvent *e) override;
+
+ // QAbstractTableModel
+
+ // Header:
+ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+
+ // Basic functionality:
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override;
+
+ QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
+
+ // IPagedItemModel
+
+ // Cached rows management
+ virtual void clearCache() override;
+ virtual void refreshData() override;
+
+ // Sorting TODO: enable multiple columns sort/filter with custom QHeaderView
+ virtual void setSortingColumn(int col) override;
+
+ // StationLinesListModel
+ void setStationId(db_id stationId);
+
+ int getLineRow(db_id lineId);
+ db_id getLineIdAt(int row);
+
+signals:
+ void resultsReady();
+
+private:
+ void fetchRow(int row);
+ Q_INVOKABLE void internalFetch(int first, int sortColumn, int valRow, const QVariant &val);
+ void handleResult(const QVector &items, int firstRow);
+
+private:
+ db_id m_stationId;
+ QVector cache;
+ int cacheFirstRow;
+ int firstPendingRow;
+};
+
+#endif // STATIONLINESLISTMODEL_H
diff --git a/src/jobs/jobeditor/model/stopcouplingmodel.cpp b/src/jobs/jobeditor/model/stopcouplingmodel.cpp
new file mode 100644
index 0000000..70fca8c
--- /dev/null
+++ b/src/jobs/jobeditor/model/stopcouplingmodel.cpp
@@ -0,0 +1,197 @@
+#include "stopcouplingmodel.h"
+
+#include
+
+#include
+using namespace sqlite3pp;
+
+#include "rslistondemandmodelresultevent.h"
+#include "utils/rs_utils.h"
+
+
+StopCouplingModel::StopCouplingModel(sqlite3pp::database &db, QObject *parent) :
+ RSListOnDemandModel(db, parent),
+ m_stopId(0),
+ m_operation(RsOp::Uncoupled)
+{
+}
+
+void StopCouplingModel::refreshData()
+{
+ if(!mDb.db())
+ return;
+
+ query q(mDb, "SELECT COUNT(1) FROM coupling WHERE stopId=? AND operation=?");
+ q.bind(1, m_stopId);
+ q.bind(2, m_operation);
+ q.step();
+ const int count = q.getRows().get(0);
+ if(count != totalItemsCount)
+ {
+ beginResetModel();
+
+ clearCache();
+ totalItemsCount = count;
+ emit totalItemsCountChanged(totalItemsCount);
+
+ //Round up division
+ const int rem = count % ItemsPerPage;
+ pageCount = count / ItemsPerPage + (rem != 0);
+ emit pageCountChanged(pageCount);
+
+ if(curPage >= pageCount)
+ {
+ switchToPage(pageCount - 1);
+ }
+
+ curItemCount = totalItemsCount ? (curPage == pageCount - 1 && rem) ? rem : ItemsPerPage : 0;
+
+ endResetModel();
+ }
+}
+
+void StopCouplingModel::setStop(db_id stopId, RsOp op)
+{
+ m_stopId = stopId;
+ m_operation = op;
+
+ refreshData();
+ clearCache();
+}
+
+void StopCouplingModel::internalFetch(int first, int sortCol, int valRow, const QVariant& val)
+{
+ query q(mDb);
+
+ int offset = first - valRow + curPage * ItemsPerPage;
+ bool reverse = false;
+
+ if(valRow > first)
+ {
+ offset = 0;
+ reverse = true;
+ }
+
+ //const char *whereCol;
+
+ QByteArray sql = "SELECT coupling.rsId,rs_list.number,rs_models.name,rs_models.suffix,rs_models.type"
+ " FROM coupling"
+ " JOIN rs_list ON rs_list.id=coupling.rsId"
+ " LEFT JOIN rs_models ON rs_models.id=rs_list.model_id"
+ " WHERE coupling.stopId=?2 AND coupling.operation=?3"
+ " ORDER BY rs_models.type,rs_models.name,rs_list.number,rs_models.suffix";
+ // switch (sortCol)
+ // {
+ // case Name:
+ // {
+ // whereCol = "name"; //Order by 2 columns, no where clause
+ // break;
+ // }
+ // }
+
+ // if(val.isValid())
+ // {
+ // sql += " WHERE ";
+ // sql += whereCol;
+ // if(reverse)
+ // sql += "3";
+ // else
+ // sql += ">?3";
+ // }
+
+ // sql += " ORDER BY ";
+ // sql += whereCol;
+
+ // if(reverse)
+ // sql += " DESC";
+
+ sql += " LIMIT ?1";
+ if(offset)
+ sql += " OFFSET ?2";
+
+ q.prepare(sql);
+ q.bind(1, BatchSize);
+ q.bind(2, m_stopId);
+ q.bind(3, m_operation);
+ if(offset)
+ q.bind(2, offset);
+
+ if(val.isValid())
+ {
+ switch (sortCol)
+ {
+ case Name:
+ {
+ q.bind(3, val.toString());
+ break;
+ }
+ }
+ }
+
+ QVector vec(BatchSize);
+
+ auto it = q.begin();
+ const auto end = q.end();
+
+ if(reverse)
+ {
+ int i = BatchSize - 1;
+
+ for(; it != end; ++it)
+ {
+ auto r = *it;
+ RSItem &item = vec[i];
+ item.rsId = r.get(0);
+
+ int number = r.get(1);
+ int modelNameLen = sqlite3_column_bytes(q.stmt(), 2);
+ const char *modelName = reinterpret_cast(sqlite3_column_text(q.stmt(), 2));
+
+ int modelSuffixLen = sqlite3_column_bytes(q.stmt(), 3);
+ const char *modelSuffix = reinterpret_cast(sqlite3_column_text(q.stmt(), 3));
+ item.type = RsType(sqlite3_column_int(q.stmt(), 4));
+
+ item.name = rs_utils::formatNameRef(modelName, modelNameLen,
+ number,
+ modelSuffix, modelSuffixLen,
+ item.type);
+ i--;
+ }
+ if(i > -1)
+ vec.remove(0, i + 1);
+ }
+ else
+ {
+ int i = 0;
+
+ for(; it != end; ++it)
+ {
+ auto r = *it;
+ RSItem &item = vec[i];
+ item.rsId = r.get(0);
+
+ int number = r.get(1);
+ int modelNameLen = sqlite3_column_bytes(q.stmt(), 2);
+ const char *modelName = reinterpret_cast(sqlite3_column_text(q.stmt(), 2));
+
+ int modelSuffixLen = sqlite3_column_bytes(q.stmt(), 3);
+ const char *modelSuffix = reinterpret_cast(sqlite3_column_text(q.stmt(), 3));
+ item.type = RsType(sqlite3_column_int(q.stmt(), 4));
+
+ item.name = rs_utils::formatNameRef(modelName, modelNameLen,
+ number,
+ modelSuffix, modelSuffixLen,
+ item.type);
+ i++;
+ }
+ if(i < BatchSize)
+ vec.remove(i, BatchSize - i);
+ }
+
+
+ RSListOnDemandModelResultEvent *ev = new RSListOnDemandModelResultEvent;
+ ev->items = vec;
+ ev->firstRow = first;
+
+ qApp->postEvent(this, ev);
+}
diff --git a/src/jobs/jobeditor/model/stopcouplingmodel.h b/src/jobs/jobeditor/model/stopcouplingmodel.h
new file mode 100644
index 0000000..7508d1e
--- /dev/null
+++ b/src/jobs/jobeditor/model/stopcouplingmodel.h
@@ -0,0 +1,29 @@
+#ifndef STOPCOUPLINGMODEL_H
+#define STOPCOUPLINGMODEL_H
+
+#include "rslistondemandmodel.h"
+
+
+class StopCouplingModel : public RSListOnDemandModel
+{
+ Q_OBJECT
+
+public:
+ StopCouplingModel(sqlite3pp::database &db, QObject *parent = nullptr);
+
+ // IPagedItemModel
+ // Cached rows management
+ virtual void refreshData() override;
+
+ // StopCouplingModel
+ void setStop(db_id stopId, RsOp op);
+
+private:
+ virtual void internalFetch(int first, int sortColumn, int valRow, const QVariant &val) override;
+
+private:
+ db_id m_stopId;
+ RsOp m_operation;
+};
+
+#endif // STOPCOUPLINGMODEL_H
diff --git a/src/jobs/jobeditor/model/stopmodel.cpp b/src/jobs/jobeditor/model/stopmodel.cpp
new file mode 100644
index 0000000..5f15141
--- /dev/null
+++ b/src/jobs/jobeditor/model/stopmodel.cpp
@@ -0,0 +1,2687 @@
+#include "stopmodel.h"
+
+#include
+
+#include "app/scopedebug.h"
+
+#include "lines/helpers.h"
+#include "lines/linestorage.h"
+
+#include "app/session.h"
+
+#include
+
+StopModel::StopModel(database &db, QObject *parent) :
+ QAbstractListModel(parent),
+ mJobId(0),
+ mNewJobId(0),
+ jobShiftId(0),
+ newShiftId(0),
+ category(JobCategory::FREIGHT),
+ oldCategory(JobCategory::FREIGHT),
+ editState(NotEditing),
+ mDb(db),
+ q_segPos(mDb),
+ q_getRwNode(mDb),
+ q_lineHasSt(mDb),
+ q_getCoupled(mDb),
+ q_setArrival(mDb),
+ q_setDeparture(mDb),
+ q_setSegPos(mDb),
+ q_setSegLine(mDb),
+ q_setStopSeg(mDb),
+ q_setNextSeg(mDb),
+ q_setStopSt(mDb),
+ q_removeSeg(mDb),
+ q_setPlatform(mDb),
+ timeCalcEnabled(true),
+ autoInsertTransits(false),
+ autoMoveUncoupleToNewLast(true),
+ autoUncoupleAtLast(true)
+{
+ reloadSettings();
+ connect(&AppSettings, &TrainTimetableSettings::stopOptionsChanged, this, &StopModel::reloadSettings);
+ connect(Session, &MeetingSession::shiftJobsChanged, this, &StopModel::onExternalShiftChange);
+ connect(Session, &MeetingSession::shiftNameChanged, this, &StopModel::onShiftNameChanged);
+
+ auto lines = Session->mLineStorage;
+ connect(lines, &LineStorage::lineNameChanged, this, &StopModel::onStationLineNameChanged);
+ connect(lines, &LineStorage::stationNameChanged, this, &StopModel::onStationLineNameChanged);
+}
+
+void StopModel::prepareQueries()
+{
+
+
+ q_segPos.prepare("SELECT num FROM jobsegments WHERE id=?");
+
+ q_getRwNode.prepare("SELECT railways.id FROM railways"
+ " JOIN jobsegments ON jobsegments.id=?"
+ " WHERE railways.stationId=? AND railways.lineId=jobsegments.lineId");
+
+ q_lineHasSt.prepare("SELECT * FROM railways WHERE lineId=? AND stationId=?");
+
+ q_getCoupled.prepare("SELECT rsId, operation FROM coupling WHERE stopId=?");
+
+
+
+
+
+ q_setArrival.prepare("UPDATE stops SET arrival=? WHERE id=?");
+ q_setDeparture.prepare("UPDATE stops SET departure=? WHERE id=?");
+
+ q_setSegPos.prepare("UPDATE jobsegments SET num=num+? WHERE jobId=? AND num>=?");
+
+ q_setSegLine.prepare("UPDATE jobsegments SET lineId=? WHERE id=?");
+
+ q_setStopSeg.prepare("UPDATE stops SET segmentId=?, rw_node=? WHERE id=?");
+
+ q_setNextSeg.prepare("UPDATE stops SET otherSegment=?, other_rw_node=? WHERE id=?");
+
+ q_setStopSt.prepare("UPDATE stops SET stationId=?, rw_node=? WHERE id=?");
+
+ q_removeSeg.prepare("DELETE FROM jobsegments WHERE id=?");
+
+ q_setPlatform.prepare("UPDATE stops SET platform=? WHERE id=?");
+
+}
+
+void StopModel::finalizeQueries()
+{
+ q_segPos.finish();
+ q_getRwNode.finish();
+ q_lineHasSt.finish();
+ q_getCoupled.finish();
+
+ q_setArrival.finish();
+ q_setDeparture.finish();
+
+ q_setSegPos.finish();
+ q_setSegLine.finish();
+ q_setStopSeg.finish();
+ q_setNextSeg.finish();
+ q_setStopSt.finish();
+ q_removeSeg.finish();
+ q_setPlatform.finish();
+}
+
+void StopModel::reloadSettings()
+{
+ setAutoInsertTransits(AppSettings.getAutoInsertTransits());
+ setAutoMoveUncoupleToNewLast(AppSettings.getAutoShiftLastStopCouplings());
+ setAutoUncoupleAtLast(AppSettings.getAutoUncoupleAtLastStop());
+}
+
+const QSet &StopModel::getStationsToUpdate() const
+{
+ return stationsToUpdate;
+}
+
+LineType StopModel::getLineTypeAfterStop(db_id stopId) const
+{
+ int row = getStopRow(stopId);
+ if(row == -1)
+ return LineType(-1); //Error
+
+ const StopItem& item = stops.at(row);
+ query q(mDb, "SELECT type FROM lines WHERE id=?");
+
+ if(item.nextLine)
+ q.bind(1, item.nextLine);
+ else
+ q.bind(1, item.curLine);
+ if(q.step() != SQLITE_ROW)
+ return LineType(-1); //Error
+
+ LineType type = LineType(q.getRows().get(0));
+ return type;
+}
+
+void StopModel::uncoupleStillCoupledAtLastStop()
+{
+ if(!autoUncoupleAtLast)
+ return;
+
+ for(int i = stops.size() - 1; i >= 0; i--)
+ {
+ const StopItem& s = stops.at(i);
+ if(s.addHere != 0 || !s.stationId)
+ continue;
+ uncoupleStillCoupledAtStop(s);
+
+ //Select them to update them
+ query q_selectMoved(mDb, "SELECT rsId FROM coupling WHERE stopId=?");
+ q_selectMoved.bind(1, s.stopId);
+ for(auto rs : q_selectMoved)
+ {
+ db_id rsId = rs.get(0);
+ rsToUpdate.insert(rsId);
+ }
+ break;
+ }
+}
+
+void StopModel::uncoupleStillCoupledAtStop(const StopItem& s)
+{
+ //Uncouple all still-coupled RS
+ command q_uncoupleRS(mDb, "INSERT OR IGNORE INTO coupling(id,rsId,stopId,operation) VALUES(NULL,?,?,0)");
+ query q_selectStillOn(mDb, "SELECT coupling.rsId,MAX(stops.arrival)"
+ " FROM stops"
+ " JOIN coupling ON coupling.stopId=stops.id"
+ " WHERE stops.jobId=?1 AND stops.arrival2"
+ " GROUP BY coupling.rsId"
+ " HAVING coupling.operation=1");
+ q_selectStillOn.bind(1, mJobId);
+ q_selectStillOn.bind(2, s.arrival);
+ for(auto rs : q_selectStillOn)
+ {
+ db_id rsId = rs.get(0);
+ rsToUpdate.insert(rsId);
+ q_uncoupleRS.bind(1, rsId);
+ q_uncoupleRS.bind(2, s.stopId);
+ q_uncoupleRS.execute();
+ q_uncoupleRS.reset();
+ }
+}
+
+bool StopModel::getStationPlatfCount(db_id stationId, int &platfCount, int &depotCount)
+{
+ query q_getPlatfs(mDb, "SELECT platforms,depot_platf FROM stations WHERE id=?");
+ q_getPlatfs.bind(1, stationId);
+ if(q_getPlatfs.step() != SQLITE_ROW)
+ {
+ //Error
+ return false;
+ }
+ auto r = q_getPlatfs.getRows();
+ platfCount = r.get(0);
+ depotCount = r.get(1);
+ return true;
+}
+
+const QSet &StopModel::getRsToUpdate() const
+{
+ return rsToUpdate;
+}
+
+int StopModel::rowCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : stops.count();
+}
+
+Qt::ItemFlags StopModel::flags(const QModelIndex &index) const
+{
+ return QAbstractListModel::flags(index) | Qt::ItemIsEditable;
+}
+
+QVariant StopModel::data(const QModelIndex &index, int role) const
+{
+ if(!index.isValid() || index.row() >= stops.count() || index.column() > 0)
+ return QVariant();
+
+ const StopItem& s = stops.at(index.row());
+
+ switch (role) {
+ case JOB_ID_ROLE:
+ return mJobId;
+ case JOB_SHIFT_ID:
+ return jobShiftId;
+ case JOB_CATEGORY_ROLE:
+ return int(category);
+ case STOP_ID:
+ return s.stopId;
+ case STATION_ID:
+ case STATION_ROLE:
+ return s.stationId;
+ case STOP_TYPE_ROLE:
+ return int(s.type);
+ case ARR_ROLE:
+ return s.arrival;
+ case DEP_ROLE:
+ return s.departure;
+ case SEGMENT_ROLE:
+ return s.segment;
+ case OTHER_SEG_ROLE:
+ return s.nextSegment;
+ case CUR_LINE_ROLE:
+ return s.curLine;
+ case NEXT_LINE_ROLE:
+ return s.nextLine;
+ case POSSIBLE_LINE_ROLE:
+ return s.possibleLine;
+ case ADDHERE_ROLE:
+ return s.addHere;
+ case PLATF_ID:
+ return s.platform;
+ case STOP_DESCR_ROLE:
+ return getDescription(s);
+ default:
+ break;
+ }
+
+ return QVariant();
+}
+
+bool StopModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ //DEBUG_ENTRY;
+ if(!index.isValid() || index.row() >= stops.count() || index.column() > 0)
+ return false;
+
+ switch (role)
+ {
+ case STATION_ID:
+ case STATION_ROLE:
+ setStation(index, value.toLongLong());
+ break;
+ case STOP_TYPE_ROLE:
+ setStopType(index, StopType(value.toInt()));
+ break;
+ case ARR_ROLE:
+ setArrival(index, value.toTime(), false);
+ break;
+ case DEP_ROLE:
+ setDeparture(index, value.toTime(), true);
+ break;
+ case NEXT_LINE_ROLE:
+ setLine(index, value.toLongLong());
+ break;
+ case PLATF_ID:
+ setPlatform(index, value.toInt());
+ break;
+ case STOP_DESCR_ROLE:
+ setDescription(index, value.toString());
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+void StopModel::setArrival(const QModelIndex& idx, const QTime& time, bool setDepTime)
+{
+ StopItem& s = stops[idx.row()];
+ if(s.type == First || s.arrival == time)
+ return;
+
+ //Cannot arrive at the same time (or even before) the departure from previous station
+ //You shouldn't get here because StopEditor and EditStopDialog already check this
+ //but it's correct to have it checked also in the model
+ QTime minArrival = stops[idx.row() - 1].departure.addSecs(60);
+ if(time < minArrival)
+ return;
+
+ startStopsEditing();
+
+ //Mark the station for update
+ stationsToUpdate.insert(s.stationId);
+
+ const QTime oldArr = s.arrival;
+ s.arrival = time;
+
+ if(s.type == Last && s.departure != time)
+ {
+ s.departure = time;
+
+ q_setDeparture.bind(1, time);
+ q_setDeparture.bind(2, s.stopId);
+ q_setDeparture.execute();
+ q_setDeparture.reset();
+ }
+ else
+ {
+ if(setDepTime || s.arrival >= s.departure || s.type == Transit || s.type == TransitLineChange)
+ shiftStopsBy24hoursFrom(oldArr);
+ }
+
+ q_setArrival.bind(1, time);
+ q_setArrival.bind(2, s.stopId);
+ q_setArrival.execute();
+ q_setArrival.reset();
+
+
+ if(s.type == Transit || s.type == TransitLineChange)
+ {
+ setDeparture(idx, s.arrival, true);
+ }
+ else if(setDepTime)
+ {
+ //Shift Departure by the same amount of time to preserve same stop time
+ const int diff = oldArr.secsTo(time);
+ setDeparture(idx, s.departure.addSecs(diff), true);
+ }
+
+ if(s.arrival > s.departure || (s.arrival == s.departure && s.type != Transit && s.type != TransitLineChange))
+ {
+ int secs = defaultStopTimeSec();
+
+ if(secs == 0 && s.type != Last)
+ {
+ //If secs is 0, trasform stop in Transit unless it's Last stop
+ setStopType(idx, Transit);
+ }
+ else
+ {
+ setDeparture(idx, time.addSecs(secs), true);
+ }
+ }
+
+ emit dataChanged(idx, idx);
+}
+
+void StopModel::setDeparture(const QModelIndex& idx, QTime time, bool propagate)
+{
+ StopItem& s = stops[idx.row()];
+ //Don't allow departure < arrival unless it's First Stop:
+ //In First stops setting departure sets also arrival
+ //So initially departure < arrival but then arrival = departure
+ //And the proble disappears
+ if(s.type == Last || s.departure == time)
+ return;
+
+ if(s.type != First) //Allow to set Departure on First stop (which updates Arrival)
+ {
+ QTime minDep = s.arrival; //For transits
+ if(s.type == Normal)
+ minDep = minDep.addSecs(60); //Normal stops, stop for at least 1 minute
+ if(time < minDep)
+ return; //Invalid Departure time
+ }
+
+ startStopsEditing();
+
+ //Mark the station for update
+ stationsToUpdate.insert(s.stationId);
+
+ if(s.type == Transit || s.type == TransitLineChange)
+ time = s.arrival; //On transits stop time is 0 minutes so Departure is same of Arrival
+
+ if(s.type == First && s.arrival != time)
+ {
+ s.arrival = time;
+
+ q_setArrival.bind(1, time);
+ q_setArrival.bind(2, s.stopId);
+ q_setArrival.execute();
+ q_setArrival.reset();
+ }
+
+ if(idx.row() < stops.count() - 1) //Not last stop
+ {
+ StopItem& next = stops[idx.row() + 1];
+ if(next.addHere == 0 && time >= s.arrival)
+ propagate = true;
+ }
+
+ if(propagate)
+ shiftStopsBy24hoursFrom(s.arrival);
+
+ q_setDeparture.bind(1, time);
+ q_setDeparture.bind(2, s.stopId);
+ q_setDeparture.execute();
+ q_setDeparture.reset();
+
+ int offset = s.departure.msecsTo(time);
+ s.departure = time;
+
+ QModelIndex finalIdx;
+ if(propagate)
+ {
+ int r = propageteTimeOffset(idx.row() + 1, offset);
+ finalIdx = index(r - 1, 0);
+ }
+ if(!finalIdx.isValid())
+ finalIdx = idx;
+
+ emit dataChanged(idx, finalIdx);
+}
+
+int StopModel::propageteTimeOffset(int row, const int msecOffset)
+{
+ if(row >= stops.count() || msecOffset == 0)
+ return row;
+
+ if(row > 0) //Not first
+ shiftStopsBy24hoursFrom(stops.at(row - 1).arrival);
+
+ while (row < stops.count())
+ {
+ StopItem& s = stops[row];
+ if(s.addHere != 0)
+ {
+ row++;
+ continue;
+ }
+
+ s.arrival = s.arrival.addMSecs(msecOffset);
+
+ q_setArrival.bind(1, s.arrival);
+ q_setArrival.bind(2, s.stopId);
+ q_setArrival.execute();
+ q_setArrival.reset();
+
+ s.departure = s.departure.addMSecs(msecOffset);
+
+ q_setDeparture.bind(1, s.departure);
+ q_setDeparture.bind(2, s.stopId);
+ q_setDeparture.execute();
+ q_setDeparture.reset();
+
+ row++;
+ }
+
+ return row;
+}
+
+//Returns error codes of type ErrorCodes
+int StopModel::setStopType(const QModelIndex& idx, StopType type)
+{
+ if(!idx.isValid() || idx.row() >= stops.count())
+ return ErrorInvalidIndex;
+ StopItem& s = stops[idx.row()];
+
+ if(s.type == First || s.type == Last)
+ {
+ qWarning() << "Error: tried change type of First/Last stop:" << idx << s.stopId << "Job:" << mJobId;
+ return ErrorFirstLastTransit;
+ }
+
+ //Fix possible wrong transit types
+ if(s.nextLine && type == Transit)
+ type = TransitLineChange;
+ else if (s.nextLine == 0 && type == TransitLineChange)
+ type = Transit;
+
+ if(s.type == type)
+ return NoError;
+
+ //Cannot couple or uncouple in transits
+ if(type == Transit || type == TransitLineChange)
+ {
+ q_getCoupled.bind(1, s.stopId);
+ int res = q_getCoupled.step();
+ q_getCoupled.reset();
+
+ if(res == SQLITE_ROW)
+ {
+ qWarning() << "Error: trying to set Transit on stop:" << s.stopId << "Job:" << mJobId
+ << "while having coupling operation for this stop";
+ return ErrorTransitWithCouplings;
+ }
+ if(res != SQLITE_OK && res != SQLITE_DONE)
+ {
+ qWarning() << "Error while setting stopType for stop:" << s.stopId << "Job:" << mJobId
+ << "DB Err:" << res << mDb.error_msg() << mDb.extended_error_code();
+ }
+ }
+
+ startStopsEditing();
+
+ //Mark the station for update
+ stationsToUpdate.insert(s.stationId);
+
+
+ command q_setTransitType(mDb, "UPDATE stops SET transit=? WHERE id=?");
+
+ if(type == Transit || type == TransitLineChange)
+ q_setTransitType.bind(1, Transit); //1 = Transit
+ else
+ q_setTransitType.bind(1, Normal); //0 = Normal
+ q_setTransitType.bind(2, s.stopId);
+ q_setTransitType.execute();
+ q_setTransitType.finish();
+
+ s.type = type;
+
+ if(s.type == Transit || s.type == TransitLineChange)
+ {
+ //Transit don't stop so departure is the same of arrival -> stop time = 0 minutes
+ setDeparture(idx, s.arrival, true);
+ }
+ else
+ {
+ if(s.arrival == s.departure)
+ {
+ int stopSecs = qMax(60, defaultStopTimeSec()); //Default stop time but at least 1 minute
+ setDeparture(idx, s.arrival.addSecs(stopSecs), true);
+ }
+ }
+
+ //TODO: should be already emitted by setDeparture()
+ emit dataChanged(idx, idx);
+
+ return NoError;
+}
+
+//Returns error codes of type ErrorCodes
+int StopModel::setStopTypeRange(int firstRow, int lastRow, StopType type)
+{
+ if(firstRow < 0 || firstRow > lastRow || lastRow >= stops.count())
+ return ErrorInvalidIndex;
+
+ if(type == First || type == Last)
+ return ErrorInvalidArgument;
+
+ if(type == TransitLineChange)
+ type = Transit;
+
+ int defaultStopMsec = qMax(60, defaultStopTimeSec()) * 1000; //At least 1 minute
+
+ StopType destType = type;
+
+ shiftStopsBy24hoursFrom(stops.at(firstRow).arrival);
+
+ command q_setArrDep(mDb, "UPDATE stops SET arrival=?,departure=? WHERE id=?");
+ command q_setTransitType(mDb, "UPDATE stops SET transit=? WHERE id=?");
+
+ int msecOffset = 0;
+
+ for(int r = firstRow; r <= lastRow; r++)
+ {
+ StopItem& s = stops[r];
+
+ if(s.type == First || s.type == Last)
+ {
+ qWarning() << "Error: tried change type of First/Last stop:" << r << s.stopId << "Job:" << mJobId;
+
+ //Always update time even if msecOffset == 0, because they have been shifted
+ s.arrival = s.arrival.addMSecs(msecOffset);
+ s.departure = s.departure.addMSecs(msecOffset);
+ q_setArrDep.bind(1, s.arrival);
+ q_setArrDep.bind(2, s.departure);
+ q_setArrDep.bind(3, s.stopId);
+ q_setArrDep.execute();
+ q_setArrDep.reset();
+ continue;
+ }
+
+ if(type == ToggleType)
+ {
+ if(s.type == Normal)
+ destType = Transit;
+ else
+ destType = Normal;
+ }
+
+ //Cannot couple or uncouple in transits
+ if(destType == Transit)
+ {
+ q_getCoupled.bind(1, s.stopId);
+ int res = q_getCoupled.step();
+ q_getCoupled.reset();
+
+ if(res == SQLITE_ROW)
+ {
+ qWarning() << "Error: trying to set Transit on stop:" << s.stopId << "Job:" << mJobId
+ << "while having coupling operation for this stop";
+ continue;
+ }
+ if(res != SQLITE_OK && res != SQLITE_DONE)
+ {
+ qWarning() << "Error while setting stopType for stop:" << s.stopId << "Job:" << mJobId
+ << "DB Err:" << res << mDb.error_msg() << mDb.extended_error_code();
+ return ErrorInvalidArgument;
+ }
+ }
+
+ startStopsEditing();
+
+ //Mark the station for update
+ stationsToUpdate.insert(s.stationId);
+
+ if(destType == Transit)
+ q_setTransitType.bind(1, Transit); //1 = Transit
+ else
+ q_setTransitType.bind(1, Normal); //0 = Normal
+ q_setTransitType.bind(2, s.stopId);
+ q_setTransitType.execute();
+ q_setTransitType.reset();
+
+ s.arrival = s.arrival.addMSecs(msecOffset);
+ s.departure = s.departure.addMSecs(msecOffset);
+
+ if(destType == Normal)
+ {
+ s.type = Normal;
+
+ if(s.arrival == s.departure)
+ {
+ msecOffset += defaultStopMsec;
+ s.departure = s.arrival.addMSecs(defaultStopMsec);
+ }
+ }
+ else
+ {
+ s.type = s.nextLine ? TransitLineChange : Transit;
+ //Transit don't stop so departure is the same of arrival -> stop time = 0 minutes
+ msecOffset -= s.arrival.msecsTo(s.departure);
+ s.departure = s.arrival;
+ }
+ q_setArrDep.bind(1, s.arrival);
+ q_setArrDep.bind(2, s.departure);
+ q_setArrDep.bind(3, s.stopId);
+ q_setArrDep.execute();
+ q_setArrDep.reset();
+ }
+
+ QModelIndex firstIdx = index(firstRow, 0);
+ QModelIndex lastIdx = index(stops.count() - 1, 0);
+
+ //Always update time even if msecOffset == 0, because they have been shifted
+ for(int r = lastRow + 1; r < stops.count(); r++)
+ {
+ StopItem& s = stops[r];
+ s.arrival = s.arrival.addMSecs(msecOffset);
+ s.departure = s.departure.addMSecs(msecOffset);
+ q_setArrDep.bind(1, s.arrival);
+ q_setArrDep.bind(2, s.departure);
+ q_setArrDep.bind(3, s.stopId);
+ q_setArrDep.execute();
+ q_setArrDep.reset();
+ }
+
+
+
+ //TODO: should be already emitted by setDeparture()
+ emit dataChanged(firstIdx, lastIdx);
+
+ return NoError;
+}
+
+void StopModel::loadJobStops(db_id jobId)
+{
+ DEBUG_ENTRY;
+
+ beginResetModel();
+ stops.clear();
+ rsToUpdate.clear();
+ stationsToUpdate.clear();
+
+ mNewJobId = mJobId = jobId;
+ emit jobIdChanged(mJobId);
+
+ {
+ query q_getCatAndShift(mDb, "SELECT category,shiftId FROM jobs WHERE id=?");
+ q_getCatAndShift.bind(1, mJobId);
+ if(q_getCatAndShift.step() != SQLITE_ROW)
+ {
+ //Error: job not existent???
+ }
+
+ auto r = q_getCatAndShift.getRows();
+ oldCategory = category = JobCategory(r.get(0));
+ emit categoryChanged(int(category));
+
+ jobShiftId = newShiftId = r.get(1);
+ emit jobShiftChanged(jobShiftId);
+ }
+
+
+
+ query q_countStops(mDb, "SELECT COUNT(id) FROM stops WHERE jobId=?");
+ q_countStops.bind(1, mJobId);
+ if(q_countStops.step() != SQLITE_ROW)
+ {
+ qWarning() << database_error(mDb).what();
+ }
+ int count = q_countStops.getRows().get(0);
+ q_countStops.finish();
+
+ if(count == 0)
+ {
+ endResetModel();
+
+ insertAddHere(0, 1);
+
+ stops.squeeze();
+
+ startStopsEditing();
+
+ return;
+ }
+
+ stops.reserve(count);
+
+ db_id oldLineId = 0;
+ db_id oldSeg = 0;
+ int i = 0;
+
+ query q_selectStops(mDb, "SELECT id,"
+ "stationId,"
+ "arrival,"
+ "departure,"
+ "platform,"
+ "transit,"
+ "otherSegment"
+ " FROM stops"
+ " WHERE segmentId=?1 OR otherSegment=?1"
+ " ORDER BY arrival ASC");
+
+ query q_selectSegments(mDb, "SELECT id, lineId FROM jobsegments WHERE jobId=? ORDER BY num ASC");
+ q_selectSegments.bind(1, mJobId);
+ for(auto segment : q_selectSegments)
+ {
+ db_id segId = segment.get(0);
+ db_id lineId = segment.get(1);
+
+ q_selectStops.bind(1, segId);
+ for(auto stop : q_selectStops)
+ {
+ db_id otherSeg = stop.get(6);
+ if(otherSeg != 0 && otherSeg != segId)
+ {
+ //It's last of this segment but we show it as first of next one
+ oldSeg = segId;
+ oldLineId = lineId;
+ continue;
+ }
+
+ StopItem s;
+ s.possibleLine = 0;
+ s.addHere = 0;
+
+ s.stopId = stop.get(0);
+ s.stationId = stop.get(1);
+
+ s.arrival = stop.get(2);
+ s.departure = stop.get(3);
+
+ s.platform = stop.get(4);
+
+ int transit = stop.get(5);
+
+ qDebug() << "Got" << s.stationId << s.arrival << s.departure;
+
+ if(i == 0) //First
+ {
+ s.segment = segId;
+ s.curLine = 0;
+ s.nextLine = lineId;
+ }
+ else if(otherSeg == segId)
+ {
+ //First of new Segment
+ s.segment = oldSeg; //cur is the old one
+ s.curLine = oldLineId;
+ s.nextSegment = segId; //next is the new one
+ s.nextLine = lineId;
+ }
+ else
+ {
+ s.curLine = lineId;
+ s.segment = segId;
+ s.nextSegment = otherSeg;
+ }
+
+ StopType type = Normal;
+
+ if(i == 0)
+ {
+ type = First;
+ if(transit != 0)
+ {
+ //Error First cannot be a transit
+ qWarning() << "Error: First stop cannot be transit! Job:" << mJobId << "StopId:" << s.stopId;
+ }
+ }
+ else if (transit)
+ {
+ if(s.nextLine)
+ {
+ type = TransitLineChange;
+ }
+ else
+ {
+ type = Transit;
+ }
+
+ if(i == count - 1)
+ {
+ //Error Last cannot be a transit
+ qWarning() << "Error: Last stop cannot be transit! Job:" << mJobId << "StopId:" << s.stopId;
+ }
+ }
+ s.type = type;
+
+ //Load rollingstok
+ q_getCoupled.bind(1, s.stopId);
+ for(auto rs : q_getCoupled)
+ {
+ db_id rsId = rs.get(0);
+ int op = rs.get(1);
+
+ if(op == Coupled)
+ s.coupled.insert(rsId);
+ else
+ s.uncoupled.insert(rsId);
+ }
+ q_getCoupled.reset();
+
+ stops.append(s);
+ i++;
+ }
+ q_selectStops.reset();
+ }
+ q_selectSegments.finish();
+ q_selectStops.finish();
+
+ stops.last().type = Last;
+
+ endResetModel();
+
+ insertAddHere(stops.count(), 1);
+
+ if(stops.count() < 3) //First + Last + AddHere
+ startStopsEditing(); //Set editied to enable deletion if user clicks Cancel
+
+ stops.squeeze();
+ rsToUpdate.squeeze();
+ stationsToUpdate.squeeze();
+}
+
+void StopModel::clearJob()
+{
+ mJobId = mNewJobId = 0;
+ emit jobIdChanged(mJobId);
+
+ jobShiftId = newShiftId = 0;
+ emit jobShiftChanged(jobShiftId);
+
+ oldCategory = category = JobCategory(-1);
+ emit categoryChanged(int(category));
+
+ int count = stops.count();
+ if(count == 0)
+ return;
+ beginRemoveRows(QModelIndex(), 0, count - 1);
+
+ stops.clear();
+ stops.squeeze();
+
+ endRemoveRows();
+}
+
+void StopModel::insertAddHere(int row, int type)
+{
+ DEBUG_ENTRY;
+
+ beginInsertRows(QModelIndex(), row, row);
+
+ StopItem s;
+ s.addHere = type;
+ stops.insert(row, s);
+
+ endInsertRows();
+}
+
+db_id StopModel::createStop(db_id jobId, db_id segId, const QTime& time, int transit)
+{
+ command q_addStop(mDb, "INSERT INTO stops"
+ "(id,jobId,stationId,arrival,departure,platform,transit,description,segmentId,otherSegment)"
+ " VALUES (NULL,?,NULL,?,?,?,?,NULL,?,NULL)");
+ q_addStop.bind(1, jobId);
+ q_addStop.bind(2, time);
+ q_addStop.bind(3, time);
+ q_addStop.bind(4, 0); //Platform
+ q_addStop.bind(5, transit); //Transit
+ q_addStop.bind(6, segId);
+
+ sqlite3_mutex *mutex = sqlite3_db_mutex(mDb.db());
+ sqlite3_mutex_enter(mutex);
+ q_addStop.execute();
+ db_id stopId = mDb.last_insert_rowid();
+ sqlite3_mutex_leave(mutex);
+ q_addStop.reset();
+
+ return stopId;
+}
+
+db_id StopModel::createSegment(db_id jobId, int num)
+{
+ command q_addSegment(mDb, "INSERT INTO jobsegments(id,jobId,lineId,num) VALUES (NULL,?,NULL,?)");
+ q_addSegment.bind(1, jobId);
+ q_addSegment.bind(2, num);
+
+ sqlite3_mutex *mutex = sqlite3_db_mutex(mDb.db());
+ sqlite3_mutex_enter(mutex);
+ q_addSegment.execute();
+ db_id segId = mDb.last_insert_rowid();
+ sqlite3_mutex_leave(mutex);
+
+ q_addSegment.reset();
+
+ return segId;
+}
+
+db_id StopModel::createSegmentAfter(db_id jobId, db_id prevSeg)
+{
+ q_segPos.bind(1, prevSeg);
+ q_segPos.step();
+
+ /*Get seg pos and then increment by 1 to get nextSeg pos*/
+ int pos = q_segPos.getRows().get(0) + 1;
+ q_segPos.reset();
+
+ q_setSegPos.bind(1, 1); //Shift by +1
+ q_setSegPos.bind(2, jobId);
+ q_setSegPos.bind(3, pos);
+ q_setSegPos.execute();
+ q_setSegPos.reset();
+
+ return createSegment(jobId, pos);
+}
+
+void StopModel::destroySegment(db_id segId, db_id jobId)
+{
+ q_segPos.bind(1, segId); //Get segment pos, increment to get next segments
+ if(q_segPos.step() != SQLITE_ROW)
+ {
+ qWarning() << "JobId" << jobId << "Segment:" << segId << "Err: tryed to destroy segment that doesn't exist";
+ q_segPos.reset();
+ return;
+ }
+ int pos = q_segPos.getRows().get(0) + 1;
+ q_segPos.reset();
+
+ q_removeSeg.bind(1, segId); //Remove segment
+ int ret = q_removeSeg.execute();
+ if(ret != SQLITE_OK)
+ {
+ qWarning() << "DB err:" << ret << mDb.error_code() << mDb.error_msg() << mDb.extended_error_code();
+ }
+ q_removeSeg.reset();
+
+ q_setSegPos.bind(1, -1); //Shift by -1 to fill hole in 'num' column
+ q_setSegPos.bind(2, jobId);
+ q_setSegPos.bind(3, pos);
+ q_setSegPos.execute();
+ q_setSegPos.reset();
+}
+
+void StopModel::deleteStop(db_id stopId)
+{
+ command q_removeStop(mDb, "DELETE FROM stops WHERE id=?");
+ q_removeStop.bind(1, stopId);
+ int ret = q_removeStop.execute();
+ if(ret != SQLITE_OK)
+ {
+ qWarning() << "DB err:" << ret << mDb.error_code() << mDb.error_msg() << mDb.extended_error_code();
+ }
+ q_removeStop.reset();
+}
+
+void StopModel::addStop()
+{
+ DEBUG_IMPORTANT_ENTRY;
+ if(stops.count() == 0)
+ return;
+
+ startStopsEditing();
+
+ int idx = stops.count() - 1;
+ StopItem& last = stops[idx];
+
+ if(last.addHere != 1)
+ {
+ qWarning() << "Error: addStop not an AddHere Item";
+ }
+
+ last.addHere = 0; //Reset AddHere
+
+ int prevIdx = idx - 1;
+ if(prevIdx >= 0) //Has stops before
+ {
+ last.type = Last;
+
+ StopItem& s = stops[prevIdx];
+ if(prevIdx > 0)
+ {
+ s.type = Normal;
+ //Previously 's' was Last so it CANNOT have a NextLine
+ last.curLine = s.curLine;
+
+ int secs = defaultStopTimeSec();
+
+ if(secs == 0 && s.type != Last)
+ {
+ //If secs is 0, trasform stop in Transit unless it's Last stop
+ setStopType(index(prevIdx, 0), Transit);
+ }
+ else
+ {
+ //Don't propagate because we have to initialize last stop before
+ setDeparture(index(prevIdx, 0), s.arrival.addSecs(secs), false);
+ }
+ }
+ else
+ {
+ //First has olny NextLine
+ last.curLine = s.nextLine;
+ }
+
+ /* Next stop must be at least one minute after
+ * This is to prevent contemporary stops that will break ORDER BY arrival queries */
+ const QTime time = s.departure.addSecs(60);
+ last.arrival = time;
+
+ last.stopId = createStop(mJobId, s.segment, last.arrival);
+ last.segment = s.segment;
+ last.departure = last.arrival;
+
+ if(autoMoveUncoupleToNewLast)
+ {
+ if(prevIdx >= 1)
+ {
+ //We are new last stop and previous is not First (>= 1)
+ //Move uncoupled rs from former last stop (now last but one) to new last stop
+ command q_moveUncoupled(mDb, "UPDATE OR IGNORE coupling SET stopId=? WHERE stopId=? AND operation=?");
+ q_moveUncoupled.bind(1, last.stopId);
+ q_moveUncoupled.bind(2, s.stopId);
+ q_moveUncoupled.bind(3, RsOp::Uncoupled);
+ int ret = q_moveUncoupled.execute();
+ if(ret != SQLITE_OK)
+ {
+ qDebug() << "Error shifting uncoupling from stop:" << s.stopId << "to:" << last.stopId << "Job:" << mJobId
+ << "err:" << ret << mDb.error_msg();
+
+ }
+ }
+
+ uncoupleStillCoupledAtStop(last);
+
+ //Select them to update them
+ query q_selectMoved(mDb, "SELECT rsId FROM coupling WHERE stopId=?");
+ q_selectMoved.bind(1, last.stopId);
+ for(auto rs : q_selectMoved)
+ {
+ db_id rsId = rs.get(0);
+ rsToUpdate.insert(rsId);
+ }
+ }
+
+ emit dataChanged(index(prevIdx, 0),
+ index(prevIdx, 0));
+ }
+ else
+ {
+ last.type = First;
+
+ last.arrival = QTime(0, 0);
+ last.departure = last.arrival;
+
+ //Doesn't need 'setStopSeg' because it sets on 'createStop'
+ last.segment = createSegment(mJobId, 0);
+ last.stopId = createStop(mJobId, last.segment, last.arrival);
+ }
+
+ emit dataChanged(index(idx, 0),
+ index(idx, 0));
+
+ insertAddHere(idx + 1, 1); //Insert only at end because may invalidate StopItem& references due to mem realloc
+}
+
+bool StopModel::lineHasSt(db_id lineId, db_id stId)
+{
+ q_lineHasSt.bind(1, lineId);
+ q_lineHasSt.bind(2, stId);
+ int res = q_lineHasSt.step();
+ q_lineHasSt.reset();
+
+ return (res == SQLITE_ROW);
+}
+
+void StopModel::setStopSeg(StopItem& s, db_id segId)
+{
+ s.segment = segId;
+
+ q_getRwNode.bind(1, segId);
+ q_getRwNode.bind(2, s.stationId);
+ q_getRwNode.step();
+ db_id nodeId = q_getRwNode.getRows().get(0);
+ q_getRwNode.reset();
+
+ q_setStopSeg.bind(1, segId);
+ if(nodeId)
+ q_setStopSeg.bind(2, nodeId);
+ else
+ q_setStopSeg.bind(2, null_type{}); //Bind NULL instead of 0
+ q_setStopSeg.bind(3, s.stopId);
+ q_setStopSeg.execute();
+ q_setStopSeg.reset();
+}
+
+void StopModel::setNextSeg(StopItem& s, db_id nextSeg)
+{
+ s.nextSegment = nextSeg;
+ if(nextSeg == 0)
+ {
+ q_setNextSeg.bind(1, null_type{}); //Bind NULL
+ q_setNextSeg.bind(2, null_type{}); //Bind NULL
+ }
+ else
+ {
+ q_getRwNode.bind(1, nextSeg);
+ q_getRwNode.bind(2, s.stationId);
+ q_getRwNode.step();
+ db_id nodeId = q_getRwNode.getRows().get(0);
+ q_getRwNode.reset();
+
+ q_setNextSeg.bind(1, nextSeg);
+ if(nodeId)
+ q_setNextSeg.bind(2, nodeId);
+ else
+ q_setNextSeg.bind(2, null_type{}); //Bind NULL instead of 0
+ }
+
+ q_setNextSeg.bind(3, s.stopId);
+ q_setNextSeg.execute();
+ q_setNextSeg.reset();
+}
+
+void StopModel::resetStopsLine(int idx, StopItem& s)
+{
+ //Cannot reset nextLine on First stop
+ if(s.type == First)
+ return;
+
+ startStopsEditing();
+
+ if(s.type == TransitLineChange)
+ s.type = Transit;
+
+ int r = idx + 1;
+ for(; r < stops.count(); r++)
+ {
+ StopItem& stop = stops[r];
+
+ if(s.addHere == 2)
+ {
+ break;
+ }
+ if((stop.curLine != s.curLine && stop.segment != s.nextSegment) || stop.addHere == 1)
+ {
+ break; //It's an AddHere or in another line so we break loop.
+ }
+
+ if(!lineHasSt(s.curLine, stop.stationId) && stop.stationId != 0)
+ {
+ break;
+ }
+
+ stop.curLine = s.curLine;
+
+ db_id oldSegment = stop.segment;
+ setStopSeg(stop, s.segment);
+
+ //Check if next stop is using this segment otherwise delete it.
+ //If we are last stop just delete it.
+ //But if it's equal to s.nextSegment do not delete it because stop 's' still holds a reference to it,
+ //it gets deleted anyway after the loop.
+ if(oldSegment != s.segment && oldSegment != s.nextSegment && (r == stops.count() - 2 || stops[r + 1].segment != oldSegment))
+ {
+ destroySegment(oldSegment, mJobId);
+ }
+
+ if(stop.nextLine == s.curLine)
+ {
+ //Reset next segment
+ db_id oldNextSeg = stop.nextSegment;
+ setNextSeg(stop, 0);
+ stop.nextLine = 0;
+
+ //Check if next stop is using this segment otherwise delete it.
+ if(r == stops.count() - 2 || stops[r + 1].segment != oldNextSeg)
+ {
+ destroySegment(oldNextSeg, mJobId);
+ }
+ }
+
+ if((stop.nextLine != s.curLine && stop.nextLine != 0) || stop.stationId == 0)
+ {
+ break;
+ }
+ }
+
+ //First reset next segment on previous stop
+ db_id oldNextSeg = s.nextSegment;
+ setNextSeg(s, 0);
+ s.nextLine = 0;
+
+ //Only now that there aren't any references to s.nextSegment we can destroy it.
+ destroySegment(oldNextSeg, mJobId);
+
+ emit dataChanged(index(idx, 0),
+ index(r, 0));
+}
+
+void StopModel::propagateLineChange(int idx, StopItem& s, db_id lineId)
+{
+ startStopsEditing();
+
+ if(s.type == Transit)
+ s.type = TransitLineChange;
+
+ s.nextLine = lineId;
+ if(s.type == First)
+ {
+ q_setSegLine.bind(1, lineId);
+ q_setSegLine.bind(2, s.segment);
+ q_setSegLine.execute();
+ q_setSegLine.reset();
+
+ //Update rw_node field
+ setStopSeg(s, s.segment);
+ }
+ else
+ {
+ db_id nextSeg = s.nextSegment;
+ if(s.nextSegment == 0)
+ {
+ nextSeg = createSegmentAfter(mJobId, s.segment);
+ }
+
+ q_setSegLine.bind(1, lineId);
+ q_setSegLine.bind(2, nextSeg);
+ q_setSegLine.execute();
+ q_setSegLine.reset();
+
+ //Update other_rw_node field
+ setNextSeg(s, nextSeg);
+ }
+
+ bool endHere = false;
+
+ int r = idx + 1;
+ for(; r < stops.count(); r++)
+ {
+ StopItem& stop = stops[r];
+
+ if(stop.addHere != 0)
+ {
+ endHere = true;
+ break;
+ }
+
+ if(stop.stationId != 0 && !lineHasSt(lineId, stop.stationId))
+ {
+ //Create a new segment for this stop and next on this old segment
+ //Because they are in another line
+
+ db_id oldSeg = stop.segment;
+ db_id newSeg = createSegmentAfter(mJobId, oldSeg);
+
+ q_setSegLine.bind(1, stop.curLine);
+ q_setSegLine.bind(2, newSeg);
+ q_setSegLine.execute();
+ q_setSegLine.reset();
+
+ //Point prev stop.next... to this stop
+ StopItem& prev = stops[r - 1];
+ prev.nextLine = stop.curLine;
+ setNextSeg(prev, newSeg);
+
+ setStopSeg(stop, newSeg);
+
+ r += 1;
+ for(; r < stops.count(); r++)
+ {
+ StopItem& next = stops[r];
+ if(next.addHere != 0)
+ {
+ break;
+ }
+
+ setStopSeg(next, newSeg);
+ if(next.nextSegment != 0)
+ {
+ break;
+ }
+ }
+
+ endHere = true;
+ break;
+ }
+
+ stop.curLine = lineId;
+
+ db_id oldCurSeg = stop.segment;
+ setStopSeg(stop, s.type == First ? s.segment : s.nextSegment);
+
+ /*
+ * 'stop' is on same line as 's' so we reset 'stop' nextSegment because it's the same of current segment
+ * then obviously we reset nextLine
+ */
+ if(stop.nextLine == lineId)
+ {
+ stop.nextLine = 0;
+ setNextSeg(stop, 0);
+ }
+ else
+ {
+ bool destroy = oldCurSeg != stop.segment && stop.nextSegment != oldCurSeg;
+ if(destroy && r > 0)
+ {
+ const StopItem& prev = stops[r - 1];
+ if(s.segment == oldCurSeg ||
+ s.nextSegment == oldCurSeg ||
+ prev.segment == oldCurSeg)
+ {
+ destroy = false;
+ }
+ }
+ if(destroy && stop.type != Last)
+ {
+ //If stop isn't Last check if oldCurSeg is used by nextStops
+ if(stops[r + 1].segment == oldCurSeg)
+ destroy = false;
+ }
+
+ if(destroy)
+ {
+ //oldCurSeg is not used anymore by this job
+ destroySegment(oldCurSeg, mJobId);
+ }
+ }
+
+ if(stop.type == Last && stop.nextSegment != 0)
+ {
+ //Clean up if needed (Should not be needed but just in case)
+
+ //If they are different nextSegment isn't used otherwise don't destroy it
+ //because it is used by previous stop
+ if(stop.nextSegment != stop.segment)
+ destroySegment(stop.nextSegment, mJobId);
+ stop.nextLine = 0;
+ stop.nextSegment = 0;
+ }
+
+ if(stop.type == Last || stop.nextLine != 0 || stop.stationId == 0)
+ {
+ endHere = true;
+ break;
+ }
+ }
+
+ if(endHere)
+ return;
+
+ for(int i = r; i < stops.count(); i++)
+ {
+ StopItem& stop = stops[i];
+ if(stop.addHere != 0)
+ break;
+
+ stop.possibleLine = lineId;
+
+ if(stop.nextLine != 0)
+ break;
+ }
+
+ emit dataChanged(index(idx, 0),
+ index(r, 0));
+}
+
+void StopModel::setLine(const QModelIndex& idx, db_id lineId)
+{
+ DEBUG_IMPORTANT_ENTRY;
+ if(!idx.isValid() || idx.row() >= stops.count())
+ return;
+ qDebug() << "Setting line: stop" << idx.row() << "To Id:" << lineId;
+
+ StopItem& s = stops[idx.row()];
+ if(s.type == Last || s.addHere != 0 || s.nextLine == lineId)
+ return;
+
+ if(s.nextLine == 0 && (s.curLine == lineId || lineId == 0))
+ return;
+
+ if(s.nextLine != 0 && (s.curLine == lineId || lineId == 0))
+ {
+ resetStopsLine(idx.row(), s); //Reset
+ }
+ else
+ {
+ propagateLineChange(idx.row(), s, lineId);
+ }
+}
+
+#ifdef ENABLE_AUTO_TIME_RECALC
+void StopModel::rebaseTimesToSpeed(int firstIdx, QTime firstArr, QTime firstDep)
+{
+ //Recalc times from this stop until last, also apply offset with new departure
+ if(firstIdx < 0 || firstIdx >= stops.size() - 2) //At least one before Last stop
+ return; //Error
+
+ startStopsEditing();
+
+ StopItem& firstStop = stops[firstIdx];
+
+ QTime minArrival;
+ if(firstIdx > 0)
+ minArrival = stops.at(firstIdx - 1).departure.addSecs(60);
+
+ if(firstArr < minArrival)
+ {
+ firstDep = firstDep.addSecs(minArrival.secsTo(firstArr));
+ }
+
+ if(firstDep < firstArr)
+ {
+ firstDep = firstArr.addSecs(60); //At least 1 minute stop, it cannot be a transit if we couple RS
+ }
+
+ int offsetSecs = firstStop.departure.secsTo(firstDep);
+ firstStop.arrival = firstArr;
+ firstStop.departure = firstDep;
+
+ query q(Session->m_Db, "SELECT MIN(rs_models.max_speed), rsId FROM("
+ "SELECT coupling.rsId AS rsId, MAX(stops.arrival)"
+ " FROM stops"
+ " JOIN coupling ON coupling.stopId=stops.id"
+ " WHERE stops.jobId=? AND stops.arrival"
+ " GROUP BY coupling.rsId"
+ " HAVING coupling.operation=1)"
+ " JOIN rs_list ON rs_list.id=rsId"
+ " JOIN rs_models ON rs_models.id=rs_list.model_id");
+
+ QTime prevDep = firstDep;
+ db_id prevStId = firstStop.stationId;
+
+ db_id lineId = firstStop.nextLine ? firstStop.nextLine : firstStop.curLine;
+ int lineSpeed = linesModel->getLineSpeed(lineId);
+
+ //size() - 1 to exclude AddHere
+ for(int idx = firstIdx + 1; idx < stops.size() - 1; idx++)
+ {
+ StopItem& item = stops[idx];
+ if(item.curLine != lineId)
+ {
+ lineId = item.curLine;
+ lineSpeed = linesModel->getLineSpeed(lineId);
+ }
+
+ stationsToUpdate.insert(item.stationId);
+ //TODO: should update also RS
+
+ //Use original arrival to query train max speed
+
+ //Temporarily set arrival and departure
+ //Don't sync with database otherwise we could mess with stop order
+ //Sync at end when we calculated all new times
+ //If a previous stop gets postponed it appears after current stop
+ //until we postpone also current stop. This temp reorder breaks the
+ //query for retrieving train speed because this query is based on sorting by time
+
+ //Get train speed
+ q.bind(1, mJobId);
+ q.bind(2, item.arrival);
+ q.step();
+ int speedKmH = q.getRows().get(0);
+ q.reset();
+
+ //If line is slower or we couldn't get trains speed (likely no RS coupled) use line max speed instead
+ if(speed <= 0 || (lineSpeed >= 0 && lineSpeed < speed))
+ speed = lineSpeed;
+
+ double distanceMeters = linesModel->getStationsDistance(lineId, prevStId, item.stationId);
+
+ if(qFuzzyIsNull(distance) || speed < 1.0)
+ {
+ //Error
+ }
+
+ const double secs = (distanceMeters + accelerationDistMeters)/double(speed) * 3.6;
+ int roundedTop = qCeil(secs) + offsetSecs;
+
+ if(roundedTop < 60)
+ {
+ //At least 1 minute between stops
+ roundedTop = 60;
+ }
+ else
+ {
+ //Align to minutes, we don't support seconds in train scheduled times
+ int rem = roundedTop % 60;
+ if(rem > 10) //TODO: maybe 20 as treshold, sync calcTimeBetweenStations()
+ roundedTop += 60 - rem; //Round to next minute
+ else
+ roundedTop -= rem;
+ }
+
+ int stopTime = item.arrival.secsTo(item.departure);
+ item.arrival = prevDep.addSecs(roundedTop);
+ item.departure = prevDep.addSecs(roundedTop + stopTime);
+
+ prevStId = item.stationId;
+ prevDep = item.departure;
+ }
+
+ //Now prepare query to set Arrival and departure
+ q.prepare("UPDATE stops SET arrival=?,departure=? WHERE id=?");
+
+ for(int idx = firstIdx; idx < stops.size() - 1; idx++)
+ {
+ //Now sync with database
+
+ const StopItem& item = stops.at(idx);
+ q.bind(1, item.arrival);
+ q.bind(2, item.departure);
+ q.bind(3, item.stopId);
+ q.step();
+ q.reset();
+ }
+
+ //Finally inform the view
+ QModelIndex first = index(firstIdx, 0);
+ QModelIndex last = index(stops.size() - 1);
+ emit dataChanged(first, last);
+}
+#endif
+
+void StopModel::setStation_internal(StopItem& item, db_id stId, db_id nodeId)
+{
+ int platf = 0;
+ //Check if platform is out of range
+ query q_getDefPlatf(mDb, "SELECT defplatf_freight,defplatf_passenger FROM stations WHERE id=?");
+ q_getDefPlatf.bind(1, stId);
+ if(q_getDefPlatf.step() != SQLITE_ROW)
+ {
+ //Error
+ }
+ auto r = q_getDefPlatf.getRows();
+ const int defPlatf_freight = r.get(0);
+ const int defPlatf_passenger = r.get(1);
+
+ platf = category >= FirstPassengerCategory ?
+ defPlatf_passenger : defPlatf_freight;
+
+
+ q_setPlatform.bind(1, platf);
+ q_setPlatform.bind(2, item.stopId);
+ q_setPlatform.execute();
+ q_setPlatform.reset();
+ item.platform = platf;
+
+ //Mark for update both old and new station
+ if(item.stationId)
+ stationsToUpdate.insert(item.stationId);
+ if(stId)
+ stationsToUpdate.insert(stId);
+
+ item.stationId = stId;
+
+ q_setStopSt.bind(1, stId);
+ if(nodeId)
+ q_setStopSt.bind(2, nodeId);
+ else
+ q_setStopSt.bind(2, null_type{}); //Bind NULL instead of 0
+ q_setStopSt.bind(3, item.stopId);
+ q_setStopSt.execute();
+ q_setStopSt.reset();
+}
+
+void StopModel::shiftStopsBy24hoursFrom(const QTime &startTime)
+{
+ //HACK: when applying msecOffset to stops query might not work because there might be a (next) stop with the same arrival
+ // that is being set and thus SQL hits UNIQUE constraint and rejects the modification
+ //SOLUTION: shift all subsequent stops by 24 hours so there will be no conflicts and then reset the time once at a time
+ // so in the end they all will have correct time (no need to shift backwards)
+
+ command q_shiftArrDep(mDb, "UPDATE stops SET arrival=arrival+?1,departure=departure+?1 WHERE jobId=?2 AND arrival>?3");
+ const int shiftMin = 24 * 60; //Shift by 24h
+ q_shiftArrDep.bind(1, shiftMin);
+ q_shiftArrDep.bind(2, mJobId);
+ q_shiftArrDep.bind(3, startTime);
+ q_shiftArrDep.execute();
+ q_shiftArrDep.finish();
+}
+
+void StopModel::setStation(const QPersistentModelIndex& idx, db_id stId)
+{
+ if(!idx.isValid() || idx.row() >= stops.count())
+ return;
+
+ StopItem& s = stops[idx.row()];
+ if(s.stationId == stId)
+ return;
+
+ startStopsEditing();
+
+ q_getRwNode.bind(1, s.segment);
+ q_getRwNode.bind(2, stId);
+ q_getRwNode.step();
+ db_id nodeId = q_getRwNode.getRows().get(0);
+ q_getRwNode.reset();
+
+ setStation_internal(s, stId, nodeId);
+
+ emit dataChanged(idx, idx);
+
+ //NOTE: Here 'idx' may change so we use QPersistentModelIndex
+ if(autoInsertTransits)
+ {
+ insertTransitsBefore(idx);
+ }
+
+ //Calculate time after transits have been inserted in beetween
+ if(timeCalcEnabled)
+ {
+ int prevIdx = idx.row() - 1;
+ if(prevIdx >= 0)
+ {
+ const StopItem& prev = stops[prevIdx];
+ QTime arrival = prev.departure;
+
+ //Add travel duration (At least 60 secs)
+ arrival = arrival.addSecs(qMax(60, calcTimeBetweenStInSecs(prev.stationId, stId, s.curLine)));
+ int secs = arrival.second();
+ if(secs > 10)
+ {
+ //Round seconds to next minute
+ arrival = arrival.addSecs(60 - secs);
+ }
+ else
+ {
+ //Round seconds to previous minute
+ arrival = arrival.addSecs(-secs);
+ }
+
+ setArrival(idx, arrival, true);
+ }
+ }
+}
+
+void StopModel::setPlatform(const QModelIndex& idx, int platf)
+{
+ if(!idx.isValid() || idx.row() >= stops.count())
+ return;
+
+ StopItem& s = stops[idx.row()];
+ if(s.platform == platf)
+ return;
+
+ int platfCount = 0;
+ int depotCount = 0;
+ getStationPlatfCount(s.stationId, platfCount, depotCount);
+
+ //Check if it's valid
+ if(platf < 0)
+ {
+ //Depot platform
+ if(platf < -depotCount)
+ return; //Out of range
+ //Note: only '>' because they start from '-1', '-2' and so on
+ //Index 0 is a main platform
+ }
+ else
+ {
+ //Main platform
+ if(platf >= platfCount)
+ return; //Out of range
+ //Note: '>=' because they start from '0', '1' and so on
+ }
+
+ startStopsEditing();
+
+ //Mark the station for update
+ stationsToUpdate.insert(s.stationId);
+
+ s.platform = platf;
+ q_setPlatform.bind(1, s.platform);
+ q_setPlatform.bind(2, s.stopId);
+ q_setPlatform.execute();
+ q_setPlatform.reset();
+
+ emit dataChanged(idx, idx);
+}
+
+bool StopModel::isAddHere(const QModelIndex &idx)
+{
+ if(idx.isValid() && idx.row() < stops.count())
+ return (stops[idx.row()].addHere != 0);
+ return false;
+}
+
+void StopModel::removeStop(const QModelIndex &idx)
+{
+ const int row = idx.row();
+
+ if(!idx.isValid() && row >= stops.count())
+ return;
+
+ StopItem& s = stops[row];
+ if(s.addHere != 0)
+ return;
+
+ startStopsEditing();
+
+ //Mark the station for update
+ if(s.stationId)
+ stationsToUpdate.insert(s.stationId);
+
+ //BIG TODO: refactor code (Too many if/else) and emit dataChanged signal
+
+ //Handle special cases: remove First or remove Last BIG TODO
+
+ if(stops.count() == 2) //First + AddHere
+ {
+ //Special case:
+ //Remove First but we don't need to update next stops because there aren't
+ //After this operation there is only the AddHere
+
+ beginRemoveRows(QModelIndex(), row, row);
+
+ deleteStop(s.stopId);
+ destroySegment(s.segment, mJobId); //We were last stop so remove segment
+ stops.removeAt(row);
+
+ endRemoveRows();
+ return;
+ }
+
+ if(s.type == First)
+ {
+ beginRemoveRows(QModelIndex(), row, row);
+
+ QModelIndex nextIdx = index(row + 1, 0);
+ StopItem& next = stops[nextIdx.row()];
+ next.type = First;
+ setDeparture(nextIdx, next.departure, true);
+
+ if(next.nextLine != 0)
+ {
+ //There is a line change so this becomes First line
+ //And we discard oldFirst line
+
+ setStopSeg(next, next.nextSegment);
+ setNextSeg(next, 0); //Reset nextSeg
+ next.curLine = 0;
+
+ //nextLine stays as is because it's First
+
+ //Destroy oldFirst segment because now it's empty
+ destroySegment(s.segment, mJobId);
+ }
+ else
+ {
+ next.nextLine = next.curLine;
+ next.curLine = 0; //Don't destroySegment
+ }
+
+ deleteStop(s.stopId);
+ stops.removeAt(row);
+
+ endRemoveRows();
+
+ QModelIndex newFirstIdx = index(0, 0);
+ emit dataChanged(newFirstIdx, newFirstIdx); //Update new First Stop
+ }
+ else if (s.type == Last)
+ {
+ QModelIndex prevIdx = index(row - 1, idx.column());
+ StopItem& prev = stops[prevIdx.row()];
+
+ if(autoMoveUncoupleToNewLast)
+ {
+ //We are new last stop and previous is not First (>= 1)
+ //Move couplings from former last stop (now removed) to new last stop (former last but one)
+ command q_moveUncoupled(mDb, "UPDATE OR IGNORE coupling SET stopId=? WHERE stopId=?");
+ q_moveUncoupled.bind(1, prev.stopId);
+ q_moveUncoupled.bind(2, s.stopId);
+ int ret = q_moveUncoupled.execute();
+ if(ret != SQLITE_OK)
+ {
+ qDebug() << "Error shifting uncoupling from stop:" << s.stopId << "to:" << prev.stopId << "Job:" << mJobId
+ << "err:" << ret << mDb.error_msg();
+
+ }
+
+ //Select them to update them
+ query q_selectMoved(mDb, "SELECT rsId FROM coupling WHERE stopId=?");
+ q_selectMoved.bind(1, prev.stopId);
+ for(auto rs : q_selectMoved)
+ {
+ db_id rsId = rs.get(0);
+ rsToUpdate.insert(rsId);
+ }
+ }
+
+ if(prev.type != First)
+ {
+ //Set previous stop to be Last
+ //unless it's First: First remains of type Fisrt obviuosly
+
+ //Reset Departure so it's equal to Arrival
+ //Before setting stop type = Last, otherwise setDeparture doesn't work
+ setDeparture(prevIdx, prev.arrival, false);
+ setStopType(prevIdx, Last);
+ }
+
+ if(prev.nextLine == 0 || prev.type == First)
+ {
+ //If nextLine == 0 or previous is First
+ //we just delete this stop leaving prev segment untouched.
+ beginRemoveRows(QModelIndex(), row, row);
+ deleteStop(s.stopId);
+ stops.removeAt(row);
+ endRemoveRows();
+ }
+ else
+ {
+ //Prev stop changes line so we are in a new segment.
+ //When we delete this stop, the segment becomes useless so we destroy it.
+ beginRemoveRows(QModelIndex(), row, row);
+
+ db_id nextSeg = prev.nextSegment;
+
+ setNextSeg(prev, 0); //Reset nextSegment of prev stop
+ prev.nextLine = 0;
+
+ destroySegment(nextSeg, mJobId);
+ deleteStop(s.stopId);
+ stops.removeAt(row);
+
+ endRemoveRows();
+ }
+
+ emit dataChanged(prevIdx, prevIdx); //Update previous stop
+ }
+ else
+ {
+ //BIG TODO: what if 'prev' is a transit???
+ //Maybe we should go up to a non-transit stop?
+ //Or set transit-linechange?
+
+ //QModelIndex prevIdx = index(row - 1, 0);
+ //StopItem& prev = stops[prevIdx.row()];
+ }
+}
+
+void StopModel::insertStopBefore(const QModelIndex &idx)
+{
+ if(!idx.isValid() && idx.row() >= stops.count())
+ return;
+
+ const int row = idx.row();
+
+ //Use a pointer because QVector may realloc so a reference would became invalid
+ StopItem* s = &stops[row];
+ if(s->addHere != 0)
+ return;
+
+ startStopsEditing();
+
+ //Handle special case: insert before First BIG TODO
+ if(s->type == First)
+ {
+ beginInsertRows(QModelIndex(), 0, 0);
+ stops.insert(0, StopItem());
+ endInsertRows();
+
+ const QModelIndex oldFirstIdx = index(1, 0);
+
+ StopItem& oldFirst = stops[1];
+ StopItem& first = stops[0];
+
+ first.type = First;
+ oldFirst.type = Normal;
+
+ first.addHere = 0;
+ first.arrival = oldFirst.arrival;
+
+ first.segment = oldFirst.segment;
+
+ first.nextLine = oldFirst.nextLine;
+ oldFirst.curLine = first.nextLine;
+ oldFirst.nextLine = 0;
+
+ first.stopId = createStop(mJobId, first.segment, first.arrival);
+
+ //Shift next stops by 1 minute
+ setArrival(oldFirstIdx, first.arrival.addSecs(60), true);
+
+ emit dataChanged(index(0, 0),
+ oldFirstIdx);
+ }
+ else
+ {
+ beginInsertRows(QModelIndex(), row, row);
+ stops.insert(row, StopItem());
+ endInsertRows();
+ s = &stops[row + 1];
+ StopItem& new_stop = stops[row];
+ const StopItem& prev = stops[row - 1];
+
+ const QTime arr = prev.departure.addSecs(60);
+ new_stop.arrival = arr;
+ new_stop.segment = s->segment;
+
+ new_stop.stopId = createStop(mJobId, new_stop.segment, new_stop.arrival);
+ new_stop.nextLine = 0;
+ new_stop.curLine = s->curLine;
+ new_stop.addHere = 0;
+ new_stop.type = Normal;
+
+ //Set stop time and shift next stops
+ int secs = defaultStopTimeSec();
+
+ if(secs == 0)
+ {
+ //If secs is 0, trasform stop in Transit
+ setStopType(idx, Transit);
+ }
+ else
+ {
+ setDeparture(idx, arr.addSecs(secs), true);
+ }
+
+ emit dataChanged(idx,
+ idx);
+ }
+}
+
+JobCategory StopModel::getCategory() const
+{
+ return category;
+}
+
+void StopModel::setCategory(int value)
+{
+ if(int(category) == value)
+ return;
+
+ startInfoEditing();
+
+ category = JobCategory(value);
+ emit categoryChanged(int(category));
+}
+
+bool StopModel::setNewJobId(db_id jobId)
+{
+ if(mNewJobId == jobId)
+ return true;
+
+ if(jobId != mJobId)
+ {
+ //If setting a different id than original, check if it's already existent
+ query q_getJob(mDb, "SELECT 1 FROM jobs WHERE id=?");
+ q_getJob.bind(1, jobId);
+ int ret = q_getJob.step();
+ if(ret == SQLITE_ROW)
+ {
+ //Already exists, revert back to previous job id
+ emit jobIdChanged(mNewJobId);
+ return false;
+ }
+ }
+
+ //The new job id is valid
+ startInfoEditing();
+ mNewJobId = jobId;
+ emit jobIdChanged(mNewJobId);
+ return true;
+}
+
+db_id StopModel::getJobId() const
+{
+ return mJobId;
+}
+
+void StopModel::setNewShiftId(db_id shiftId)
+{
+ if(newShiftId == shiftId)
+ return;
+
+ if(stops.count() < 3) //First + Last + AddHere
+ {
+ emit errorSetShiftWithoutStops();
+ return;
+ }
+
+ startInfoEditing();
+
+ newShiftId = shiftId;
+
+ emit jobShiftChanged(newShiftId);
+}
+
+//Called for example when changing a job's shift from the ShiftGraphEditor
+void StopModel::onExternalShiftChange(db_id shiftId, db_id jobId)
+{
+ if(jobId == mJobId)
+ {
+ //Don't start stop/info editing because the change was already made by JobsModel
+ //Prevent discarding the change by updating also original shift
+ jobShiftId = newShiftId = shiftId;
+
+ emit jobShiftChanged(jobShiftId);
+ }
+}
+
+void StopModel::onShiftNameChanged(db_id shiftId)
+{
+ if(newShiftId == shiftId)
+ {
+ emit jobShiftChanged(newShiftId);
+ }
+}
+
+void StopModel::onStationLineNameChanged()
+{
+ //Stations and line names are fetched by delegate while painting
+ //We just need to repaint
+ QModelIndex start = index(0, 0);
+ QModelIndex end = index(stops.count(), 0);
+ emit dataChanged(start, end);
+}
+
+bool StopModel::isEdited() const
+{
+ return editState != NotEditing;
+}
+
+db_id StopModel::getNewShiftId() const
+{
+ return newShiftId;
+}
+
+db_id StopModel::getJobShiftId() const
+{
+ return jobShiftId;
+}
+
+QString StopModel::getDescription(const StopItem& s) const
+{
+ if(s.addHere != 0)
+ return QString();
+
+ query q_getDescr(mDb, "SELECT description FROM stops WHERE id=?");
+ q_getDescr.bind(1, s.stopId);
+ q_getDescr.step();
+ const QString descr = q_getDescr.getRows().get(0);
+ q_getDescr.reset();
+
+ return descr;
+}
+
+void StopModel::setDescription(const QModelIndex& idx, const QString& descr)
+{
+ if(!idx.isValid() && idx.row() >= stops.count())
+ return;
+
+ StopItem& s = stops[idx.row()];
+ if(s.addHere != 0)
+ return;
+
+ startStopsEditing();
+
+ //Mark the station for update
+ stationsToUpdate.insert(s.stationId);
+
+ command q_setDescr(mDb, "UPDATE stops SET description=? WHERE id=?");
+ q_setDescr.bind(1, descr);
+ q_setDescr.bind(2, s.stopId);
+ q_setDescr.execute();
+ q_setDescr.finish();
+
+ emit dataChanged(idx, idx, {STOP_DESCR_ROLE});
+}
+
+void StopModel::setTimeCalcEnabled(bool value)
+{
+ timeCalcEnabled = value;
+}
+
+int StopModel::calcTimeBetweenStInSecs(db_id stA, db_id stB, db_id lineId)
+{
+ DEBUG_IMPORTANT_ENTRY;
+
+ query q(mDb, "SELECT max_speed FROM lines WHERE id=?");
+ q.bind(1, lineId);
+ if(q.step() != SQLITE_ROW)
+ return 0; //Error
+ const double speedKmH = q.getRows().get(0);
+
+ const double meters = lines::getStationsDistanceInMeters(mDb, lineId, stA, stB);
+
+ qDebug() << "Km:" << meters/1000.0 << "Speed:" << speedKmH;
+
+ if(qFuzzyIsNull(meters) || speedKmH < 1.0)
+ return 0; //Error
+
+ const double secs = (meters + accelerationDistMeters)/speedKmH * 3.6;
+ qDebug() << "Time:" << secs;
+ return qCeil(secs);
+}
+
+int StopModel::defaultStopTimeSec()
+{
+ //TODO: the prefernces should be stored also in database
+ return AppSettings.getDefaultStopMins(int(category)) * 60;
+}
+
+void StopModel::removeLastIfEmpty()
+{
+ if(stops.count() < 2) //Empty stop + AddHere TODO: count < 3
+ return;
+
+ int row = stops.count() - 2; //Last index (size - 1) is AddHere so we need 'size() - 2'
+
+ //Recursively remove empty stops
+ //GreaterEqual because we include First
+ while (row >= 0)
+ {
+ const StopItem& stop = stops[row];
+
+ if(stop.stationId == 0 || stop.segment == 0)
+ {
+ removeStop(index(row));
+ }
+ else
+ {
+ //This stop is valid so it will be the actual Last stop
+ //(Or First if we removed all rows but first one)
+ //So stop the loop
+ break;
+ }
+
+ //Try with previous stop
+ row--;
+ }
+}
+
+std::pair StopModel::getFirstLastTimes() const
+{
+ QTime first, last;
+
+ if(stops.count() > 1) //First + AddHere
+ {
+ first = stops[0].departure;
+ }
+
+ int row = stops.count() - 2; //Last indx (size - 1) is AddHere so we need 'size() - 2'
+
+ //GreaterEqual because we include First
+ //Recursively skip last stop if empty
+ while (row >= 0)
+ {
+ const StopItem& stop = stops[row];
+
+ if(stop.stationId != 0)
+ {
+ last = stop.arrival;
+ break;
+ }
+
+ //Try with previous stop
+ row--;
+ }
+
+ return std::make_pair(first, last);
+}
+
+QSet StopModel::getCoupled(int row) const
+{
+ if(row < 0 || row >= stops.size())
+ return QSet();
+ return stops.at(row).coupled;
+}
+
+QSet StopModel::getUncoupled(int row) const
+{
+ if(row < 0 || row >= stops.size())
+ return QSet();
+ return stops.at(row).uncoupled;
+}
+
+void StopModel::setAutoInsertTransits(bool value)
+{
+ autoInsertTransits = value;
+}
+
+void StopModel::setAutoMoveUncoupleToNewLast(bool value)
+{
+ autoMoveUncoupleToNewLast = value;
+}
+
+void StopModel::setAutoUncoupleAtLast(bool value)
+{
+ autoUncoupleAtLast = value;
+}
+
+void StopModel::insertTransitsBefore(const QPersistentModelIndex& stop)
+{
+ //TODO: should ask user when deleting transits to replace them
+ //TODO: to change path user could also change 'NextLine' in previous
+
+ if(stop.row() == 0) //It's First Stop, there is nothing before it.
+ return;
+ const StopItem to = stops.at(stop.row()); //Make a deep copy
+ db_id prevStId = 0;
+ QTime prevDep;
+
+ //Find previous stop (ignore transits in beetween)
+ int prevRow = stop.row();
+ for(; prevRow >= 0; prevRow--)
+ {
+ StopType t = stops.at(prevRow).type;
+ if(t == Normal || t == First || t == TransitLineChange)
+ {
+ prevStId = stops.at(prevRow).stationId;
+ prevDep = stops.at(prevRow).departure;
+ break;
+ }
+ }
+
+ int oldLastTransitRow = stop.row() - 1;
+ if(prevRow < oldLastTransitRow)
+ {
+ //Remove old transits in between
+ beginRemoveRows(QModelIndex(), prevRow + 1, oldLastTransitRow);
+
+ for(int i = prevRow + 1; i <= oldLastTransitRow; i++)
+ {
+ deleteStop(stops.at(i).stopId);
+ }
+
+ auto it = stops.begin() + prevRow + 1;
+ auto end = stops.begin() + oldLastTransitRow;
+ stops.erase(it, end + 1);
+
+ endRemoveRows();
+ }
+
+ //Count transits first
+ query q(mDb, "SELECT COUNT()"
+ " FROM railways r"
+ " JOIN railways fromSt ON fromSt.stationId=? AND fromSt.lineId=r.lineId"
+ " JOIN railways toSt ON toSt.stationId=? AND toSt.lineId=r.lineId"
+ " WHERE r.lineId=? AND"
+ " CASE WHEN fromSt.pos_meters < toSt.pos_meters"
+ " THEN (r.pos_meters < toSt.pos_meters AND r.pos_meters > fromSt.pos_meters)"
+ " ELSE (r.pos_meters > toSt.pos_meters AND r.pos_meters < fromSt.pos_meters)"
+ " END");
+ q.bind(1, prevStId);
+ q.bind(2, to.stationId);
+ q.bind(3, to.curLine);
+ q.step();
+ int count = q.getRows().get(0);
+ q.finish();
+
+ if(count == 0)
+ return; //No transits to insert
+
+ q.prepare("SELECT max_speed FROM lines WHERE id=?");
+ q.bind(1, to.curLine);
+ q.step();
+ const double speedKmH = qMax(1.0, q.getRows().get(0));
+ q.finish();
+
+ int curRow = prevRow + 1;
+ beginInsertRows(QModelIndex(), curRow, curRow + count - 1);
+
+ stops.insert(curRow, count, StopItem());
+
+ q.prepare("SELECT r.id, r.pos_meters, r.stationId, fromSt.pos_meters"
+ " FROM railways r"
+ " JOIN railways fromSt ON fromSt.stationId=? AND fromSt.lineId=r.lineId"
+ " JOIN railways toSt ON toSt.stationId=? AND toSt.lineId=r.lineId"
+ " WHERE r.lineId=? AND"
+ " CASE WHEN fromSt.pos_meters < toSt.pos_meters"
+ " THEN (r.pos_meters < toSt.pos_meters AND r.pos_meters > fromSt.pos_meters)"
+ " ELSE (r.pos_meters > toSt.pos_meters AND r.pos_meters < fromSt.pos_meters)"
+ " END"
+ " ORDER BY"
+ " CASE WHEN fromSt.pos_meters > toSt.pos_meters THEN r.pos_meters END DESC,"
+ " CASE WHEN fromSt.pos_meters < toSt.pos_meters THEN r.pos_meters END ASC");
+
+ q.bind(1, prevStId);
+ q.bind(2, to.stationId);
+ q.bind(3, to.curLine);
+
+ auto it = q.begin();
+
+ int oldKmMeters = (*it).get(3);
+
+ autoInsertTransits = false; //Prevent recursion from setStaton()
+ for(; it != q.end(); ++it)
+ {
+ auto r = *it;
+ db_id nodeId = r.get(0);
+ int kmInMeters = r.get(1);
+ db_id stId = r.get(2);
+
+ //qDebug() << "Km:" << km << "St:" << stationsModel->getStName(stId) << stId;
+
+ if(timeCalcEnabled)
+ {
+ int distanceMeters = qAbs(oldKmMeters - kmInMeters);
+ //Add travel duration (At least 60 secs)
+ const double secs = (distanceMeters + accelerationDistMeters)/speedKmH * 3.6;
+ prevDep = prevDep.addSecs(qMax(60, qCeil(secs)));
+
+ int reminder = prevDep.second();
+ if(reminder > 10)
+ {
+ //Round seconds to next minute
+ prevDep = prevDep.addSecs(60 - reminder);
+ }
+ else
+ {
+ //Round seconds to previous minute
+ prevDep = prevDep.addSecs(-reminder);
+ }
+
+ }
+ else
+ {
+ prevDep = prevDep.addSecs(60);
+ }
+
+ oldKmMeters = kmInMeters;
+
+ StopItem &item = stops[curRow];
+ item.type = Transit;
+ item.addHere = 0;
+ item.stationId = 0;
+ item.curLine = to.curLine;
+ item.segment = to.segment;
+ item.nextLine = 0;
+ item.nextSegment = 0;
+ item.platform = 0;
+ item.arrival = item.departure = prevDep;
+ item.stopId = createStop(mJobId, item.segment, item.arrival, Transit);
+
+ setStation_internal(item, stId, nodeId);
+
+ curRow++;
+ }
+ q.finish();
+ autoInsertTransits = true; //Re-enable flag
+
+ endInsertRows();
+
+ qDebug() << "End";
+}
+
+bool StopModel::startInfoEditing() //Faster than 'startStopEditing()' use this if change doesen.t affect stops
+{
+ if(editState != NotEditing)
+ return true;
+
+ if(!mJobId)
+ return false;
+
+ editState = InfoEditing;
+ emit edited(true);
+
+ return true;
+}
+
+
+//NOTE: this must be called before any edit to database in order to save previous state
+bool StopModel::startStopsEditing()
+{
+ if(editState == StopsEditing)
+ return true;
+
+ if(!mJobId)
+ return false;
+
+ bool alreadyEditing = editState == InfoEditing;
+ editState = StopsEditing;
+
+ //Backup jobsegments
+ command q_backupSegments(mDb, "INSERT INTO old_jobsegments SELECT * FROM jobsegments WHERE jobId=?");
+ q_backupSegments.bind(1, mJobId);
+ int ret = q_backupSegments.execute();
+ q_backupSegments.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qDebug() << "Error while saving old segments:" << ret << mDb.error_msg() << mDb.extended_error_code();
+ return false;
+ }
+
+ //Backup stops
+ command q_backupStops(mDb, "INSERT INTO old_stops SELECT * FROM stops WHERE jobId=?");
+ q_backupStops.bind(1, mJobId);
+ ret = q_backupStops.execute();
+ q_backupStops.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qDebug() << "Error while saving old stops:" << ret << mDb.error_msg() << mDb.extended_error_code();
+ return false;
+ }
+
+ //Backup couplings
+ command q_backupCouplings(mDb, "INSERT INTO old_coupling(id, stopId, rsId, operation)"
+ " SELECT coupling.id, coupling.stopId, coupling.rsId, coupling.operation"
+ " FROM coupling"
+ " JOIN stops ON stops.id=coupling.stopId WHERE stops.jobId=?");
+ q_backupCouplings.bind(1, mJobId);
+ ret = q_backupCouplings.execute();
+ q_backupCouplings.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qDebug() << "Error while saving old couplings:" << ret << mDb.error_msg() << mDb.extended_error_code();
+ return false;
+ }
+
+ if(!alreadyEditing)
+ emit edited(true);
+
+ return true;
+}
+
+bool StopModel::endStopsEditing()
+{
+ if(editState == NotEditing)
+ return false;
+
+ if(editState == StopsEditing)
+ {
+ //Clear old_stops (will automatically clear old_couplings with FK: ON DELETE CASCADE)
+ command q_clearOldStops(mDb, "DELETE FROM old_stops WHERE jobId=?");
+ q_clearOldStops.bind(1, mJobId);
+ int ret = q_clearOldStops.execute();
+ q_clearOldStops.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qDebug() << "Error while clearing old stops:" << ret << mDb.error_msg() << mDb.extended_error_code();
+ return false;
+ }
+
+ //Clear old_jobsegments
+ command q_clearOldSegments(mDb, "DELETE FROM old_jobsegments WHERE jobId=?");
+ q_clearOldSegments.bind(1, mJobId);
+ ret = q_clearOldSegments.execute();
+ q_clearOldSegments.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qDebug() << "Error while clearing old segments:" << ret << mDb.error_msg() << mDb.extended_error_code();
+ return false;
+ }
+ }
+
+ editState = NotEditing;
+
+ emit edited(false);
+
+ return true;
+}
+
+bool StopModel::commitChanges()
+{
+ if(editState == NotEditing)
+ return true;
+
+ command q(mDb, "UPDATE jobs SET category=?, shiftId=? WHERE id=?");
+ q.bind(1, int(category));
+ if(newShiftId)
+ q.bind(2, newShiftId);
+ else
+ q.bind(2); //NULL shift
+ q.bind(3, mJobId);
+ if(q.execute() != SQLITE_OK)
+ {
+ category = oldCategory;
+ emit categoryChanged(int(category));
+ newShiftId = jobShiftId;
+ emit jobShiftChanged(newShiftId);
+ }
+
+ db_id oldJobId = mJobId;
+
+ if(mNewJobId != mJobId)
+ {
+ q.prepare("UPDATE jobs SET id=? WHERE id=?");
+ q.bind(1, mNewJobId);
+ q.bind(2, mJobId);
+ int ret = q.execute();
+ if(ret == SQLITE_OK)
+ {
+ mJobId = mNewJobId;
+ }
+ else
+ {
+ //Reset to old value
+ mNewJobId = oldJobId;
+ emit jobIdChanged(mJobId);
+ }
+ }
+
+ oldCategory = category;
+
+ emit Session->shiftJobsChanged(jobShiftId, oldJobId);
+ emit Session->shiftJobsChanged(newShiftId, mNewJobId);
+ jobShiftId = newShiftId;
+
+ emit Session->jobChanged(mNewJobId, oldJobId);
+
+ rsToUpdate.clear();
+ stationsToUpdate.clear();
+
+ return endStopsEditing();
+}
+
+bool StopModel::revertChanges()
+{
+ if(editState == NotEditing)
+ return true;
+
+ bool needsStopReload = false;
+
+ if(editState == StopsEditing)
+ {
+ //Delete current data
+
+ //Clear stops (will automatically clear couplings with FK: ON DELETE CASCADE)
+ command q_clearCurStops(mDb, "DELETE FROM stops WHERE jobId=?");
+ q_clearCurStops.bind(1, mJobId);
+ int ret = q_clearCurStops.execute();
+ q_clearCurStops.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qDebug() << "Error while clearing current stops:" << ret << mDb.error_msg() << mDb.extended_error_code();
+ return false;
+ }
+
+ //Clear jobsegments
+ command q_clearCurSegments(mDb, "DELETE FROM jobsegments WHERE jobId=?");
+ q_clearCurSegments.bind(1, mJobId);
+ ret = q_clearCurSegments.execute();
+ q_clearCurSegments.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qDebug() << "Error while clearing current segments:" << ret << mDb.error_msg() << mDb.extended_error_code();
+ return false;
+ }
+
+ //Now restore old data
+
+ //Restore jobsegments
+ command q_restoreSegments(mDb, "INSERT INTO jobsegments SELECT * FROM old_jobsegments WHERE jobId=?");
+ q_restoreSegments.bind(1, mJobId);
+ ret = q_restoreSegments.execute();
+ q_restoreSegments.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qDebug() << "Error while restoring old segments:" << ret << mDb.error_msg() << mDb.extended_error_code();
+ return false;
+ }
+
+ //Restore stops
+ command q_restoreStops(mDb, "INSERT INTO stops SELECT * FROM old_stops WHERE jobId=?");
+ q_restoreStops.bind(1, mJobId);
+ ret = q_restoreStops.execute();
+ q_restoreStops.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qDebug() << "Error while restoring old stops:" << ret << mDb.error_msg() << mDb.extended_error_code();
+ return false;
+ }
+
+ //Restore couplings
+ command q_restoreCouplings(mDb, "INSERT INTO coupling(id, stopId, rsId, operation)"
+ " SELECT old_coupling.id, old_coupling.stopId, old_coupling.rsId, old_coupling.operation"
+ " FROM old_coupling"
+ " JOIN stops ON stops.id=old_coupling.stopId WHERE stops.jobId=?");
+ q_restoreCouplings.bind(1, mJobId);
+ ret = q_restoreCouplings.execute();
+ q_restoreCouplings.reset();
+
+ if(ret != SQLITE_OK)
+ {
+ qDebug() << "Error while restoring old couplings:" << ret << mDb.error_msg() << mDb.extended_error_code();
+ return false;
+ }
+
+ //Reload info and stops
+ needsStopReload = true;
+ }
+ else
+ {
+ //Just reset info in case of InfoEditing
+ mNewJobId = mJobId;
+ emit jobIdChanged(mJobId);
+
+ category = oldCategory;
+ emit categoryChanged(int(category));
+
+ newShiftId = jobShiftId;
+ emit jobShiftChanged(jobShiftId);
+
+ rsToUpdate.clear();
+ stationsToUpdate.clear();
+ }
+
+ bool ret = endStopsEditing();
+ if(!ret)
+ return ret;
+
+ if(needsStopReload)
+ loadJobStops(mJobId);
+
+ return true;
+}
+
+int StopModel::getStopRow(db_id stopId) const
+{
+ for(int row = 0; row < stops.size(); row++)
+ {
+ if(stops.at(row).stopId == stopId)
+ return row;
+ }
+
+ return -1;
+}
diff --git a/src/jobs/jobeditor/model/stopmodel.h b/src/jobs/jobeditor/model/stopmodel.h
new file mode 100644
index 0000000..33cbf6b
--- /dev/null
+++ b/src/jobs/jobeditor/model/stopmodel.h
@@ -0,0 +1,229 @@
+#ifndef STOPMODEL_H
+#define STOPMODEL_H
+
+#include
+#include
+
+#include
+#include
+
+#include "utils/model_roles.h"
+
+#include "utils/types.h"
+
+#include "sqlite3pp/sqlite3pp.h"
+using namespace sqlite3pp;
+
+class StopItem
+{
+public:
+ db_id stopId = 0;
+ db_id stationId = 0;
+
+ db_id segment = 0;
+ db_id nextSegment = 0;
+
+ db_id curLine = 0;
+ db_id nextLine = 0;
+
+ db_id possibleLine = 0;
+ int addHere = 0;
+ int platform = 0;
+
+ QTime arrival;
+ QTime departure;
+
+ QSet coupled; //TODO: really needed???
+ QSet uncoupled;
+
+ StopType type = Normal;
+};
+
+//BIG TODO: when changing arrival to a station where a RS is (un)coupled, the station is marked for update but not the RS
+// if a stop is removed, couplings get removed too but RS are not marked for update, also if Job is removed, needs also RsErrorCheck
+class StopModel : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ StopModel(sqlite3pp::database& db, QObject *parent = nullptr);
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role) override;
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+
+ typedef enum
+ {
+ NoError = 0,
+ GenericError = 1,
+ ErrorInvalidIndex,
+ ErrorInvalidArgument,
+ ErrorFirstLastTransit,
+ ErrorTransitWithCouplings
+ } ErrorCodes;
+
+ void setTimeCalcEnabled(bool value);
+ void setAutoInsertTransits(bool value);
+ void setAutoMoveUncoupleToNewLast(bool value);
+
+ void prepareQueries();
+ void finalizeQueries();
+
+ void loadJobStops(db_id jobId);
+ void clearJob();
+
+ void addStop();
+ void insertStopBefore(const QModelIndex& idx);
+ void removeStop(const QModelIndex& idx);
+ void removeLastIfEmpty();
+
+ bool isAddHere(const QModelIndex& idx);
+
+ void setArrival(const QModelIndex &idx, const QTime &time, bool setDepTime);
+ void setDeparture(const QModelIndex &index, QTime time, bool propagate);
+ int setStopType(const QModelIndex &idx, StopType type);
+ int setStopTypeRange(int firstRow, int lastRow, StopType type);
+ void setLine(const QModelIndex &idx, db_id lineId);
+ bool lineHasSt(db_id lineId, db_id stId);
+ void setStation(const QPersistentModelIndex &idx, db_id stId);
+ void setPlatform(const QModelIndex &idx, int platf);
+
+ QString getDescription(const StopItem &s) const;
+ void setDescription(const QModelIndex &idx, const QString &descr);
+
+ int calcTimeBetweenStInSecs(db_id stA, db_id stB, db_id lineId);
+ int defaultStopTimeSec();
+
+ std::pair getFirstLastTimes() const;
+
+ //TODO: seems useless
+ QSet